Overview

Architecture

doticca-ui separates domain logic (dates, options) from infrastructure (overlays, theming, focus, form binding). Components stay small because everything reusable lives in a shared core.

The layers

1 · Shared core

Framework-free primitives: lifecycle, events, overlays, positioning, focus, theming, virtualization, and input binding.

2 · Component layer

DateTimePicker and SelectPicker. Each owns only its domain: value model, rendering, and selection logic.

3 · Token theme

A single set of --dt-* CSS variables on .dt-scope styles every surface the suite renders.

Shared core modules

Components extend BaseComponent and delegate everything that is not domain-specific to a focused primitive. This is why both components feel identical to use and behave consistently.

ModuleResponsibility
BaseComponentOption merging, a namespaced uid, the event surface, tracked listeners, theming, and the open()/close() lifecycle.
EventEmitterTiny on/off/once/emit implementation that returns disposers.
OverlayManagerRenders popover / modal / inline surfaces, handles outside-click and Escape, and manages a stack so only the topmost overlay reacts.
PositioningEngineAnchors a popover to its trigger and flips it within the viewport.
FocusManagerTraps and restores focus for modal dialogs.
ThemeEngineResolves light / dark / auto and stamps data-dt-theme on scoped nodes.
InputAdapterBridges a component to a native <input> or <select> for form submission.
VirtualListWindowed rendering so option lists of any size stay fast.
InertialWheelThe iOS-style momentum scroll columns used by the mobile date sheet.

Lifecycle & the component layer

A component never manages overlays directly. BaseComponent exposes open() and close(), which call the subclass hooks onMount() and onUnmount(). The subclass builds its body DOM and hands it to an OverlayManager; teardown is symmetric. Because every listener is registered through this.listen(...), destroy() can guarantee there are no leaks.

class DateTimePicker extends BaseComponent {
  onMount() {
    this.body = createEl("div");
    this.overlay = new OverlayManager({
      anchor: this.input,
      mode: this.touch ? "modal" : "popover",
      onRequestClose: () => this.close(),
    });
    const panel = this.overlay.open(this.body);
    this.theme.register(panel);   // tokens cascade into the portal
    this._render();
  }
  onUnmount() { this.overlay.close(); }
}

Overlay & portal system

OverlayManager is the single source of truth for floating surfaces. It supports three modes and chooses the right one automatically based on the device:

  • popover — portaled to <body>, anchored to the trigger, dismissed on outside click. Used on desktop.
  • modal — a full bottom sheet over a dimmed overlay with a focus trap. Used on touch devices.
  • inline — rendered directly after the anchor, always open. Used for embedded calendars.

Surfaces render through a portal to document.body so they are never clipped by overflow or stacking contexts. A module-level stack ensures that with nested overlays, only the topmost one responds to Escape and outside clicks. Each panel carries the dt-scope class and a data-dt-theme attribute, so tokens cascade into the portal exactly as they do inline.

Input integration model

Components enhance existing form elements rather than replacing them, so values submit through normal HTML forms. InputAdapter handles two shapes:

ModeUsed byBehavior
"text"DateTimePickerKeeps the visible <input> as the control: writes a human display string to value and a machine string to data-dt-value.
"value"SelectPickerHides the source element and maintains a hidden field (or reuses a native <select>) carrying the raw value(s).

On destroy(), the adapter restores the original element — names, visibility, and attributes — so enhancement is fully reversible. Read more in Form integration.

Data flow at a glance

user interaction
      │
      ▼
component selection logic        (domain: dates / options)
      │  updates value model
      ▼
InputAdapter  ──►  native form value  (submits normally)
      │
      ├──►  onChange(value)         (option callback)
      └──►  emit("change", value)   (event subscribers)