npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zs3-ui

v1.0.2

Published

Modern Web Components library with built-in theming system and CSS variables support

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

  • 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$Form genèric i $LoginForm, $RegisterForm, $RecoverPasswordForm preconstruïts
  • Validators — funcions pures de validació (email, contrasenya, DNI, IBAN, telèfon...) composables via validate() i validateAll()

Installation

npm install zs3-ui

Importa els components i el CSS:

import 'zs3-ui'           // registra tots els Custom Elements
import 'zs3-ui/dist/zs3.css'  // variables CSS i estils base

O 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 + stylelint

El 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 DOMi18n.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.submit

Pots 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/dark

Escolta 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í: mouseenter mostra, mouseleave amaga (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'