{"version":3,"file":"lithentElement.cjs","sources":["../src/index.ts"],"sourcesContent":["import { h, mount, render } from 'lithent';\nimport type { Props, TagFunction } from 'lithent';\n\n/**\n * Prop declarations for `defineElement`. Each key is a camelCase prop name\n * observed as its kebab-case attribute; the constructor picks the attribute\n * conversion (DESIGN §4.1).\n */\nexport type PropSpec = Record<\n  string,\n  StringConstructor | NumberConstructor | BooleanConstructor | ObjectConstructor\n>;\n\n/** The prop type a constructor declares (DESIGN §8). */\ntype PropType<C> = C extends NumberConstructor\n  ? number\n  : C extends BooleanConstructor\n    ? boolean\n    : C extends StringConstructor\n      ? string\n      : unknown;\n\n/**\n * Props a declaration produces. Every prop can be absent (no attribute, no\n * property yet) except Boolean ones, which start as `false` (DESIGN §4.1).\n */\nexport type PropsOf<S extends PropSpec> = {\n  [K in keyof S as S[K] extends BooleanConstructor ? K : never]: boolean;\n} & {\n  [K in keyof S as S[K] extends BooleanConstructor ? never : K]?: PropType<\n    S[K]\n  >;\n};\n\n/** What the inner component receives: the declared props and `host`. */\nexport type ElementProps<S extends PropSpec> = PropsOf<S> & {\n  host: HTMLElement;\n};\n\n/** The element `defineElement` registers, with a property per prop. */\nexport type LithentElementOf<S extends PropSpec> = HTMLElement & PropsOf<S>;\n\nexport type DefineElementOptions<S extends PropSpec = PropSpec> = {\n  props?: S;\n  shadow?: boolean | 'open' | 'closed';\n  styles?: string[];\n};\n\n/** What an element keeps between callbacks. */\ntype State = {\n  /** Current props, owned by the element. */\n  p: Props;\n  /** The render root, created on first connect. */\n  root?: ShadowRoot | HTMLElement;\n  /** `destroy` returned by `render`, set while mounted. */\n  d?: () => void;\n  /** The Host component's `renew`, set while mounted. */\n  r?: () => void;\n};\n\n/**\n * Element state, outside the element: fields on the instance would collide\n * with props of the same name (`p`, `d`, ...), and a closed shadow root must\n * not leak through a property. (A module WeakMap instead of `#private`\n * fields: those compile to helpers that cost more bytes than this whole\n * file — RC-3.)\n */\nconst states = new WeakMap<HTMLElement, State>();\nconst stateOf = (el: HTMLElement) => states.get(el)!;\n\n/** Set a prop and re-render if mounted (renew is batched per microtask). */\nconst update = (el: HTMLElement, key: string, value: unknown) => {\n  const state = stateOf(el);\n  state.p[key] = value;\n  // Before the first connect there is no renew: the first render reads p.\n  if (state.r) state.r();\n};\n\n/**\n * Attribute string -> prop value (DESIGN §4.1). `null` means the attribute is\n * absent. Invalid numbers and JSON become `undefined` without a warning: the\n * runtime packages have no dev-only build to keep warnings out of production.\n */\nconst convert = (type: PropSpec[string], value: string | null): unknown => {\n  if (type === Boolean) return value !== null;\n  if (value === null) return undefined;\n  if (type === Number) return isNaN(+value) ? undefined : +value;\n  if (type === Object) {\n    try {\n      return JSON.parse(value);\n    } catch {\n      return undefined;\n    }\n  }\n  return value;\n};\n\n/**\n * Dispatch a DOM event from the element (DC-5). It is dispatched on the\n * element itself, so it bubbles to the host page; `composed` lets it also\n * leave an outer shadow root when the element is nested in another\n * component. It is cancelable: returns `false` when a listener called\n * `preventDefault()`. Pass `init` to change `bubbles`, `composed` or\n * `cancelable`.\n *\n * ```ts\n * const PayButton = mount<{ amount: number; host: HTMLElement }>(\n *   (_renew, props) => () =>\n *     h('button', { onClick: () => emit(props.host, 'pay', props.amount) }, 'Pay')\n * );\n * ```\n */\nexport const emit = (\n  host: Element,\n  name: string,\n  detail?: unknown,\n  // Overrides the defaults, e.g. { bubbles: false } for a noisy event that\n  // only listeners on the element itself should hear. `detail` always wins.\n  init?: EventInit\n) =>\n  host.dispatchEvent(\n    new CustomEvent(name, {\n      bubbles: true,\n      composed: true,\n      cancelable: true,\n      ...init,\n      detail,\n    })\n  );\n\n/**\n * Register a lithent component as a Custom Element.\n *\n * The element owns the props and renders a small Host component that holds\n * `renew`; the user's component sits under it unchanged (DESIGN §2).\n * Returns `undefined` where Custom Elements do not exist (SSR, Node), and the\n * existing constructor when the name is already taken (DC-7).\n */\nexport const defineElement = <\n  // `host` is reserved (DC-5): declaring it is a type error as well.\n  S extends PropSpec & { host?: never } = Record<never, never>,\n>(\n  name: `${string}-${string}`,\n  // `S` is inferred from `props` only: it sits in a mapped type here, which\n  // TypeScript does not infer from (checked in Phase 7; no NoInfer needed).\n  component: (props: ElementProps<S>, children?: never) => unknown,\n  options: DefineElementOptions<S> = {}\n): (new () => LithentElementOf<S>) | undefined => {\n  if (typeof customElements === 'undefined') return undefined;\n\n  const existing = customElements.get(name);\n  if (existing) return existing as new () => LithentElementOf<S>;\n\n  const { shadow = true, props: spec = {} as S, styles } = options;\n  // DC-5: `host` is the prop that carries the element itself.\n  if ('host' in spec) throw Error('\"host\" is reserved');\n  // FR-6: styles apply only inside a shadow root (DESIGN §5); one sheet per\n  // definition, shared by every instance and created on first use.\n  const css = shadow && styles ? styles.join('\\n') : '';\n  let sheet: CSSStyleSheet | undefined;\n\n  // FR-3: kebab-case attribute -> camelCase prop key.\n  const keyOf: Record<string, string> = {};\n  const attrOf: Record<string, string> = {};\n  for (const key in spec) {\n    keyOf[(attrOf[key] = key.replace(/[A-Z]/g, c => '-' + c.toLowerCase()))] =\n      key;\n  }\n  const observed = Object.keys(keyOf);\n\n  class LithentElement extends HTMLElement {\n    // A getter rather than a static field: static fields compile to helpers.\n    static get observedAttributes() {\n      return observed;\n    }\n\n    constructor() {\n      super();\n      const state: State = { p: {} };\n      states.set(this, state);\n      // An absent Boolean attribute is `false`, before any attribute callback.\n      for (const key in spec) if (spec[key] === Boolean) state.p[key] = false;\n    }\n\n    connectedCallback() {\n      const state = stateOf(this);\n      // Still mounted: this is the reconnect half of a move (DC-4).\n      if (state.d) return;\n\n      // FR-4: a value assigned before the element was defined is an own\n      // property that hides the prototype accessor (DESIGN §4.2). Take it over\n      // here rather than in the constructor: an upgrade runs the attribute\n      // callbacks after the constructor and before this, and the property is\n      // the richer value. lithent sets both on an element it renders before\n      // the definition (B-3), so an object or `false` must not be turned back\n      // into the attribute string.\n      for (const key in spec) {\n        // Object.hasOwn, not this.hasOwnProperty: a prop may be named that.\n        if (Object.hasOwn(this, key)) {\n          const value = (this as unknown as Props)[key];\n          delete (this as unknown as Props)[key];\n          // Through the accessor, so `undefined` falls back to the attribute\n          // (or `false`) as everywhere else.\n          (this as unknown as Props)[key] = value;\n        }\n      }\n\n      // DC-8: open shadow root by default; `shadow: false` renders into the\n      // element itself. A shadow root survives disconnects, so reuse it.\n      let root = state.root;\n      if (!root) {\n        root = state.root = shadow\n          ? this.attachShadow({ mode: shadow === 'closed' ? 'closed' : 'open' })\n          : this;\n        // Styles go in once per root: the root, and a fallback <style> in it,\n        // outlive disconnects (DESIGN §5, R-2).\n        if (css) {\n          if ('adoptedStyleSheets' in root) {\n            if (!sheet) {\n              sheet = new CSSStyleSheet();\n              sheet.replaceSync(css);\n            }\n            (root as ShadowRoot).adoptedStyleSheets = [sheet];\n          } else {\n            const style = document.createElement('style');\n            style.textContent = css;\n            root.appendChild(style);\n          }\n        }\n      }\n      // DC-6: in light DOM the element owns its children; drop server\n      // fallback content (or anything added while detached) before rendering.\n      if (!shadow) this.textContent = '';\n\n      const Host = mount(renew => {\n        state.r = renew;\n        return () =>\n          h(component as unknown as TagFunction, { ...state.p, host: this });\n      });\n\n      // DC-9: `render` only appends into its wrapper, so a ShadowRoot works.\n      state.d = render(h(Host, {}), root as unknown as HTMLElement);\n    }\n\n    disconnectedCallback() {\n      // DC-4: keep the instance across a move. A single-call move\n      // (appendChild, insertBefore) runs this after the element is back, so\n      // isConnected is already true; a remove() then insert in the same task\n      // is caught by waiting a microtask. connectedCallback sees `d` and\n      // does nothing.\n      const state = stateOf(this);\n      queueMicrotask(() => {\n        if (!this.isConnected && state.d) {\n          state.d();\n          state.d = state.r = undefined;\n        }\n      });\n    }\n\n    attributeChangedCallback(\n      attr: string,\n      _old: string | null,\n      value: string | null\n    ) {\n      const key = keyOf[attr];\n      // N changes in one task render once.\n      update(this, key, convert(spec[key], value));\n    }\n  }\n\n  // FR-4: every declared prop is also a property. Values pass through\n  // unconverted (objects, arrays, functions) and are not reflected (DC-3).\n  // `undefined` unsets the property: the prop falls back to what the\n  // attribute says, so a Boolean is `false` again when there is none. lithent\n  // assigns it when a parent stops passing the prop (B-4).\n  for (const key in spec) {\n    Object.defineProperty(LithentElement.prototype, key, {\n      get(this: LithentElement) {\n        return stateOf(this).p[key];\n      },\n      set(this: LithentElement, value: unknown) {\n        update(\n          this,\n          key,\n          value === undefined\n            ? convert(spec[key], this.getAttribute(attrOf[key]))\n            : value\n        );\n      },\n    });\n  }\n\n  customElements.define(name, LithentElement);\n  return LithentElement as unknown as new () => LithentElementOf<S>;\n};\n"],"names":["states","stateOf","el","update","key","value","state","convert","type","emit","host","name","detail","init","defineElement","component","options","existing","shadow","spec","styles","css","sheet","keyOf","attrOf","c","observed","LithentElement","root","style","Host","mount","renew","h","render","attr","_old"],"mappings":"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"}