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.
| Module | Responsibility |
|---|---|
BaseComponent | Option merging, a namespaced uid, the event surface, tracked listeners, theming, and the open()/close() lifecycle. |
EventEmitter | Tiny on/off/once/emit implementation that returns disposers. |
OverlayManager | Renders popover / modal / inline surfaces, handles outside-click and Escape, and manages a stack so only the topmost overlay reacts. |
PositioningEngine | Anchors a popover to its trigger and flips it within the viewport. |
FocusManager | Traps and restores focus for modal dialogs. |
ThemeEngine | Resolves light / dark / auto and stamps data-dt-theme on scoped nodes. |
InputAdapter | Bridges a component to a native <input> or <select> for form submission. |
VirtualList | Windowed rendering so option lists of any size stay fast. |
InertialWheel | The 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:
| Mode | Used by | Behavior |
|---|---|---|
"text" | DateTimePicker | Keeps the visible <input> as the control: writes a human display string to value and a machine string to data-dt-value. |
"value" | SelectPicker | Hides 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)