zs3-ui
v1.0.2
Published
Modern Web Components library with built-in theming system and CSS variables support
Maintainers
Readme
ZS3 UI
v1.0.0 · Modern Web Components library with built-in theming, i18n, reactive state, HTTP client, touch support, and more.
ZS3 UI Components is a zero-dependency UI library based on native Web Components (Custom Elements + Shadow DOM). It provides a complete set of UI components and utilities that work in any framework or in plain HTML.
Table of Contents
- Features
- Installation
- Dev Setup
- Two Usage Approaches
- i18n — Internationalisation
- Theme System
- Touch Device Support
- Components
- Utilities
- Decorators
- CSS Variables
Features
- Web Components natifs — Shadow DOM, Custom Elements, cap dependència de framework
- i18n complet — traduccions per clau, fallback, detecció de idioma del navegador, actualització automàtica de tots els components
- Temes — light, dark, high-contrast, i temes personalitzats via CSS variables
- Suport tàctil complet — drag, float i resize funcionen tant amb ratolí com amb pantalla tàctil
- Resize — qualsevol component pot ser redimensionable per les 4 cantonades; integrat a
zs3-window - $Store — gestió d'estat reactiva amb reducers i subscripcions (similar a Redux)
- EventBus — comunicació desacoblada entre components
- HttpClient — client HTTP amb interceptors, timeout i tipatge TypeScript
- DIContainer — contenidor d'injecció de dependències
- $Storage — wrapper de LocalStorage amb serialització automàtica
- $Log — logging avançat amb nivells i formatació
- $ Helper — selecció i manipulació del DOM
- Formularis —
$Formgenèric i$LoginForm,$RegisterForm,$RecoverPasswordFormpreconstruïts - Validators — funcions pures de validació (email, contrasenya, DNI, IBAN, telèfon...) composables via
validate()ivalidateAll()
Installation
npm install zs3-uiImporta els components i el CSS:
import 'zs3-ui' // registra tots els Custom Elements
import 'zs3-ui/dist/zs3.css' // variables CSS i estils baseO importa selectivament:
import { $Button, $Form, $ModalDialog, i18n, $Store, themeManager } from 'zs3-ui'Dev Setup
git clone https://github.com/your-org/zs3-ui
cd zs3-ui
npm install
npm run dev # servidor de dev a http://localhost:5173
npm run build # build de la llibreria a dist/
npm run lint # ESLint + stylelintEl directori dev/ conté tres pàgines de demo:
| Pàgina | Descripció |
|--------|------------|
| dev/index.html | Demo original (layout amb navegació) |
| dev/index2.html | Demo completa — HTML inline + JavaScript |
| dev/index3.html | Demo completa — tot creat des de TypeScript |
Two Usage Approaches
ZS3 UI suporta dos estils d'ús que es poden combinar lliurement.
Enfocament 1 — HTML Inline (index2.html / index2.ts)
Defineix els components directament a l'HTML. Útil quan treballes amb plantilles HTML, SSR o CMS.
index2.html:
<!doctype html>
<html lang="ca">
<head>
<meta charset="UTF-8" />
<title>Demo 2 — Inline</title>
</head>
<body>
<header>
<zs3-select-locale zs3-position="position:relative;"></zs3-select-locale>
<zs3-select-theme zs3-position="position:relative;"></zs3-select-theme>
</header>
<!-- Component declarat en HTML -->
<zs3-button variant="primary" icon="save" position="left"
zs3-click="document.getElementById('out').textContent = 'Clicat!'">
Guardar
</zs3-button>
<div id="out"></div>
<!-- Modal declarat en HTML -->
<zs3-modal id="my-modal" size="medium" backdrop close-on-escape>
<div style="padding:1.5rem">
<h3>Modal</h3>
<p>Contingut del modal.</p>
<zs3-button variant="primary"
zs3-click="document.getElementById('my-modal').$.hide()">
Tancar
</zs3-button>
</div>
</zs3-modal>
<zs3-button zs3-click="document.getElementById('my-modal').$.show()">
Obrir modal
</zs3-button>
<script type="module" src="./src/js/index2.ts"></script>
</body>
</html>index2.ts — inicialitza i18n, $Store, HttpClient, i exposa funcions globals:
import '../../../src/css/zs3.css'
import { i18n, themeManager, log, storage, eventBus,
$Store, HttpClient, diContainer,
$ModalDialog, $Notification, $Icon } from '../../../src'
import translations from '../../i18n/i18n.json'
// Inicialitza i18n
i18n.loadMultiple(translations)
i18n.init({ locale: 'ca', fallbackLocale: 'en' })
// Exposa funcions globals per als handlers inline de l'HTML
window.showDialogAlert = async () => {
await $ModalDialog.alertI18n({
i18nTitle: 'section.dialog.alert.title',
i18nMessage: 'section.dialog.alert.message',
})
}
window.addEventListener('DOMContentLoaded', () => {
log.info('Demo 2 iniciada')
setupIconsGrid()
})
function setupIconsGrid(): void {
const grid = document.getElementById('icons-grid')
if (!grid) return
$Icon.getAvailableIcons().forEach((name) => {
const card = document.createElement('div')
card.className = 'demo-icon-card'
card.innerHTML = `<zs3-icon name="${name}" size="medium"></zs3-icon><span>${name}</span>`
card.addEventListener('click', () =>
$Notification.info(`Icona: ${name}`, { position: 'bottom-right', duration: 2000 })
)
grid.appendChild(card)
})
}Enfocament 2 — TypeScript Programàtic (index3.html / index3.ts)
Crea tots els components i la interfície des de TypeScript. Ideal per SPA, aplicacions dinàmiques o quan es vol control total sense HTML preescrit.
index3.html — mínim:
<!doctype html>
<html lang="ca">
<head>
<meta charset="UTF-8" />
<title>Demo 3 — TypeScript</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/js/index3.ts"></script>
</body>
</html>index3.ts — construeix tota la UI programàticament:
import '../../../src/css/zs3.css'
import { i18n, $Button, $Icon, $Form, $ModalDialog,
$Notification, $Store, HttpClient, eventBus,
storage, log, diContainer, themeManager,
$Modal, $Toolbar, $LoginForm, $RegisterForm,
$RecoverPasswordForm, $Window, $ } from '../../../src'
import translations from '../../i18n/i18n.json'
i18n.loadMultiple(translations)
i18n.init({ locale: 'ca', fallbackLocale: 'en' })
// Helper per crear elements HTML tipats
function el<K extends keyof HTMLElementTagNameMap>(
tag: K,
attrs: Record<string, string> = {},
...children: (Node | string)[]
): HTMLElementTagNameMap[K] {
const e = document.createElement(tag)
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v)
for (const c of children) e.append(c)
return e
}
// Helper per crear botons
function btn(text: string, variant?: string, icon?: string, position?: string): $Button {
const b = new $Button()
b.textContent = text
if (variant) b.variant = variant as $Button['variant']
if (icon) b.icon = icon
if (position) b.position = position as 'left' | 'right'
return b
}
window.addEventListener('DOMContentLoaded', () => {
injectStyles()
const app = document.getElementById('app')!
app.append(buildHeader())
const main = el('main', { class: 'demo-main' })
main.append(
buildButtons(),
buildIcons(),
buildToolbar(),
buildNotifications(),
buildModal(),
buildModalDialog(),
buildForm(),
buildAuthForms(),
buildWindow(),
buildUtilities(),
)
app.append(main)
})i18n — Internationalisation
El sistema i18n de ZS3 UI és complet i integrat a tots els components. Gestiona traduccions per clau, locale de fallback, persistència a localStorage, detecció automàtica del navegador, i actualització automàtica de tots els components en canviar d'idioma.
Fitxer de traduccions
Les traduccions s'organitzen per idioma en un fitxer JSON:
{
"ca": {
"app.title": "ZS3 Components",
"section.auth.login": "Iniciar sessió",
"section.auth.register": "Registrar-se",
"section.auth.recover": "Recuperar contrasenya",
"form.title": "Formulari de registre",
"form.description": "Completa el formulari per registrar-te.",
"form.field.name": "Nom",
"form.field.name.placeholder": "Introdueix el teu nom",
"form.field.email": "Correu electrònic",
"form.field.country": "País",
"form.field.country.placeholder": "Selecciona un país",
"form.country.spain": "Espanya",
"form.country.france": "França",
"section.dialog.alert.title": "Títol de l'alerta",
"section.dialog.alert.message": "Aquest és un missatge d'alerta important."
},
"en": {
"app.title": "ZS3 Components",
"section.auth.login": "Sign in",
"section.auth.register": "Register",
"section.auth.recover": "Recover password",
"form.title": "Registration form",
"form.description": "Complete the form to register.",
"form.field.name": "Name",
"form.field.name.placeholder": "Enter your name",
"form.field.email": "Email",
"form.field.country": "Country",
"form.field.country.placeholder": "Select a country",
"form.country.spain": "Spain",
"form.country.france": "France",
"section.dialog.alert.title": "Alert title",
"section.dialog.alert.message": "This is an important alert message."
}
}Inicialització
import { i18n } from 'zs3-ui'
import translations from './i18n.json'
// Carrega totes les traduccions d'una vegada
i18n.loadMultiple(translations)
// Inicialitza amb locale per defecte i fallback
i18n.init({
locale: 'ca',
fallbackLocale: 'en',
})init() també accepta translations directament:
i18n.init({
locale: 'ca',
fallbackLocale: 'en',
translations: {
ca: { 'hello': 'Hola' },
en: { 'hello': 'Hello' },
},
})API de l'objecte i18n
| Mètode | Descripció |
|--------|------------|
| i18n.init(config) | Inicialitza amb locale, fallback i traduccions |
| i18n.loadMultiple(obj) | Carrega un objecte { locale: { clau: valor } } |
| i18n.t('clau') | Retorna la traducció de la clau |
| i18n.setLocale('en') | Canvia l'idioma i actualitza tots els components |
| i18n.getLocale() | Retorna l'idioma actiu |
| i18n.updateElements() | Força l'actualització de tots els elements del document |
| i18n.updateRoot(shadowRoot) | Actualitza els elements dins d'un shadow root específic |
// Traduir una clau
const text = i18n.t('form.field.name') // → "Nom"
// Canviar idioma (actualitza tots els components automàticament)
i18n.setLocale('en')
// Obtenir l'idioma actiu
console.log(i18n.getLocale()) // → "en"Escolta el canvi d'idioma
// Enfocament HTML/inline (index2.ts)
window.addEventListener('zs3-locale-change', () => {
log.info(`Idioma canviat a: ${i18n.getLocale()}`)
})
// Actualitzar text de botons programàtics quan canvia l'idioma
window.addEventListener('zs3-locale-change', () => {
bLogin.setText(i18n.t('section.auth.login'))
bRegister.setText(i18n.t('section.auth.register'))
})data-i18n — Traduccions en HTML
Afegeix data-i18n="clau" a qualsevol element i el seu textContent s'actualitzarà automàticament en canviar l'idioma:
<!-- El text s'actualitza sol en canviar l'idioma -->
<h1 data-i18n="app.title">ZS3 Components</h1>
<p data-i18n="form.description">Text per defecte</p>Per traduir atributs (aria-label, placeholder, title…):
<!-- Tradueix l'atribut aria-label -->
<zs3-icon name="bell" data-i18n-attr='{"aria-label":"nav.notifications"}'></zs3-icon>
<!-- Tradueix el placeholder -->
<input data-i18n-attr='{"placeholder":"form.field.name.placeholder"}' />Tots dos sistemes funcionen dins de Shadow DOM — i18n.updateElements() escaneja automàticament els shadow roots de tots els components registrats.
i18n en components TypeScript
Tots els components que extenen $BaseComponent exposen el mètode this.t(key):
// Dins d'un component personalitzat
class MyComponent extends $BaseComponent {
protected render(): void {
const label = this.t('my.label.key') // traducció automàtica
this.setHTML(`<button>${label}</button>`)
}
}i18n en atributs de components
Tots els components accepten atributs i18n-* per les seves propietats de text:
<!-- HTML inline -->
<zs3-form
i18n-title="form.title"
i18n-description="form.description"
i18n-accept-text="zs3.form.accept"
i18n-cancel-text="zs3.form.cancel">
</zs3-form>
<zs3-modal-dialog
i18n-title="section.dialog.confirm.title"
i18n-message="section.dialog.confirm.message"
i18n-accept-text="zs3.dialog.accept"
i18n-cancel-text="zs3.dialog.cancel">
</zs3-modal-dialog>
<zs3-window i18n-title="form.title"></zs3-window>
<zs3-notification i18n-title="section.notification.success.title"
i18n-message="section.notification.success.message">
</zs3-notification>// TypeScript programàtic (index3.ts)
const form = new $Form()
form.setAttribute('i18n-title', 'form.title')
form.setAttribute('i18n-description', 'form.description')
const dialog = new $ModalDialog()
dialog.setAttribute('i18n-title', 'section.dialog.confirm.title')
dialog.setAttribute('i18n-message', 'section.dialog.confirm.message')
dialog.setAttribute('i18n-accept-text', 'zs3.dialog.accept')
const win = new $Window()
win.setAttribute('i18n-title', 'form.title')Traduccions internes del framework
El framework inclou traduccions per defecte per als textos interns dels components (botons, etiquetes, etc.):
import { defaultTranslations } from 'zs3-ui'
// Les claus internes del framework comencen per "zs3."
// zs3.form.accept, zs3.form.cancel
// zs3.dialog.accept, zs3.dialog.cancel, zs3.dialog.close
// zs3.login.title, zs3.login.email, zs3.login.password, zs3.login.submit
// zs3.register.title, zs3.register.submit
// zs3.recover.title, zs3.recover.submitPots sobreescriure qualsevol clau interna carregant les teves pròpies traduccions amb i18n.loadMultiple().
Detecció automàtica del navegador
Si no s'especifica locale a init(), el sistema detecta l'idioma del navegador:
// Detecta automàticament: navigator.language → 'ca-ES' → 'ca'
i18n.init({ fallbackLocale: 'en' })Selector d'idioma integrat
El component <zs3-select-locale> mostra un selector d'idioma que crida i18n.setLocale() automàticament:
<!-- HTML -->
<zs3-select-locale zs3-position="position:relative;"></zs3-select-locale>// TypeScript (index3.ts)
const locale = document.createElement('zs3-select-locale')
locale.setAttribute('zs3-position', 'position:relative;')
header.append(locale)Theme System
Temes disponibles
| Tema | Valor |
|------|-------|
| Clar (default) | light |
| Fosc | dark |
| Alt contrast | high-contrast |
API themeManager
import { themeManager } from 'zs3-ui'
themeManager.setTheme('dark') // canvia el tema
themeManager.getTheme() // → 'dark'
themeManager.isDark() // → true
themeManager.toggleDark() // alterna light/darkEscolta canvis de tema
// HTML/inline (index2.ts)
window.addEventListener('zs3-theme-change', () => {
const theme = themeManager.getTheme()
const iconEl = document.querySelector('#theme-indicator zs3-icon')
iconEl?.setAttribute('name', themeManager.isDark() ? 'moon' : 'sun')
})
// TypeScript (index3.ts)
window.addEventListener('zs3-theme-change', () => {
if (nameEl) nameEl.textContent = themeManager.getTheme()
if (iconEl) iconEl.setAttribute('name', themeManager.isDark() ? 'moon' : 'sun')
})Selector de tema integrat
<zs3-select-theme zs3-position="position:relative;"></zs3-select-theme>Touch Device Support
A partir de la versió 1.0.0, tota la interactivitat de la llibreria funciona tant en dispositius amb ratolí com en pantalles tàctils (smartphones, tablets, dispositius híbrids).
Drag & drop tàctil (move)
La funcionalitat move respon als events touchstart, touchmove i touchend a més dels events de ratolí. Funciona de forma idèntica: arrossega el component amb el dit.
<!-- Arrossegable tant amb ratolí com amb el dit -->
<zs3-window title="Finestra" move="xy" resizable>...</zs3-window>
<zs3-toolbar move="x">...</zs3-toolbar>Internament s'aplica touch-action: none als elements de drag per evitar que el navegador intercepti el gest com a scroll.
Float tàctil
Els components amb l'atribut float (parcialment amagats fins que l'usuari interactua) adapten el comportament per a tàctil:
- Ratolí:
mouseentermostra,mouseleaveamaga (comportament original) - Tàctil: primer tap mostra l'element; un tap a qualsevol lloc fora l'amaga
<!-- Toolbar flotant al costat esquerre, revela amb tap en tàctil -->
<zs3-toolbar float="left">
<zs3-button icon="settings">Opcions</zs3-button>
</zs3-toolbar>Estils hover condicionals
Els estils :hover de tots els components estan envoltats en @media (hover: hover). Això evita que en dispositius tàctils els estats de hover quedin "enganxats" visualment després del tap.
/* Internament, tots els hover estan protegits: */
@media (hover: hover) {
button:hover { ... }
}Zones de toc mínimes
En dispositius amb punter gruixut (@media (pointer: coarse)), els elements interactius petits —com els botons de control de zs3-window o el botó de tancar de zs3-notification— augmenten la seva zona mínima de toc a 44×44 px (recomanació WCAG).
Components
zs3-button
Botó versàtil amb variants, mides, icones, estats i events.
Atributs:
| Atribut | Valors | Descripció |
|---------|--------|------------|
| variant | primary secondary success danger warning info | Color del botó |
| size | sm md lg | Mida |
| icon | nom de la icona | Icona SVG integrada |
| position | left right | Posició de la icona respecte al text |
| disabled | booleà | Desactiva el botó |
| loading | booleà | Mostra spinner de càrrega |
| rounded | booleà | Forma arrodonida (perfecta per icones soles) |
Esdeveniments:
| Esdeveniment | Descripció |
|-------------|------------|
| zs3-click | Es dispara en fer clic (respecta disabled i loading) |
HTML inline (index2.html)
<!-- Variants -->
<zs3-button>Default</zs3-button>
<zs3-button variant="primary">Primary</zs3-button>
<zs3-button variant="secondary">Secondary</zs3-button>
<zs3-button variant="success">Success</zs3-button>
<zs3-button variant="danger">Danger</zs3-button>
<zs3-button variant="warning">Warning</zs3-button>
<zs3-button variant="info">Info</zs3-button>
<!-- Mides -->
<zs3-button variant="primary" size="sm">Small</zs3-button>
<zs3-button variant="primary">Medium</zs3-button>
<zs3-button variant="primary" size="lg">Large</zs3-button>
<!-- Amb icona -->
<zs3-button icon="save" variant="primary" position="left">Guardar</zs3-button>
<zs3-button icon="edit">Editar</zs3-button>
<zs3-button icon="trash" variant="danger" position="left">Eliminar</zs3-button>
<zs3-button icon="download" variant="info" position="left">Descarregar</zs3-button>
<!-- Rounded (icona sola) -->
<zs3-button icon="settings" rounded></zs3-button>
<zs3-button icon="bell" variant="warning" rounded></zs3-button>
<zs3-button icon="star" variant="success" size="lg" rounded>Favorit</zs3-button>
<!-- Estats -->
<zs3-button variant="primary" disabled>Disabled</zs3-button>
<zs3-button variant="success" loading>Loading...</zs3-button>
<!-- Event inline — `this` és el botó, `event` és el CustomEvent -->
<zs3-button id="btn-toggle" variant="info"
zs3-click="toggleLoading(this)">
Toggle Loading
</zs3-button>
<!-- Event inline simple -->
<zs3-button variant="primary" icon="plus"
zs3-click="document.getElementById('out').textContent = 'Clicat: ' + new Date().toLocaleTimeString()">
Clica'm
</zs3-button>
<div id="out">Clica un botó...</div>TypeScript programàtic (index3.ts)
import { $Button } from 'zs3-ui'
// Crear un botó
const b = new $Button()
b.textContent = 'Guardar' // text (es captura a beforeMount)
b.variant = 'primary'
b.icon = 'save'
b.position = 'left'
document.body.append(b)
// Mida i estat
b.size = 'lg'
b.disabled = true
b.loading = true
// Rounded (icona sola)
const bRound = new $Button()
bRound.icon = 'settings'
bRound.rounded = true
// Toggle loading programàtic
const bToggle = new $Button()
bToggle.textContent = 'Toggle Loading'
bToggle.variant = 'info'
bToggle.addEventListener('zs3-click', () => {
bToggle.setLoading(!bToggle.loading)
})
// Actualitzar text després de muntar (quan el component ja és al DOM)
bToggle.setText('Nou text')
// Canviar text amb i18n (actualitza en canvi d'idioma)
window.addEventListener('zs3-locale-change', () => {
bLogin.setText(i18n.t('section.auth.login'))
})API de $Button:
| Propietat / Mètode | Tipus | Descripció |
|-------------------|-------|------------|
| variant | string | Variant de color |
| size | 'sm' | 'md' | 'lg' | Mida |
| icon | string | Nom de la icona |
| position | 'left' | 'right' | Posició icona |
| disabled | boolean | Desactivat |
| loading | boolean | Estat de càrrega |
| rounded | boolean | Forma arrodonida |
| setText(text) | mètode | Actualitza el text (quan ja és al DOM) |
| setLoading(bool) | mètode | Activa/desactiva l'estat loading |
zs3-icon
Icona SVG integrada amb suport de mida, color, rotació, flip i clics.
Atributs:
| Atribut | Valors | Descripció |
|---------|--------|------------|
| name | nom de la icona | Icona a mostrar |
| size | small medium large o número en px | Mida |
| color | primary secondary success danger warning info | Color |
| rotate | 90 180 270 | Graus de rotació |
| flip | horizontal vertical | Volteig |
| clickable | booleà | Activa l'event zs3-icon-click |
| stroke-width | número | Amplada del traç SVG |
| aria-label | text | Label d'accessibilitat |
| data-i18n-attr | JSON | Traducció de l'atribut aria-label |
Icones disponibles: increase, decrease, error, warning, information, close, left-arrow, right-arrow, up-arrow, down-arrow, menu, accept, cancel, search, delete, dark, sun, moon, users, copy, trash, upload, logout, login, modal, save, edit, download, settings, home, star, heart, filter, refresh, lock, unlock, bell, mail, phone, calendar, clock, eye, eye-off, plus, minus, check, x
HTML inline (index2.html)
<!-- Bàsica -->
<zs3-icon name="star"></zs3-icon>
<!-- Mides -->
<zs3-icon name="star" size="small"></zs3-icon>
<zs3-icon name="star" size="medium"></zs3-icon>
<zs3-icon name="star" size="large"></zs3-icon>
<zs3-icon name="star" size="48"></zs3-icon>
<!-- Colors -->
<zs3-icon name="heart" size="large" color="primary"></zs3-icon>
<zs3-icon name="heart" size="large" color="danger"></zs3-icon>
<zs3-icon name="heart" size="large" color="success"></zs3-icon>
<!-- Rotació i flip -->
<zs3-icon name="right-arrow" size="large" rotate="90"></zs3-icon>
<zs3-icon name="right-arrow" size="large" rotate="180"></zs3-icon>
<zs3-icon name="right-arrow" size="large" flip="horizontal"></zs3-icon>
<!-- Clickable amb id per als event listeners de index2.ts -->
<zs3-icon id="icon-star" name="star" size="large" color="warning" clickable></zs3-icon>
<zs3-icon id="icon-heart" name="heart" size="large" color="danger" clickable></zs3-icon>
<zs3-icon id="icon-bell" name="bell" size="large" color="info" clickable></zs3-icon>
<div id="icon-click-output">Clica una icona...</div>
<!-- i18n en atribut aria-label -->
<zs3-icon name="bell" data-i18n-attr='{"aria-label":"nav.notifications"}'></zs3-icon>// index2.ts — escolta events de les icones clickables
function setupIconClickListeners(): void {
const output = document.getElementById('icon-click-output')
;['icon-star', 'icon-heart', 'icon-bell'].forEach((id) => {
const el = document.getElementById(id)
el?.addEventListener('zs3-icon-click', (e: Event) => {
const name = (e.target as Element).getAttribute('name') || id
if (output) output.textContent = `zs3-icon-click: icona "${name}" clicada`
})
})
}
// index2.ts — genera la graella de totes les icones
function setupIconsGrid(): void {
const grid = document.getElementById('icons-grid')
if (!grid) return
$Icon.getAvailableIcons().forEach((name) => {
const card = document.createElement('div')
card.className = 'demo-icon-card'
card.innerHTML = `<zs3-icon name="${name}" size="medium"></zs3-icon><span>${name}</span>`
card.addEventListener('click', () =>
$Notification.info(`Icona: ${name}`, { position: 'bottom-right', duration: 2000 })
)
grid.appendChild(card)
})
}TypeScript programàtic (index3.ts)
import { $Icon } from 'zs3-ui'
// Icona bàsica
const ic = new $Icon()
ic.setAttribute('name', 'star')
ic.setAttribute('size', 'large')
ic.setAttribute('color', 'warning')
document.body.append(ic)
// Clickable
ic.setAttribute('clickable', '')
ic.addEventListener('zs3-icon-click', (e) => {
console.log('Icona clicada:', (e as CustomEvent).detail)
})
// Totes les icones disponibles
const names = $Icon.getAvailableIcons() // string[]
// Afegir icona personalitzada
$Icon.addIcon('custom', `<path d="M12 2L2 22h20L12 2z"/>`)
$Icon.addIcons({
'logo-a': '<path .../>',
'logo-b': '<path .../>',
})zs3-toolbar
Contenidor de botons amb suport per orientació, alineació, variants compactes i posicionament flotant.
Atributs:
| Atribut | Valors | Descripció |
|---------|--------|------------|
| direction | row column | Orientació |
| align | start center end | Alineació dels elements |
| variant | default compact | Estil visual |
| float | top bottom left right | Posicionament flotant |
| move | x y xy | Permet arrossegar la toolbar |
| left top right bottom | posició en px | Posició inicial |
HTML inline (index2.html)
<!-- Horitzontal -->
<zs3-toolbar direction="row">
<zs3-button icon="home">Inici</zs3-button>
<zs3-button icon="users">Usuaris</zs3-button>
<zs3-button icon="settings">Configuració</zs3-button>
<zs3-button icon="bell" variant="warning">Notificacions</zs3-button>
<zs3-button icon="logout" variant="danger">Sortir</zs3-button>
</zs3-toolbar>
<!-- Vertical -->
<zs3-toolbar direction="column" style="width:fit-content">
<zs3-button icon="edit" variant="primary" position="left" size="sm">Editar</zs3-button>
<zs3-button icon="copy" position="left" size="sm">Copiar</zs3-button>
<zs3-button icon="trash" variant="danger" position="left" size="sm">Eliminar</zs3-button>
</zs3-toolbar>
<!-- Compact + align center (botons de navegació) -->
<zs3-toolbar direction="row" align="center" variant="compact">
<zs3-button icon="left-arrow" size="sm" rounded></zs3-button>
<zs3-button icon="refresh" size="sm" rounded></zs3-button>
<zs3-button icon="right-arrow" size="sm" rounded></zs3-button>
</zs3-toolbar>
<!-- Flotant + arrossegable horitzontalment -->
<zs3-toolbar direction="row" float="bottom" move="x" left="80px">
<zs3-button icon="save" variant="success" size="sm">Guardar</zs3-button>
<zs3-button icon="cancel" variant="danger" size="sm">Cancel·lar</zs3-button>
</zs3-toolbar>TypeScript programàtic (index3.ts)
import { $Toolbar, $Button } from 'zs3-ui'
// Horitzontal
const toolbar = new $Toolbar()
toolbar.setAttribute('direction', 'row')
const items: Array<[string, string, string | undefined]> = [
['home', 'Inici', undefined],
['users', 'Usuaris', undefined],
['bell', 'Notificacions', 'warning'],
['logout', 'Sortir', 'danger'],
]
items.forEach(([icon, label, variant]) => {
const b = new $Button()
b.textContent = label
b.icon = icon
if (variant) b.variant = variant as $Button['variant']
toolbar.append(b)
})
document.body.append(toolbar)
// Compact amb botons arrodonits
const navToolbar = new $Toolbar()
navToolbar.setAttribute('direction', 'row')
navToolbar.setAttribute('align', 'center')
navToolbar.setAttribute('variant', 'compact')
;['left-arrow', 'refresh', 'right-arrow'].forEach((icon) => {
const b = new $Button()
b.icon = icon
b.size = 'sm'
b.rounded = true
navToolbar.append(b)
})
// Flotant + movible
const floatToolbar = new $Toolbar()
floatToolbar.setAttribute('direction', 'row')
floatToolbar.setAttribute('float', 'bottom')
floatToolbar.setAttribute('move', 'x')
floatToolbar.setAttribute('left', '80px')zs3-notification
Notificació emergent (toast) amb posicions, durades, barra de progrés i estat persistent.
Atributs:
| Atribut | Valors | Descripció |
|---------|--------|------------|
| type | success error warning info | Tipus/color |
| title | text | Títol (opcional) |
| message | text | Missatge |
| position | top-left top-center top-right bottom-left bottom-center bottom-right | Posició a la pantalla |
| duration | número en ms (0 = persistent) | Temps de visibilitat |
| show-progress | booleà | Mostra barra de progrés |
| dismissible | booleà | Mostra botó de tancament |
| i18n-title | clau i18n | Títol traduït |
| i18n-message | clau i18n | Missatge traduït |
Mètodes estàtics (la manera més senzilla)
import { $Notification } from 'zs3-ui'
$Notification.success('Operació completada!', {
position: 'top-right',
duration: 3000,
showProgress: true,
})
$Notification.error('Ha ocorregut un error.', {
title: 'Error crític',
position: 'top-right',
duration: 5000,
dismissible: true,
})
$Notification.warning('Atenció: acció irreversible!', {
position: 'bottom-right',
duration: 4000,
})
$Notification.info('3 nous missatges pendents.', {
title: 'Nous missatges',
position: 'top-right',
duration: 0, // 0 = persistent
dismissible: true,
})HTML inline (index2.html)
<!-- Mètodes estàtics des de handlers inline -->
<zs3-button variant="success"
zs3-click="$Notification.success('Èxit!', { position: 'top-right', duration: 3000 })">
Success
</zs3-button>
<zs3-button variant="danger"
zs3-click="$Notification.error('Error!', { position: 'top-right', duration: 3000 })">
Error
</zs3-button>
<!-- Component inline amb show()/hide() -->
<zs3-notification
id="notif-inline"
type="success"
title="Títol"
message="Missatge de notificació"
position="top-center"
duration="0"
show-progress
dismissible>
</zs3-notification>
<zs3-button zs3-click="document.getElementById('notif-inline').$.show()">show()</zs3-button>
<zs3-button zs3-click="document.getElementById('notif-inline').$.hide()">hide()</zs3-button>
<!-- i18n -->
<zs3-notification
i18n-title="section.notification.success.title"
i18n-message="section.notification.success.message"
type="success"
position="top-right">
</zs3-notification>TypeScript programàtic (index3.ts)
// Component creat programàticament
const notif = document.createElement('zs3-notification') as HTMLElement & {
show(): void
hide(): void
}
notif.setAttribute('type', 'success')
notif.setAttribute('title', 'Notificació TypeScript')
notif.setAttribute('message', 'Creada programàticament des de TypeScript')
notif.setAttribute('position', 'top-center')
notif.setAttribute('duration', '0')
notif.setAttribute('show-progress', '')
notif.setAttribute('dismissible', '')
document.body.append(notif)
const bShow = new $Button()
bShow.textContent = 'show()'
bShow.variant = 'primary'
bShow.addEventListener('zs3-click', () => notif.show())
const bHide = new $Button()
bHide.textContent = 'hide()'
bHide.addEventListener('zs3-click', () => notif.hide())zs3-modal
Modal genèric contenidor de contingut amb mides, backdrop, tancament per ESC/backdrop i mode scrollable.
Atributs:
| Atribut | Valors | Descripció |
|---------|--------|------------|
| size | small medium large fullscreen | Mida |
| backdrop | booleà | Mostra el fons fosc |
| close-on-backdrop | booleà | Tanca en clicar el backdrop |
| close-on-escape | booleà | Tanca amb la tecla ESC |
| scrollable | booleà | Permet scroll del contingut |
| centered | booleà | Centra verticalment el modal |
Mètodes: show(), hide()
HTML inline (index2.html)
<!-- Defineix el modal a qualsevol lloc del body -->
<zs3-modal id="modal-medium" size="medium" backdrop close-on-backdrop close-on-escape>
<div style="padding:1.5rem">
<h3 style="margin-top:0">Modal Medium</h3>
<p>Contingut del modal.</p>
<zs3-button variant="primary"
zs3-click="document.getElementById('modal-medium').$.hide()">
Tancar
</zs3-button>
</div>
</zs3-modal>
<zs3-modal id="modal-fullscreen" size="fullscreen" backdrop close-on-escape>
<div style="padding:2rem">
<h2>Modal Fullscreen</h2>
<zs3-button variant="primary"
zs3-click="document.getElementById('modal-fullscreen').$.hide()">
Tancar
</zs3-button>
</div>
</zs3-modal>
<!-- Scrollable -->
<zs3-modal id="modal-scroll" size="medium" backdrop close-on-backdrop close-on-escape scrollable>
<div style="padding:1.5rem">
<h3>Scrollable</h3>
<!-- molts paràgrafs... -->
<zs3-button variant="primary"
zs3-click="document.getElementById('modal-scroll').$.hide()">
Tancar
</zs3-button>
</div>
</zs3-modal>
<zs3-button variant="primary" zs3-click="document.getElementById('modal-medium').$.show()">Obrir Medium</zs3-button>
<zs3-button variant="secondary" zs3-click="document.getElementById('modal-fullscreen').$.show()">Fullscreen</zs3-button>
<zs3-button variant="info" zs3-click="document.getElementById('modal-scroll').$.show()">Scrollable</zs3-button>TypeScript programàtic (index3.ts)
import { $Modal, $Button } from 'zs3-ui'
const modal = new $Modal()
modal.setAttribute('size', 'medium')
modal.setAttribute('backdrop', '')
modal.setAttribute('close-on-backdrop', '')
modal.setAttribute('close-on-escape', '')
const content = document.createElement('div')
content.style.padding = '1.5rem'
const closeBtn = new $Button()
closeBtn.textContent = 'Tancar'
closeBtn.variant = 'primary'
closeBtn.addEventListener('zs3-click', () => modal.hide())
content.append(closeBtn)
modal.append(content)
document.body.append(modal) // els modals van al body directament
const openBtn = new $Button()
openBtn.textContent = 'Obrir Modal'
openBtn.variant = 'primary'
openBtn.addEventListener('zs3-click', () => modal.show())zs3-modal-dialog
Diàleg modal amb títol, missatge, icona, botons d'acció configurables i mètodes estàtics per als casos d'ús comuns.
Atributs:
| Atribut | Descripció |
|---------|------------|
| title / i18n-title | Títol del diàleg |
| message / i18n-message | Missatge del cos |
| icon | Icona al costat del títol |
| size | Mida (small, medium, large) |
| show-accept | Mostra el botó d'acceptar |
| show-cancel | Mostra el botó de cancel·lar |
| show-close | Mostra el botó de tancar |
| accept-text / i18n-accept-text | Text del botó acceptar |
| cancel-text / i18n-cancel-text | Text del botó cancel·lar |
| accept-variant | Variant del botó acceptar |
| backdrop | Activa el fons fosc |
| close-on-escape | Tanca amb ESC |
Esdeveniments: zs3-dialog-accept, zs3-dialog-cancel, zs3-dialog-close
Mètodes estàtics (Promises)
import { $ModalDialog, i18n } from 'zs3-ui'
// Alert (es resol quan es tanca)
await $ModalDialog.alert({
title: 'Atenció',
message: 'El fitxer ha estat guardat correctament.',
})
// Alert amb i18n
await $ModalDialog.alertI18n({
i18nTitle: 'section.dialog.alert.title',
i18nMessage: 'section.dialog.alert.message',
})
// Confirm (retorna true/false)
const confirmed = await $ModalDialog.confirm({
title: 'Confirmació',
message: 'Estàs segur que vols continuar?',
})
// Confirm amb i18n
const result = await $ModalDialog.confirmI18n({
i18nTitle: 'section.dialog.confirm.title',
i18nMessage: 'section.dialog.confirm.message',
})
// Prompt (retorna el valor introduït o null si cancel·la)
const value = await $ModalDialog.prompt({
title: 'Introdueix un valor',
placeholder: 'Escriu aquí...',
})
if (value !== null) { console.log('Valor:', value) }
// Prompt amb i18n
const val = await $ModalDialog.promptI18n({
i18nTitle: 'section.dialog.prompt.title',
placeholder: i18n.t('section.dialog.prompt.message'),
})
// Confirm Delete (diàleg especialitzat d'eliminació)
const deleted = await $ModalDialog.confirmDelete('Element #42')
if (deleted) { /* eliminar l'element */ }HTML inline (index2.html)
<!-- Diàleg personalitzat declarat en HTML -->
<zs3-modal-dialog
id="custom-dialog"
i18n-title="section.dialog.confirm.title"
i18n-message="section.dialog.confirm.message"
icon="warning"
show-accept
show-cancel
accept-variant="warning"
i18n-accept-text="zs3.dialog.accept"
i18n-cancel-text="zs3.dialog.cancel"
backdrop
close-on-escape>
</zs3-modal-dialog>
<zs3-button variant="warning" icon="warning"
zs3-click="document.getElementById('custom-dialog').$.show()">
Obrir diàleg
</zs3-button>
<!-- Mètodes estàtics des de handlers inline (definits a index2.ts) -->
<zs3-button variant="primary" zs3-click="showDialogAlert()">Alert</zs3-button>
<zs3-button variant="info" zs3-click="showDialogConfirm()">Confirm</zs3-button>
<zs3-button variant="secondary" zs3-click="showDialogPrompt()">Prompt</zs3-button>
<zs3-button variant="danger" icon="trash" zs3-click="showDialogDelete()">Confirm Delete</zs3-button>
<div id="dialog-output">Resultat del diàleg...</div>// index2.ts — handlers globals per als mètodes estàtics
window.showDialogAlert = async () => {
await $ModalDialog.alertI18n({
i18nTitle: 'section.dialog.alert.title',
i18nMessage: 'section.dialog.alert.message',
})
setOutput('dialog-output', 'Alert tancat.')
}
window.showDialogConfirm = async () => {
const result = await $ModalDialog.confirmI18n({
i18nTitle: 'section.dialog.confirm.title',
i18nMessage: 'section.dialog.confirm.message',
})
setOutput('dialog-output', result ? '✓ Confirmat!' : '✗ Cancel·lat')
}
window.showDialogPrompt = async () => {
const value = await $ModalDialog.promptI18n({
i18nTitle: 'section.dialog.prompt.title',
placeholder: i18n.t('section.dialog.prompt.message'),
})
setOutput('dialog-output', value !== null ? `Valor: "${value}"` : 'Prompt cancel·lat')
}
window.showDialogDelete = async () => {
const result = await $ModalDialog.confirmDelete('Element #42')
setOutput('dialog-output', result ? '🗑 Element eliminat!' : '✗ Cancel·lat')
}
// Escolta l'event del diàleg personalitzat (HTML)
function setupCustomDialogListener(): void {
const dialog = document.getElementById('custom-dialog')
dialog?.addEventListener('zs3-dialog-accept', () => {
setOutput('dialog-output', '✓ ACCEPTAT')
$Notification.success('Acció confirmada!', { position: 'top-right', duration: 3000 })
})
dialog?.addEventListener('zs3-dialog-cancel', () => {
setOutput('dialog-output', '✗ CANCEL·LAT')
})
}TypeScript programàtic (index3.ts)
import { $ModalDialog, $Button, $Notification } from 'zs3-ui'
const dialog = new $ModalDialog()
dialog.setAttribute('i18n-title', 'section.dialog.confirm.title')
dialog.setAttribute('i18n-message', 'section.dialog.confirm.message')
dialog.setAttribute('icon', 'warning')
dialog.showAccept = true
dialog.showCancel = true
dialog.acceptVariant = 'warning'
dialog.setAttribute('i18n-accept-text', 'zs3.dialog.accept')
dialog.setAttribute('i18n-cancel-text', 'zs3.dialog.cancel')
dialog.setAttribute('backdrop', '')
dialog.setAttribute('close-on-escape', '')
document.body.append(dialog) // va al body directament
dialog.addEventListener('zs3-dialog-accept', () => {
$Notification.success('Acció confirmada!', { position: 'top-right', duration: 3000 })
})
dialog.addEventListener('zs3-dialog-cancel', () => {
console.log('Diàleg cancel·lat')
})
const openBtn = new $Button()
openBtn.textContent = 'Obrir diàleg'
openBtn.variant = 'warning'
openBtn.icon = 'warning'
openBtn.addEventListener('zs3-click', () => dialog.show())zs3-form
Formulari genèric amb camps configurables, validació, i18n complet i events de submit/cancel.
Atributs:
| Atribut | Descripció |
|---------|------------|
| title / i18n-title | Títol del formulari |
| description / i18n-description | Descripció |
| accept-text / i18n-accept-text | Text del botó d'enviar |
| cancel-text / i18n-cancel-text | Text del botó de cancel·lar |
| accept-variant | Variant del botó d'enviar |
| show-cancel | Mostra el botó de cancel·lar |
Tipus de camp type: text, email, password, number, tel, url, date, textarea, select, checkbox
Validació per camp: required, minLength, maxLength, min, max, pattern
Esdeveniments: zs3-form-submit (amb event.detail.values), zs3-form-cancel
TypeScript programàtic (index3.ts) — recomanat
import { $Form } from 'zs3-ui'
const form = new $Form()
form.setAttribute('i18n-title', 'form.title')
form.setAttribute('i18n-description', 'form.description')
form.showCancel = true
form.acceptVariant = 'success'
form.setFields([
{
name: 'name',
label: 'Nom',
i18nLabel: 'form.field.name',
type: 'text',
required: true,
placeholder: 'Nom',
i18nPlaceholder: 'form.field.name.placeholder',
validation: { minLength: 2, maxLength: 50 },
},
{
name: 'email',
label: 'Correu',
i18nLabel: 'form.field.email',
type: 'email',
required: true,
i18nPlaceholder: 'form.field.email.placeholder',
},
{
name: 'age',
label: 'Edat',
i18nLabel: 'form.field.age',
type: 'number',
required: true,
validation: { min: 18, max: 120 },
},
{
name: 'birthdate',
label: 'Data de naixement',
type: 'date',
required: false,
},
{
name: 'country',
label: 'País',
i18nLabel: 'form.field.country',
type: 'select',
required: true,
i18nPlaceholder: 'form.field.country.placeholder',
options: [
{ value: 'es', label: 'Espanya', i18nLabel: 'form.country.spain' },
{ value: 'fr', label: 'França', i18nLabel: 'form.country.france' },
{ value: 'it', label: 'Itàlia', i18nLabel: 'form.country.italy' },
{ value: 'de', label: 'Alemanya', i18nLabel: 'form.country.germany' },
{ value: 'uk', label: 'Regne Unit', i18nLabel: 'form.country.uk' },
],
},
{
name: 'bio',
label: 'Biografia',
i18nLabel: 'form.field.bio',
type: 'textarea',
required: false,
i18nPlaceholder: 'form.field.bio.placeholder',
rows: 3,
validation: { maxLength: 500 },
},
{
name: 'terms',
label: 'Accepto els termes i condicions',
i18nLabel: 'form.field.terms',
type: 'checkbox',
required: true,
},
])
form.addEventListener('zs3-form-submit', (e: Event) => {
const { values } = (e as CustomEvent).detail
console.log('Dades enviades:', values)
// values → { name: 'Joan', email: '[email protected]', country: 'es', terms: true, ... }
})
form.addEventListener('zs3-form-cancel', () => {
form.reset()
})
document.getElementById('app')!.append(form)zs3-login-form
Formulari de login preconstruït amb camps d'email i contrasenya, opció de botó de registre i traduccions integrades.
Atributs: show-register, show-cancel, i18n-login-text
Esdeveniments: zs3-form-submit (amb detail.values.email i detail.values.password), zs3-login-register
HTML inline (index2.html) — en un modal
<zs3-modal-dialog id="dlg-login" i18n-title="zs3.login.title" size="medium" backdrop close-on-escape>
<zs3-login-form
show-register
zs3-form-submit="document.getElementById('dlg-login').$.hide(); document.getElementById('auth-out').textContent = 'Login: ' + JSON.stringify(event.detail.values)"
zs3-login-register="document.getElementById('dlg-login').$.hide(); document.getElementById('dlg-register').$.show()">
</zs3-login-form>
</zs3-modal-dialog>
<zs3-button variant="primary" icon="login" position="left"
zs3-click="document.getElementById('dlg-login').$.show()">
Iniciar sessió
</zs3-button>
<div id="auth-out">Resultat auth...</div>TypeScript programàtic (index3.ts)
import { $LoginForm, $ModalDialog } from 'zs3-ui'
const loginDialog = new $ModalDialog()
loginDialog.setAttribute('i18n-title', 'zs3.login.title')
loginDialog.setAttribute('size', 'medium')
loginDialog.setAttribute('backdrop', '')
loginDialog.setAttribute('close-on-escape', '')
document.body.append(loginDialog)
const loginForm = new $LoginForm()
loginForm.showRegister = true
loginForm.addEventListener('zs3-form-submit', (e: Event) => {
const { values } = (e as CustomEvent).detail
loginDialog.hide()
console.log('Login:', values.email, values.password)
})
loginForm.addEventListener('zs3-login-register', () => {
loginDialog.hide()
registerDialog.show() // mostra el diàleg de registre
})
loginDialog.append(loginForm)
// Login inline (sense modal)
const inlineLogin = new $LoginForm()
inlineLogin.showRegister = true
inlineLogin.showCancel = true
inlineLogin.addEventListener('zs3-form-submit', (e: Event) => {
const { values } = (e as CustomEvent).detail
console.log('Login inline:', values)
})
document.getElementById('app')!.append(inlineLogin)zs3-register-form
Formulari de registre preconstruït amb camps nom, email, contrasenya i confirmació.
Atributs: show-cancel, i18n-register-text
Esdeveniments: zs3-form-submit, zs3-form-cancel
HTML inline
<zs3-modal-dialog id="dlg-register" i18n-title="zs3.register.title" size="medium" backdrop close-on-escape>
<zs3-register-form
show-cancel
zs3-form-submit="document.getElementById('dlg-register').$.hide()"
zs3-form-cancel="document.getElementById('dlg-register').$.hide()">
</zs3-register-form>
</zs3-modal-dialog>TypeScript programàtic
import { $RegisterForm, $ModalDialog } from 'zs3-ui'
const registerDialog = new $ModalDialog()
registerDialog.setAttribute('i18n-title', 'zs3.register.title')
registerDialog.setAttribute('size', 'medium')
registerDialog.setAttribute('backdrop', '')
registerDialog.setAttribute('close-on-escape', '')
document.body.append(registerDialog)
const registerForm = new $RegisterForm()
registerForm.showCancel = true
registerForm.addEventListener('zs3-form-submit', (e: Event) => {
const { values } = (e as CustomEvent).detail
registerDialog.hide()
console.log('Registre:', values)
})
registerForm.addEventListener('zs3-form-cancel', () => registerDialog.hide())
registerDialog.append(registerForm)zs3-recover-password-form
Formulari de recuperació de contrasenya amb camp d'email.
Atributs: show-cancel, i18n-submit-text, i18n-description
Esdeveniments: zs3-form-submit (amb detail.values.email), zs3-form-cancel
HTML inline
<zs3-modal-dialog id="dlg-recover" i18n-title="zs3.recover.title" size="small" backdrop close-on-escape>
<zs3-recover-password-form
i18n-description="zs3.recover.description"
show-cancel
zs3-form-submit="document.getElementById('dlg-recover').$.hide()"
zs3-form-cancel="document.getElementById('dlg-recover').$.hide()">
</zs3-recover-password-form>
</zs3-modal-dialog>TypeScript programàtic
import { $RecoverPasswordForm, $ModalDialog } from 'zs3-ui'
const recoverDialog = new $ModalDialog()
recoverDialog.setAttribute('i18n-title', 'zs3.recover.title')
recoverDialog.setAttribute('size', 'small')
recoverDialog.setAttribute('backdrop', '')
recoverDialog.setAttribute('close-on-escape', '')
document.body.append(recoverDialog)
const recoverForm = new $RecoverPasswordForm()
recoverForm.setAttribute('i18n-description', 'zs3.recover.description')
recoverForm.showCancel = true
recoverForm.addEventListener('zs3-form-submit', (e: Event) => {
const { values } = (e as CustomEvent).detail
recoverDialog.hide()
console.log('Email per recuperar:', values.email)
})
recoverForm.addEventListener('zs3-form-cancel', () => recoverDialog.hide())
recoverDialog.append(recoverForm)zs3-window
Finestra amb barra de títol, botons de control, contingut personalitzable, arrossegament i redimensió per les 4 cantonades. Tot funciona tant amb ratolí com amb pantalla tàctil.
Atributs:
| Atribut | Descripció |
|---------|------------|
| title / i18n-title | Títol de la finestra |
| width | Amplada inicial (px, %, etc.) |
| height | Alçada inicial (px, %, etc.) |
| move | x y xy — permet arrossegar la finestra per la barra de títol |
| resizable | Activa els handles de redimensió a les 4 cantonades |
| closable | Mostra el botó de tancar |
| minimizable | Mostra el botó de minimitzar |
| maximizable | Mostra el botó de maximitzar |
Esdeveniments de moviment:
| Esdeveniment | Descripció |
|-------------|------------|
| zs3-move-start | Inici de l'arrossegament |
| zs3-move | Durant l'arrossegament (detail.x, detail.y) |
| zs3-move-end | Fi de l'arrossegament |
| zs3-move-cancel | Cancel·lació amb Escape |
| zs3-move-boundary | La finestra ha tocat el límit del contenidor |
Esdeveniments de redimensió:
| Esdeveniment | Descripció |
|-------------|------------|
| zs3-resize-start | Inici de la redimensió |
| zs3-resize | Durant la redimensió (detail.width, detail.height, detail.corner) |
| zs3-resize-end | Fi de la redimensió |
El detail dels events de resize conté: width, height, left, top, corner ('nw' | 'ne' | 'sw' | 'se').
HTML inline (index2.html)
<!-- Contenidor relatiu per delimitar el moviment i el resize -->
<div style="position:relative;height:320px;border:1px dashed #e5e7eb;border-radius:.5rem;overflow:hidden">
<!-- Finestra arrossegable i redimensionable -->
<zs3-window
title="Finestra Interactiva"
width="280px"
height="180px"
move="xy"
resizable
closable
minimizable
style="position:absolute;top:16px;left:16px">
<div style="padding:.75rem">
<p>Arrossega per la barra de títol. Redimensiona per les cantonades.</p>
<zs3-button variant="success" icon="save" size="sm">Guardar</zs3-button>
</div>
</zs3-window>
<!-- Finestra amb títol i18n, només arrossegable -->
<zs3-window
i18n-title="form.title"
width="240px"
move="xy"
closable
style="position:absolute;top:16px;left:320px">
<div style="padding:.75rem">
<p>Títol via i18n-title.</p>
</div>
</zs3-window>
</div>
<!-- Escoltar events de resize inline -->
<zs3-window
title="Finestra"
width="300px" height="200px"
move="xy" resizable
zs3-resize="console.log('Mida:', event.detail.width, 'x', event.detail.height)"
zs3-resize-end="document.getElementById('mida').textContent = event.detail.width + 'x' + event.detail.height">
<p>Redimensiona-la!</p>
</zs3-window>
<span id="mida"></span>TypeScript programàtic (index3.ts)
import { $Window, $Toolbar, $Button, $Notification } from 'zs3-ui'
const container = document.createElement('div')
container.style.cssText = 'position:relative;height:320px;border:1px dashed #e5e7eb;border-radius:.5rem;overflow:hidden'
// Finestra arrossegable i redimensionable
const win1 = new $Window()
win1.setAttribute('title', 'Finestra — Drag & Resize')
win1.setAttribute('width', '280px')
win1.setAttribute('height', '180px')
win1.setAttribute('move', 'xy')
win1.setAttribute('resizable', '')
win1.setAttribute('closable', '')
win1.setAttribute('minimizable', '')
win1.style.cssText = 'position:absolute;top:16px;left:16px'
// Escoltar events de resize
win1.addEventListener('zs3-resize-end', (e: Event) => {
const { width, height, corner } = (e as CustomEvent).detail
$Notification.info(`Redimensionat des de ${corner}: ${Math.round(width)}×${Math.round(height)}px`, {
position: 'top-right',
duration: 2000,
})
})
// Escoltar events de moviment
win1.addEventListener('zs3-move-end', (e: Event) => {
const { endX, endY } = (e as CustomEvent).detail
console.log(`Finestra moguda a: ${Math.round(endX)}, ${Math.round(endY)}`)
})
const toolbar = new $Toolbar()
toolbar.setAttribute('direction', 'row')
const bSave = new $Button()
bSave.textContent = 'Guardar'
bSave.variant = 'success'
bSave.icon = 'save'
bSave.size = 'sm'
bSave.addEventListener('zs3-click', () =>
$Notification.success('Guardat!', { position: 'top-right', duration: 2000 })
)
toolbar.append(bSave)
win1.append(toolbar)
// Finestra amb i18n al títol, sense resize
const win2 = new $Window()
win2.setAttribute('i18n-title', 'form.title')
win2.setAttribute('width', '240px')
win2.setAttribute('move', 'xy')
win2.setAttribute('closable', '')
win2.style.cssText = 'position:absolute;top:16px;left:320px'
container.append(win1, win2)
document.getElementById('app')!.append(container)Nota sobre move + resizable: els dos atributs són totalment compatibles. move arrossega la finestra per la barra de títol; resizable permet canviar les dimensions per les 4 cantonades. En dispositius tàctils, ambdues funcionalitats responen als events de toc.
Utilities
$Store — Reactive State
Gestió d'estat reactiva amb reducers i subscripcions tipades. Inspirat en Redux.
import { $Store } from 'zs3-ui'
interface CounterState { count: number }
const counterStore = new $Store<CounterState>({ count: 0 })
// Registra reducers
counterStore.registerReducer('increment', (state) => ({ count: state.count + 1 }))
counterStore.registerReducer('decrement', (state) => ({ count: Math.max(0, state.count - 1) }))
counterStore.registerReducer('reset', () => ({ count: 0 }))
counterStore.registerReducer('add', (state, payload) => ({ count: state.count + (payload as number) }))
// Subscriu-te als canvis
counterStore.subscribe('*', (state) => {
document.getElementById('counter')!.textContent = String(state.count)
})
// Subscripció per acció específica
counterStore.subscribe('increment', (state) => {
console.log('Increment! Nou valor:', state.count)
})
// Dispatch
counterStore.dispatch('increment')
counterStore.dispatch('add', 10)
counterStore.dispatch('reset')
// Llegir l'estat actual
const current = counterStore.getState()HTML inline (index2.html) + index2.ts
<div id="store-counter" style="font-size:3.5rem;font-weight:700;text-align:center">0</div>
<div>
<zs3-button variant="danger" zs3-click="storeDecrement()">-1</zs3-button>
<zs3-button variant="success" zs3-click="storeIncrement()">+1</zs3-button>
<zs3-button zs3-click="storeReset()">Reset</zs3-button>
<zs3-button variant="info" zs3-click="storeAdd(10)">+10</zs3-button>
</div>
<div id="store-log"></div>// index2.ts
window.storeIncrement = () => counterStore.dispatch('increment')
window.storeDecrement = () => counterStore.dispatch('decrement')
window.storeReset = () => counterStore.dispatch('reset')
window.storeAdd = (n: number) => counterStore.dispatch('add', n)
counterStore.subscribe('*', (state) => {
const el = document.getElementById('store-counter')
if (el) el.textContent = String(state.count)
const logEl = document.getElementById('store-log')
if (logEl) {
const entry = document.createElement('div')
entry.textContent = `[${new Date().toLocaleTimeString()}] count = ${state.count}`
logEl.prepend(entry)
}
})TypeScript programàtic (index3.ts)
// Subscripció que actualitza elements creats per TypeScript
counterStore.subscribe('*', (state) => {
counterEl.textContent = String(state.count)
const entry = el('div', {}, `[${new Date().toLocaleTimeString()}] count = ${state.count}`)
storeLog.prepend(entry)
})
// Botons d'acció creats programàticament
const storeActions: Array<[string, string, () => void]> = [
['-1', 'danger', () => counterStore.dispatch('decrement')],
['+1', 'success', () => counterStore.dispatch('increment')],
['Reset', '', () => counterStore.dispatch('reset')],
['+10', 'info', () => counterStore.dispatch('add', 10)],
]
storeActions.forEach(([label, variant, fn]) => {
const b = btn(label, variant || undefined)
b.addEventListener('zs3-click', fn)
storeRow.append(b)
})EventBus
Comunicació desacoblada entre components via events amb nom.
import { eventBus } from 'zs3-ui'
// Subscripció
eventBus.on('user:login', (data) => console.log('Usuari ha entrat:', data))
eventBus.on('app:error', (data) => console.error('Error:', data))
eventBus.on('data:update', (data) => console.log('Dades actualitzades:', data))
// Subscripció a TOTS els events
eventBus.on('*', (payload) => console.log('Event rebut:', payload))
// Emissió
eventBus.emit('user:login', { name: 'Joan', role: 'admin' })
eventBus.emit('app:error', { code: 404, msg: 'Not found' })
eventBus.emit('data:update', { items: 42 })HTML inline (index2.html) + index2.ts
<div>
<zs3-button variant="primary" zs3-click="emitBusEvent('user:login', { name: 'Joan', role: 'admin' })">user:login</zs3-button>
<zs3-button variant="warning" zs3-click="emitBusEvent('app:error', { code: 404 })">app:error</zs3-button>
<zs3-button variant="info" zs3-click="emitBusEvent('data:update', { items: 42 })">data:update</zs3-button>
</div>
<div id="eventbus-output"></div>// index2.ts
window.emitBusEvent = (name: string, data: unknown) => eventBus.emit(name, data)
function setupEventBusListener(): void {
const output = document.getElementById('eventbus-output')
;['user:login', 'app:error', 'data:update'].forEach((evt) => {
eventBus.on(evt, (data) => {
if (output) {
const entry = document.createElement('div')
entry.textContent = `[${new Date().toLocaleTimeString()}] ${evt}: ${JSON.stringify(data)}`
output.prepend(entry)
if (output.children.length > 10) output.lastChild?.remove()
}
})
})
}TypeScript programàtic (index3.ts)
const busEvents: Array<[string, string, object]> = [
['user:login', 'primary', { name: 'Joan', role: 'admin' }],
['app:error', 'warning', { code: 404, msg: 'Not found' }],
['data:update','info', { items: 42 }],
]
busEvents.forEach(([evtName, variant, payload]) => {
eventBus.on(evtName, (data) => {
const entry = el('div', {}, `[${new Date().toLocaleTimeString()}] ${evtName}: ${JSON.stringify(data)}`)
busLog.prepend(entry)
})
const b = btn(`Emit ${evtName}`, variant)
b.addEventListener('zs3-click', () => eventBus.emit(evtName, payload))
busRow.append(b)
})$Storage — LocalStorage
Wrapper de localStorage amb serialització JSON automàtica.
import { storage } from 'zs3-ui'
storage.set('user.name', 'Joan')
storage.set('user.prefs', { theme: 'dark', locale: 'ca' })
const name = storage.get('user.name') // → 'Joan'
const prefs = storage.get('user.prefs') // → { theme: 'dark', locale: 'ca' }
const miss = storage.get('noexist') // → null
storage.remove('user.name')
storage.clear()HTML inline (index2.html) + index2.ts
<div>
<input id="storage-key" type="text" placeholder="clau" value="demo.key" />
<input id="storage-value" type="text" placeholder="valor" value="valor de prova" />
<zs3-button variant="primary" zs3-click="storageSet()" size="sm">set()</zs3-button>
<zs3-button zs3-click="storageGet()" size="sm">get()</zs3-button>
<zs3-button variant="danger" zs3-click="storageRemove()" size="sm">remove()</zs3-button>
</div>
<div id="storage-output"></div>// index2.ts
window.storageSet = () => {
const key = (document.getElementById('storage-key') as HTMLInputElement).value
const val = (document.getElementById('storage-value') as HTMLInputElement).value
storage.set(key, val)
setOutput('storage-output', `Guardat: "${key}" = "${val}"`)
}
window.storageGet = () => {
const key = (document.getElementById('storage-key') as HTMLInputElement).value
const val = storage.get(key)
setOutput('storage-output', val !== null
? `Llegit: "${key}" = "${val}"`
: `Clau "${key}" no trobada`
)
}
window.storageRemove = () => {
const key = (document.getElementById('storage-key') as HTMLInputElement).value
storage.remove(key)
setOutput('storage-output', `Eliminat: "${key}"`)
}TypeScript programàtic (index3.ts)
const bSet = btn('set()', 'primary'); bSet.size = 'sm'
bSet.addEventListener('zs3-click', () => {
storage.set(keyInput.value, valInput.value)
setOutput('storage-out-ts', `Guardat: "${keyInput.value}" = "${valInput.value}"`)
})
const bGet = btn('get()'); bGet.size = 'sm'
bGet.addEventListener('zs3-click', () => {
const v = storage.get(keyInput.value)
setOutput('storage-out-ts', v !== null
? `Llegit: "${keyInput.value}" = "${v}"`
: `Clau "${keyInput.value}" no trobada`
)
})
const bRm = btn('remove()', 'danger'); bRm.size = 'sm'
bRm.addEventListener('zs3-click', () => {
storage.remove(keyInput.value)
setOutput('storage-out-ts', `Eliminat: "${keyInput.value}"`)
})HttpClient
Client HTTP amb base URL, timeout i tipatge genèric de respostes.
import { HttpClient } from 'zs3-ui'
const http = new HttpClient({
baseUrl: 'https://api.example.com',
timeout: 8000,
})
// GET tipat
const res = await http.get<User[]>('/users?_limit=3')
console.log(res.status) // → 200
console.log(res.data) // → User[]
// POST / PUT / PATCH / DELETE
await http.post('/users', { name: 'Joan', email: '[email protected]' })
await http.put('/users/1', { name: 'Joan Updated' })
await http.patch('/users/1', { name: 'Joan' })
await http.delete('/users/1')
// Gestió d'errors
try {
await http.get('/nonexistent')
} catch (e: unknown) {
const err = e as { status?: number; message?: string }
console.error(`HTTP [${err.status ?? 'network'}]: ${err.message}`)
}HTML inline (index2.html) + index2.ts
<div>
<zs3-button variant="primary" zs3-click="httpGetUsers()">GET /users</zs3-button>
<zs3-button variant="success" zs3-click="httpGetPost()">GET /posts/1</zs3-button>
<zs3-button variant="danger" zs3-click="httpError()">Error 404</zs3-button>
</div>
<div id="http-output">Fes una petició...</div>// index2.ts
const http = new HttpClient({ baseUrl: 'https://jsonplaceholder.typicode.com', timeout: 8000 })
window.httpGetUsers = async () => {
setOutput('http-output', 'Carregant...')
try {
const res = await http.get<{ id: number; name: string }[]>('/users?_limit=3')
setOutput('http-output'