{"version":3,"file":"lithentElement.mjs","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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"}