1/** 2 * <x-button> â light DOM button component. 3 * 4 * Renders a styled <button> or <a> inside itself (light DOM, no shadow). 5 * Keeps the custom element in the DOM so Alpine/HTMX bindings on parent 6 * elements continue to work. 7 * 8 * Attributes: 9 * variant: "primary" (default) | "secondary" | "danger" | "ghost" 10 * size: "sm" (default) | "md" | "lg" | "xl" | "2xl" 11 * href: if set, renders as <a> instead of <button> 12 * target, rel: forwarded onto the rendered <a> (href only) 13 * type: button type (default "button", use "submit" for forms) 14 * disabled: disables the button 15 * class: extra classes moved onto the rendered element (e.g. "w-full") 16 * 17 * HTMX/Alpine: put hx-* and @* attributes directly on <x-button>. 18 * They stay on the host element and work normally. Alpine directives on 19 * the children (x-show spans for a button label that changes) work too: 20 * the render below re-inserts the children, and Alpine's MutationObserver 21 * initialises the re-inserted nodes whether it started before or after 22 * the render (verified both orders, welcome.html relies on it). 23 * 24 * Usage: 25 * <x-button variant="primary" type="submit">Save</x-button> 26 * <x-button variant="ghost" href="/dash/">Cancel</x-button> 27 * <x-button variant="danger" size="sm">Delete</x-button> 28 */ 29class XButton extends HTMLElement { 30 static BASE = 31 "inline-flex items-center justify-center font-semibold rounded-md " + 32 "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary " + 33 "disabled:opacity-50 disabled:pointer-events-none transition-[background-color,color,box-shadow,transform] " + 34 "active:scale-[0.98] cursor-pointer"; 35 36 static VARIANTS = { 37 primary: "bg-primary text-white hover:bg-primary-hover shadow-xs", 38 secondary: 39 "bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 ring-1 ring-gray-300 dark:ring-gray-600 ring-inset hover:bg-gray-50 dark:hover:bg-gray-700 shadow-xs", 40 danger: "bg-red-600 text-white hover:bg-red-500 shadow-xs", 41 ghost: "text-primary hover:underline", 42 }; 43 44 static SIZES = { 45 sm: "text-xs px-2 py-1 gap-x-1", 46 md: "text-sm px-3 py-1.5 gap-x-1.5", 47 lg: "text-sm px-3.5 py-2 gap-x-2", 48 xl: "text-[15px] px-4 py-2.5 gap-x-2", 49 "2xl": "text-base px-5 py-3.5 gap-x-2.5", 50 }; 51 52 connectedCallback() { 53 if (this.hasAttribute("data-rendered")) return; 54 this.setAttribute("data-rendered", ""); 55 56 // Defer to ensure child nodes are parsed before reading innerHTML 57 requestAnimationFrame(() => this._render()); 58 } 59 60 _render() { 61 const variant = this.getAttribute("variant") || "primary"; 62 const size = this.getAttribute("size") || "sm"; 63 const href = this.getAttribute("href"); 64 const type = this.getAttribute("type") || "button"; 65 const disabled = this.hasAttribute("disabled"); 66 67 // Host classes (e.g. w-full) move to the rendered element â the host 68 // stays display:inline and never carries layout itself. 69 const extra = this.getAttribute("class") || ""; 70 if (extra) this.removeAttribute("class"); 71 72 const classes = [ 73 XButton.BASE, 74 XButton.VARIANTS[variant] || XButton.VARIANTS.primary, 75 XButton.SIZES[size] || XButton.SIZES.sm, 76 extra, 77 ] 78 .join(" ") 79 .trim(); 80 81 const inner = this.innerHTML; 82 83 // Style the host element as inline so it doesn't affect layout 84 this.style.display = "inline"; 85 86 if (href) { 87 const target = this.getAttribute("target"); 88 const rel = this.getAttribute("rel"); 89 const linkAttrs = 90 (target ? ` target="${target}"` : "") + (rel ? ` rel="${rel}"` : ""); 91 this.innerHTML = `<a href="${href}" class="${classes}"${linkAttrs}>${inner}</a>`; 92 } else { 93 const disabledAttr = disabled ? " disabled" : ""; 94 this.innerHTML = `<button type="${type}" class="${classes}"${disabledAttr}>${inner}</button>`; 95 } 96 } 97} 98 99customElements.define("x-button", XButton);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.