/** * Shared utilities for Web Component wrappers. * * @generated by scripts/generate-wc-components.ts * DO NOT EDIT MANUALLY */ import React from 'react'; import { createRoot } from 'react-dom/client'; export type PropType = 'string' | 'number' | 'boolean' | 'json'; export type PropDefs = Record; function warnInvalidValue(type: PropType, value: unknown): void { if (typeof console !== 'undefined' && typeof console.warn === 'function') { console.warn('[humain-ui wc] Ignoring invalid ' + type + ' value:', value); } } export function parseAttr(value: string | null, type: PropType): unknown { if (value === null) return undefined; switch (type) { case 'number': { const parsed = Number(value); if (!Number.isFinite(parsed)) { warnInvalidValue(type, value); return undefined; } return parsed; } case 'boolean': return value !== 'false' && value !== '0'; case 'json': try { return JSON.parse(value); } catch { warnInvalidValue(type, value); return undefined; } default: return value; } } export function parseValue(value: unknown, type: PropType): unknown { if (type === 'boolean' && typeof value === 'boolean') return value; if (type === 'number' && typeof value === 'number') { if (!Number.isFinite(value)) { warnInvalidValue(type, value); return undefined; } return value; } if (type === 'string' && typeof value === 'string') return value; if (type === 'json' && typeof value === 'object' && value !== null) return value; if (typeof value === 'string') { switch (type) { case 'boolean': return value !== 'false' && value !== '0' && value !== ''; case 'number': { const parsed = Number(value); if (!Number.isFinite(parsed)) { warnInvalidValue(type, value); return undefined; } return parsed; } case 'json': try { return JSON.parse(value); } catch { warnInvalidValue(type, value); return undefined; } default: return value; } } if (type === 'number' || type === 'json') { warnInvalidValue(type, value); return undefined; } return value; } export function asArray(value: unknown): T[] { return Array.isArray(value) ? (value as T[]) : []; } export function asOptionalArray(value: unknown): T[] | undefined { if (value === undefined) { return undefined; } return asArray(value); } export function camelToKebab(name: string): string { return name.replace(/([A-Z])/g, '-$1').toLowerCase(); } export function collectSlots(host: HTMLElement): Map { const slots = new Map(); for (const child of Array.from(host.children)) { const slotName = child.getAttribute('slot') ?? ''; const list = slots.get(slotName) ?? []; list.push(child); slots.set(slotName, list); } return slots; } export function portalNode(node: Node | null): React.ReactElement | null { if (!node) return null; return React.createElement('span', { ref: (el: HTMLSpanElement | null) => { if (el && node) { el.replaceWith(node); } }, style: { display: 'contents' }, }); } export function wrapSlotNodes( nodes: Element[], ownerDocument?: Document, ): HTMLDivElement { const documentForNodes = ownerDocument ?? nodes[0]?.ownerDocument; if (!documentForNodes) { throw new Error('Cannot wrap slot nodes without an owner document'); } const container = documentForNodes.createElement('div'); container.style.display = 'contents'; for (const node of nodes) { container.appendChild(node); } return container; } export function dispatchEvent( element: HTMLElement, eventName: string, detail?: unknown, ): void { const CustomEventConstructor = element.ownerDocument.defaultView?.CustomEvent ?? CustomEvent; element.dispatchEvent( new CustomEventConstructor(eventName, { detail, bubbles: true, composed: true, }), ); } export abstract class BaseCompoundWC extends HTMLElement { protected _root: ReturnType | null = null; protected _reactContainer: HTMLDivElement | null = null; protected _props: Record = {}; protected _slotContainers: Map = new Map(); protected _defaultSlotContainer: HTMLDivElement | null = null; protected _initialized = false; abstract get propDefs(): PropDefs; protected abstract _renderReact(): React.ReactElement; connectedCallback() { setTimeout(() => this._initialize(), 0); } disconnectedCallback() { this._root?.unmount(); this._root = null; this._reactContainer = null; } private _initialize() { if (this._initialized) return; this._initialized = true; const slots = collectSlots(this); for (const [name, elements] of slots) { const container = wrapSlotNodes(elements, this.ownerDocument); if (name === '') { this._defaultSlotContainer = container; } else { this._slotContainers.set(name, container); } } while (this.firstChild) this.removeChild(this.firstChild); this._reactContainer = this.ownerDocument.createElement('div'); this._reactContainer.style.display = 'contents'; this.appendChild(this._reactContainer); this._root = createRoot(this._reactContainer); this._render(); } attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null) { this._render(); } protected _getProp(name: string): unknown { if (name in this._props) return this._props[name]; const attrName = camelToKebab(name); const type = this.propDefs[name] ?? 'string'; return parseAttr(this.getAttribute(attrName), type); } protected _setProp(name: string, value: unknown) { const type = this.propDefs[name] ?? 'string'; this._props[name] = parseValue(value, type); this._render(); } protected _readAllProps(): Record { const result: Record = {}; for (const [propName, propType] of Object.entries(this.propDefs)) { if (propName in this._props) { result[propName] = this._props[propName]; } else { const attrName = camelToKebab(propName); result[propName] = parseAttr(this.getAttribute(attrName), propType); } } return result; } protected _getSlot(name: string): HTMLDivElement | null { return this._slotContainers.get(name) ?? null; } protected _getDefaultSlot(): HTMLDivElement | null { return this._defaultSlotContainer; } protected _render() { if (!this._root || !this._initialized) return; this._root.render(this._renderReact()); } } export function installPropAccessors( WCClass: new () => BaseCompoundWC, propDefs: PropDefs, ): void { for (const propName of Object.keys(propDefs)) { Object.defineProperty(WCClass.prototype, propName, { get(this: BaseCompoundWC) { return this._getProp(propName); }, set(this: BaseCompoundWC, value: unknown) { this._setProp(propName, value); }, enumerable: true, configurable: true, }); } } export function registerWC( tagName: string, WCClass: CustomElementConstructor, ): void { if (typeof customElements !== 'undefined' && !customElements.get(tagName)) { customElements.define(tagName, WCClass); } }