stimeo-ui 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cable/consumer.ts","../../src/cable/live_counter_controller.ts","../../src/utils/safe_timeout.ts","../../src/cable/presence_controller.ts","../../src/cable/typing_indicator_controller.ts","../../src/cable/index.ts"],"names":["Controller","template"],"mappings":"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","file":"index.js","sourcesContent":["import { createConsumer } from \"@rails/actioncable\";\n\n/**\n * Minimal structural view of an Action Cable subscription — the two members the\n * server-bound controllers use. Keeping our own narrow interface (instead of\n * re-exporting the full `@rails/actioncable` types) lets consumers hand us any\n * structurally compatible object, including test doubles.\n */\nexport interface CableSubscription {\n /** Invokes a channel action on the server (`ChannelName#action`). */\n perform(action: string, data?: Record<string, unknown>): void;\n /** Cancels the subscription (the channel's `unsubscribed` runs server-side). */\n unsubscribe(): void;\n}\n\n/** The mixin a controller passes to {@link CableConsumer.subscriptions}' create. */\nexport interface CableSubscriptionMixin {\n /** Called once the subscription is confirmed (`perform` is deliverable). */\n connected?(): void;\n /**\n * Called when the connection drops — perform() is silently undeliverable\n * until Action Cable reconnects and re-confirms (`connected` fires again).\n */\n disconnected?(): void;\n /** Called when the server refuses the subscription (it will never confirm). */\n rejected?(): void;\n /** Called with each broadcast the channel transmits to this client. */\n received?(data: unknown): void;\n}\n\n/** Minimal structural view of an Action Cable consumer (the websocket owner). */\nexport interface CableConsumer {\n subscriptions: {\n create(\n channel: string | Record<string, unknown>,\n mixin: CableSubscriptionMixin,\n ): CableSubscription;\n };\n}\n\n/**\n * The shared consumer. Deliberately module-scoped: an Action Cable consumer is\n * *connection infrastructure* (one websocket per app, the Rails\n * `channels/consumer.js` convention), not UI state — it must survive Turbo\n * navigations, so it is deliberately not rebuilt per `connect()` the way\n * DOM-derived state is.\n */\nlet sharedConsumer: CableConsumer | null = null;\n\n/**\n * Replaces (or clears, with `null`) the shared Action Cable consumer.\n *\n * Call this once at boot when the app already owns a consumer (the usual\n * `app/javascript/channels/consumer.js`), so the server-bound controllers reuse\n * its websocket instead of opening a second one. Tests use it to inject a\n * double. With `null`, the next {@link getCableConsumer} lazily re-creates one.\n */\nexport function setCableConsumer(consumer: CableConsumer | null): void {\n sharedConsumer = consumer;\n}\n\n/**\n * The shared Action Cable consumer, lazily created on first use via\n * `createConsumer()` (which reads the standard `action_cable_meta_tag` URL).\n */\nexport function getCableConsumer(): CableConsumer {\n if (!sharedConsumer) sharedConsumer = createConsumer();\n return sharedConsumer;\n}\n\n/**\n * A {@link CableSubscription} that also tracks its confirmation lifecycle.\n * Action Cable silently drops a `perform()` sent before the subscription is\n * confirmed or while the connection is down — every server-bound controller\n * must therefore gate its sends (and their local side effects: optimistic\n * updates, throttle bookkeeping) on {@link confirmed}.\n */\nexport interface ConfirmedCableSubscription extends CableSubscription {\n /** True while the subscription is confirmed — `perform()` is deliverable. */\n readonly confirmed: boolean;\n /** True once the server refused the subscription (it will never confirm). */\n readonly rejected: boolean;\n}\n\n/**\n * Creates a subscription on the shared consumer with confirmation tracking\n * layered over the caller's mixin: `connected` / `disconnected` / `rejected`\n * flip the {@link ConfirmedCableSubscription.confirmed} flag *before* the\n * caller's own handler runs (so a handler reading `subscription.confirmed`\n * sees the post-transition state), and `received` passes straight through.\n *\n * This is deliberately a *tracker*, not an automatic `perform()` gate: the\n * controllers must skip the local side effects that surround a send (an\n * optimistic DOM bump, a throttle timestamp) together with the send itself,\n * which only the call site can decide — so they check `confirmed` and bail\n * before any of it.\n */\nexport function createConfirmedSubscription(\n channel: string | Record<string, unknown>,\n mixin: CableSubscriptionMixin,\n): ConfirmedCableSubscription {\n let confirmed = false;\n let rejected = false;\n const subscription = getCableConsumer().subscriptions.create(channel, {\n // A refusal is final: Action Cable never confirms a rejected subscription,\n // so a late connected/disconnected (only possible from a misbehaving\n // consumer double) must not reopen the gate `rejected` promised shut.\n connected: () => {\n if (rejected) return;\n confirmed = true;\n mixin.connected?.();\n },\n disconnected: () => {\n if (rejected) return;\n confirmed = false;\n mixin.disconnected?.();\n },\n rejected: () => {\n confirmed = false;\n rejected = true;\n mixin.rejected?.();\n },\n received: (data: unknown) => mixin.received?.(data),\n });\n return {\n perform: (action, data) => subscription.perform(action, data),\n unsubscribe: () => subscription.unsubscribe(),\n get confirmed() {\n return confirmed;\n },\n get rejected() {\n return rejected;\n },\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { type ConfirmedCableSubscription, createConfirmedSubscription } from \"./consumer\";\n\n/**\n * Marker on triggers this controller disabled (mirroring submit-once's\n * marker): `disabled` is a shared attribute, so only marked ones are ever\n * re-enabled — an authored-disabled trigger stays untouched.\n */\nconst DISABLED_MARKER = \"data-live-counter-disabled\";\n\n/**\n * Headless **live counter** — a *server-bound* behavior: a number bound to an\n * Action Cable stream (likes, views, active users) that ticks across every\n * connected client. A local action applies an **optimistic** increment\n * immediately, then reconciles with the broadcast; the own echo of a delta\n * broadcast is deduped so the increment is never applied twice. Ships in the\n * opt-in `stimeo-ui/cable` subpath (`@rails/actioncable` optional peer).\n *\n * Markup contract (identifier: `stimeo--live-counter`):\n * <div data-controller=\"stimeo--live-counter\"\n * data-stimeo--live-counter-channel-value=\"LikesChannel\"\n * data-stimeo--live-counter-params-value='{\"post\":42}'\n * data-stimeo--live-counter-id-value=\"<%= SecureRandom.uuid %>\">\n * <span data-stimeo--live-counter-target=\"value\">128</span>\n * <button type=\"button\" data-action=\"stimeo--live-counter#increment\"\n * data-stimeo--live-counter-target=\"trigger\">♥</button>\n * </div>\n *\n * Wire contract — the broadcast is either **authoritative** or a **delta**:\n * `{ count: 129 }` sets the absolute value (naturally idempotent; preferred —\n * the server owns the number), while `{ delta: 1, by: \"17\" }` adds to it,\n * skipped when `by` matches this client's `id` (the optimistic increment\n * already applied it). `increment` performs `increment` on the channel with\n * `{ id, delta }`; the server persists and broadcasts. Without an own `id`\n * the delta echo is indistinguishable from a foreign delta, so the optimistic\n * bump is skipped and the broadcast applies the increment exactly once — set\n * `id` (any per-client string, e.g. `SecureRandom.uuid`) for optimistic UX.\n *\n * @remarks\n * Behavior only — the displayed number IS the state, and the **DOM is the\n * source of truth**: the server renders the initial count into the `value`\n * target (the element itself without one), so a Turbo cache restore needs no\n * reconciliation and `connect()` only (re)subscribes. Sending tracks the full\n * subscription lifecycle (via the shared confirmation-aware subscription):\n * increments are dropped before confirmation AND while the connection is down\n * (`disconnected` shuts the gate until Action Cable re-confirms), so the\n * display never advances past what the server can receive. A refused\n * subscription publishes the `data-live-counter-rejected` hook (cleared on\n * `connect()` — rejection is transient server state) so the consumer's CSS can\n * disable the trigger. Optional `trigger` targets make that declarative: they\n * carry the real `disabled` attribute exactly while an increment would be\n * dropped (before confirmation, during an outage, after a rejection) — no CSS\n * required, and a disabled form control is announced as such by AT. An\n * authored `disabled` (set by the consumer for its own reasons) is respected:\n * only a disabled this controller applied — tracked via a marker attribute —\n * is ever lifted. Channel-less (local-only) counters never disable their\n * triggers. There are no timers; the subscription is released on\n * `disconnect()` (Turbo navigation included).\n */\nexport class LiveCounterController extends Controller<HTMLElement> {\n static override targets = [\"value\", \"trigger\"];\n static override values = {\n channel: { type: String, default: \"\" },\n params: { type: Object, default: {} },\n id: { type: String, default: \"\" },\n };\n static actions = [\"increment\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly hasValueTarget: boolean;\n declare readonly valueTarget: HTMLElement;\n declare readonly triggerTargets: HTMLElement[];\n declare channelValue: string;\n declare paramsValue: Record<string, unknown>;\n declare idValue: string;\n\n #subscription: ConfirmedCableSubscription | null = null;\n\n override connect(): void {\n // Rejection is transient server state: a Turbo cache snapshot must not\n // resurrect the hook — the fresh subscription below re-decides it.\n this.element.removeAttribute(\"data-live-counter-rejected\");\n if (this.channelValue) {\n this.#subscription = createConfirmedSubscription(\n { channel: this.channelValue, ...this.paramsValue },\n {\n connected: () => this.#syncTriggers(),\n // A drop closes the send window (the shared subscription tracks it)\n // until Action Cable reconnects and re-confirms — an increment during\n // the outage would bump the display while its perform() is silently\n // discarded by the closed socket.\n disconnected: () => this.#syncTriggers(),\n // The server refused the subscription (auth, bad params): the gate\n // stays shut for good, and the hook lets the consumer's CSS disable\n // or hide the trigger instead of leaving a silently dead button.\n rejected: () => {\n this.element.setAttribute(\"data-live-counter-rejected\", \"true\");\n this.#syncTriggers();\n },\n received: (data: unknown) => this.#onReceived(data),\n },\n );\n }\n // Also covers a Turbo cache restore that snapshotted a disabled trigger:\n // the fresh (unconfirmed or absent) subscription re-decides the state.\n this.#syncTriggers();\n }\n\n override disconnect(): void {\n this.#subscription?.unsubscribe();\n this.#subscription = null;\n this.element.removeAttribute(\"data-live-counter-rejected\");\n }\n\n /** Late-added triggers (e.g. via a Turbo Stream) pick up the current gate. */\n triggerTargetConnected(target: HTMLElement): void {\n this.#syncTrigger(target);\n }\n\n /**\n * Optimistic local increment: bumps the display immediately, then asks the\n * server to persist and broadcast. The step comes from the action param\n * (`data-stimeo--live-counter-delta-param`), default 1. Bound via `data-action`.\n */\n increment(event?: Event & { params?: { delta?: number } }): void {\n // Before the confirmed subscription — and while the connection is down —\n // a perform() is silently dropped by Action Cable: bumping the display\n // then would diverge from the server, so the whole increment is dropped\n // (screen and server stay consistent).\n if (this.#subscription && !this.#subscription.confirmed) return;\n // Action params arrive as authored strings; normalize and ignore garbage.\n const raw = Number(event?.params?.delta ?? 1);\n const delta = Number.isFinite(raw) ? raw : 1;\n // Without an own `id` the delta echo cannot be deduped: skip the optimistic\n // bump and let the broadcast apply the increment exactly once. Channel-less\n // (local-only) counters have no echo, so they always bump.\n if (!this.#subscription || this.idValue !== \"\") {\n this.#write(this.#current + delta);\n }\n this.#subscription?.perform(\"increment\", { id: this.idValue, delta });\n }\n\n /** Reconciles a broadcast: absolute `count` wins; own-echo deltas are skipped. */\n #onReceived(data: unknown): void {\n const message = data as { count?: unknown; delta?: unknown; by?: unknown } | null;\n if (typeof message?.count === \"number\") {\n this.#write(message.count);\n return;\n }\n if (typeof message?.delta === \"number\") {\n // The own echo: this client already applied the delta optimistically.\n if (typeof message.by === \"string\" && message.by !== \"\" && message.by === this.idValue) {\n return;\n }\n this.#write(this.#current + message.delta);\n }\n }\n\n /** True while an increment would go through (channel-less counters always are). */\n get #ready(): boolean {\n return !this.#subscription || this.#subscription.confirmed;\n }\n\n /**\n * Reflects the send gate onto the optional `trigger` targets as the real\n * `disabled` attribute — the declarative alternative to styling off the\n * `data-live-counter-rejected` hook (a disabled control is also skipped by\n * keyboard focus and announced by AT, which CSS alone cannot do).\n */\n #syncTriggers(): void {\n for (const trigger of this.triggerTargets) this.#syncTrigger(trigger);\n }\n\n /**\n * `disabled` is a shared attribute: only disable what is currently enabled\n * (marking it ours), and only lift a disabled carrying our marker — so an\n * authored-disabled trigger (\"disabled until valid\", say) is never\n * re-enabled by the gate. A marked disabled restored from a Turbo cache\n * snapshot is recognized as ours and lifted once the gate opens.\n */\n #syncTrigger(trigger: HTMLElement): void {\n if (this.#ready) {\n if (trigger.hasAttribute(DISABLED_MARKER)) {\n trigger.removeAttribute(\"disabled\");\n trigger.removeAttribute(DISABLED_MARKER);\n }\n } else if (!trigger.hasAttribute(\"disabled\")) {\n trigger.setAttribute(\"disabled\", \"\");\n trigger.setAttribute(DISABLED_MARKER, \"\");\n }\n }\n\n /** The displayed element: the `value` target, else the controller element. */\n get #display(): HTMLElement {\n return this.hasValueTarget ? this.valueTarget : this.element;\n }\n\n /** The current count, parsed from the DOM (the single source of truth). */\n get #current(): number {\n // Strip separators/suffixes (\"1,200 likes\") like count-up, so a formatted\n // server-rendered value doesn't collapse to its first digit group.\n const parsed = Number.parseInt((this.#display.textContent ?? \"\").replace(/[^0-9-]/g, \"\"), 10);\n return Number.isNaN(parsed) ? 0 : parsed;\n }\n\n #write(count: number): void {\n if (count === this.#current) return;\n this.#display.textContent = String(count);\n this.dispatch(\"change\", { detail: { count } });\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeInterval, SafeTimeout } from \"../utils/safe_timeout\";\nimport { type ConfirmedCableSubscription, createConfirmedSubscription } from \"./consumer\";\n\n/** A peer currently present (another client in the same room). */\ninterface Peer {\n name: string;\n /** Auto-expiry timer id, restarted by every beacon. */\n timer: number;\n}\n\n/**\n * Minimum ms between outgoing beacons. The steady state is the `heartbeat`\n * interval; this guard only bounds the *extra* beacons sent in response to\n * newly seen peers (see the roster-convergence note in the class doc), so a\n * burst of joiners cannot make this client flood the channel.\n */\nconst BEACON_THROTTLE_MS = 2000;\n\n/**\n * Headless **presence** — a *server-bound* behavior: online dots / a\n * \"who's viewing this\" stack, bound to an Action Cable channel. Like\n * `stimeo--typing-indicator`, the server stays a trivial rebroadcast channel\n * with **zero presence state**: every client heartbeats an `appear` beacon\n * (`{ id, name }`), and each client expires peers it has not heard from for\n * `timeout` ms. Leaving (`{ id, leaving: true }`) is broadcast best-effort on\n * `disconnect()` and on `pagehide` (tab close / hard navigation, where\n * `disconnect()` never runs); a lost notice is caught by the expiry. Ships in\n * the opt-in `stimeo-ui/cable` subpath (`@rails/actioncable` optional peer).\n *\n * Roster convergence: a late joiner would otherwise see peers only as their\n * next heartbeats arrive, so on hearing a beacon from an *unknown* peer, each\n * client re-announces itself (throttled to {@link BEACON_THROTTLE_MS}) — the\n * roster converges in one round-trip instead of one heartbeat period.\n *\n * Markup contract (identifier: `stimeo--presence`):\n * <div data-controller=\"stimeo--presence\"\n * data-stimeo--presence-channel-value=\"PresenceChannel\"\n * data-stimeo--presence-params-value='{\"room\":\"doc_7\"}'\n * data-stimeo--presence-id-value=\"17\" data-stimeo--presence-name-value=\"Alice\">\n * <span data-stimeo--presence-target=\"count\" data-other=\"%{count} viewing\"></span>\n * <ul data-stimeo--presence-target=\"list\"></ul>\n * <template data-stimeo--presence-target=\"template\">\n * <li><span data-presence-name></span></li>\n * </template>\n * </div>\n *\n * Server contract (a trivial rebroadcast channel):\n * class PresenceChannel < ApplicationCable::Channel\n * def subscribed = stream_from \"presence:#{params[:room]}\"\n * def appear(data) = ActionCable.server.broadcast(\"presence:#{params[:room]}\",\n * { id: data[\"id\"], name: data[\"name\"], leaving: data[\"leaving\"] })\n * end\n *\n * @remarks\n * Behavior only — the dot/stack look is the consumer's CSS, keyed off the\n * `data-present` / `data-present-count` hooks. The roster counts and renders\n * **other** clients (`id` ≠ own `id`); rendering is optional: given a `list` +\n * `template` pair, one clone per peer is appended (elements marked\n * `data-presence-name` receive the name; the clone root is tagged\n * `data-presence-id`), and the `count` target renders through localizable\n * `data-zero` / `data-one` / `data-other` templates (`%{count}`). Richer\n * per-user rendering (avatars, links) belongs to the consumer via the `join` /\n * `leave` / `change` events or a server-rendered Turbo Stream. The `id`\n * comparison is display-level echo suppression, not authentication — identity\n * belongs to the server. Sending tracks the full subscription lifecycle (via\n * the shared confirmation-aware subscription): beacons — heartbeats, the\n * convergence answer, the leaving notice — are dropped before confirmation and\n * while the connection is down, where Action Cable would discard them anyway\n * (and a discarded beacon must not burn the convergence throttle); `connected`\n * re-fires on every reconnect and force-beacons, so the roster self-heals. A\n * refused subscription publishes the `data-presence-rejected` hook (cleared on\n * `connect()` — rejection is transient server state), mirroring\n * `data-live-counter-rejected`. Presence is transient: `connect()` clears\n * whatever a Turbo cache snapshot preserved (hooks + rendered clones), renders\n * the known-empty count (the `data-present*` hooks stay absent until the first\n * beacon), and the stream re-populates; the subscription, heartbeat interval,\n * per-peer expiry timers, and the `pagehide` listener are all released on\n * `disconnect()` (Turbo navigation included).\n */\nexport class PresenceController extends Controller<HTMLElement> {\n static override targets = [\"count\", \"list\", \"template\"];\n static override values = {\n channel: { type: String, default: \"\" },\n params: { type: Object, default: {} },\n id: { type: String, default: \"\" },\n name: { type: String, default: \"\" },\n heartbeat: { type: Number, default: 15_000 },\n timeout: { type: Number, default: 40_000 },\n };\n static events = [\"join\", \"leave\", \"change\"] as const;\n\n declare readonly hasCountTarget: boolean;\n declare readonly countTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly listTarget: HTMLElement;\n declare readonly hasTemplateTarget: boolean;\n declare readonly templateTarget: HTMLTemplateElement;\n declare channelValue: string;\n declare paramsValue: Record<string, unknown>;\n declare idValue: string;\n declare nameValue: string;\n declare heartbeatValue: number;\n declare timeoutValue: number;\n\n #subscription: ConfirmedCableSubscription | null = null;\n /** Present peers keyed by id (insertion order = join order). */\n readonly #peers = new Map<string, Peer>();\n readonly #timers = new SafeTimeout();\n readonly #intervals = new SafeInterval();\n /** Epoch ms of the last outgoing beacon, for the convergence throttle. */\n #lastBeaconAt = 0;\n /** Pending trailing-edge convergence beacon (at most one queued). */\n #pendingBeacon: number | null = null;\n\n override connect(): void {\n // Presence is transient: drop whatever a Turbo cache snapshot preserved\n // (hooks + rendered clones); the live stream re-populates the roster.\n // Rejection is transient server state too — the fresh subscription below\n // re-decides the hook.\n this.#reset();\n this.element.removeAttribute(\"data-presence-rejected\");\n // The roster is known-empty here, so the count target can say so right\n // away instead of sitting blank until the first roster change. The\n // data-present* hooks intentionally stay absent until the first beacon.\n if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(0);\n\n if (!this.channelValue) return;\n this.#subscription = createConfirmedSubscription(\n { channel: this.channelValue, ...this.paramsValue },\n {\n // The first beacon must wait for the confirmed subscription — a\n // perform() before that is silently dropped by Action Cable. Fires\n // again on every reconnect, so the roster self-heals after an outage.\n connected: () => this.#beacon(true),\n // The server refused the subscription: no beacon will ever go through,\n // and the hook lets the consumer's CSS reflect the dead stream.\n rejected: () => {\n this.element.setAttribute(\"data-presence-rejected\", \"true\");\n },\n received: (data: unknown) => this.#onReceived(data),\n },\n );\n this.#intervals.set(() => this.#beacon(true), this.heartbeatValue);\n window.addEventListener(\"pagehide\", this.#onPageHide);\n }\n\n override disconnect(): void {\n window.removeEventListener(\"pagehide\", this.#onPageHide);\n // Best-effort graceful leave; a lost notice is caught by peers' expiry\n // timers instead.\n this.#sendLeaveNotice();\n this.#subscription?.unsubscribe();\n this.#subscription = null;\n this.#intervals.clearAll();\n this.#reset();\n this.element.removeAttribute(\"data-presence-rejected\");\n }\n\n /**\n * Sends the best-effort leaving notice (skipped without an own `id`, and\n * outside the confirmed window, where Action Cable would discard it anyway).\n */\n #sendLeaveNotice(): void {\n if (!this.#subscription?.confirmed || !this.idValue) return;\n this.#subscription.perform(\"appear\", { id: this.idValue, leaving: true });\n }\n\n /**\n * `pagehide` covers the leaves `disconnect()` cannot see: closing the tab or\n * a hard (non-Turbo) navigation destroys the page without running Stimulus\n * teardown, so this listener is the only chance to announce them. Best-effort\n * by nature (the socket may close before the frame flushes); peers' expiry\n * timers stay the safety net. If the page enters the bfcache and is restored\n * instead, the next heartbeat re-announces this client, so an over-eager\n * leave self-heals.\n */\n readonly #onPageHide = (): void => {\n this.#sendLeaveNotice();\n };\n\n /**\n * Broadcasts this client's beacon (skipped without an own `id`). A throttled\n * convergence answer is deferred to the trailing edge rather than dropped —\n * otherwise a peer joining right after a heartbeat would not learn about\n * this client until the next full heartbeat period.\n *\n * Gated on the confirmed subscription: before confirmation and during an\n * outage Action Cable silently discards perform(), so a beacon sent then is\n * pure waste — worse, it would burn `#lastBeaconAt` and delay the next real\n * convergence answer by up to the throttle window. The heartbeat interval\n * keeps ticking regardless; `connected` re-fires on reconfirm and\n * force-beacons immediately, so a gated tick is never missed for long.\n */\n #beacon(force: boolean): void {\n if (!this.#subscription?.confirmed || !this.idValue) return;\n const now = Date.now();\n const wait = BEACON_THROTTLE_MS - (now - this.#lastBeaconAt);\n if (!force && wait > 0) {\n if (this.#pendingBeacon === null) {\n this.#pendingBeacon = this.#timers.set(() => {\n this.#pendingBeacon = null;\n this.#beacon(true);\n }, wait);\n }\n return;\n }\n this.#lastBeaconAt = now;\n this.#subscription.perform(\"appear\", { id: this.idValue, name: this.nameValue });\n }\n\n /**\n * Tracks a broadcast beacon: upserts the peer (restarting its expiry timer),\n * removes it on a `leaving` notice, and re-announces this client when the\n * peer was unknown (roster convergence — see the class doc).\n */\n #onReceived(data: unknown): void {\n const beacon = data as { id?: unknown; name?: unknown; leaving?: unknown } | null;\n const id = beacon?.id;\n if (typeof id !== \"string\" || id === \"\" || id === this.idValue) return;\n\n if (beacon?.leaving === true) {\n this.#drop(id);\n return;\n }\n\n const name = typeof beacon?.name === \"string\" ? beacon.name : \"\";\n const existing = this.#peers.get(id);\n if (existing !== undefined) this.#timers.clear(existing.timer);\n const timer = this.#timers.set(() => this.#drop(id), this.timeoutValue);\n this.#peers.set(id, { name, timer });\n\n if (existing === undefined) {\n this.#appendClone(id, name);\n this.#render();\n this.dispatch(\"join\", { detail: { id, name } });\n this.#beacon(false); // answer an unknown peer so its roster converges\n } else if (existing.name !== name) {\n this.#updateClone(id, name);\n this.#render();\n }\n }\n\n /** Removes a peer (expiry or graceful leave) and reflects the change. */\n #drop(id: string): void {\n const peer = this.#peers.get(id);\n if (peer === undefined) return;\n this.#timers.clear(peer.timer);\n this.#peers.delete(id);\n this.#removeClone(id);\n this.#render();\n this.dispatch(\"leave\", { detail: { id } });\n }\n\n /** Reflects the roster onto the hooks + count target and emits `change`. */\n #render(): void {\n const users = [...this.#peers.entries()].map(([id, peer]) => ({ id, name: peer.name }));\n this.element.setAttribute(\"data-present\", users.length > 0 ? \"true\" : \"false\");\n this.element.setAttribute(\"data-present-count\", String(users.length));\n if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(users.length);\n this.dispatch(\"change\", { detail: { users } });\n }\n\n /**\n * Builds the count copy. Localizable through `data-zero` / `data-one` /\n * `data-other` templates on the count target (`%{count}`); the bare number is\n * the fallback (copy-free, so nothing to localize by default).\n */\n #countMessage(count: number): string {\n const templates = this.countTarget.dataset;\n const template =\n (count === 0 ? templates.zero : count === 1 ? templates.one : templates.other) ??\n templates.other;\n return template ? template.replace(\"%{count}\", String(count)) : String(count);\n }\n\n /** Appends one template clone for a newly present peer (list + template only). */\n #appendClone(id: string, name: string): void {\n if (!this.hasListTarget || !this.hasTemplateTarget) return;\n const clone = this.templateTarget.content.cloneNode(true) as DocumentFragment;\n const root = clone.firstElementChild;\n if (!root) return;\n root.setAttribute(\"data-presence-id\", id);\n this.#fillName(root, name);\n this.listTarget.appendChild(clone);\n }\n\n #updateClone(id: string, name: string): void {\n const root = this.#cloneFor(id);\n if (root) this.#fillName(root, name);\n }\n\n #removeClone(id: string): void {\n this.#cloneFor(id)?.remove();\n }\n\n #cloneFor(id: string): Element | null {\n if (!this.hasListTarget) return null;\n // Attribute selectors cannot escape arbitrary ids reliably; match manually.\n for (const child of this.listTarget.querySelectorAll(\"[data-presence-id]\")) {\n if (child.getAttribute(\"data-presence-id\") === id) return child;\n }\n return null;\n }\n\n /** Writes the peer's name into the clone's `data-presence-name` slots. */\n #fillName(root: Element, name: string): void {\n const slots = root.querySelectorAll(\"[data-presence-name]\");\n for (const slot of slots) slot.textContent = name;\n if (slots.length === 0 && root.hasAttribute(\"data-presence-name\")) {\n root.textContent = name;\n }\n }\n\n /** Clears the transient roster state (connect reset + disconnect teardown). */\n #reset(): void {\n this.#timers.clearAll();\n this.#pendingBeacon = null;\n this.#peers.clear();\n this.#lastBeaconAt = 0;\n this.element.removeAttribute(\"data-present\");\n this.element.removeAttribute(\"data-present-count\");\n if (this.hasCountTarget) this.countTarget.textContent = \"\";\n if (this.hasListTarget) {\n for (const child of this.listTarget.querySelectorAll(\"[data-presence-id]\")) {\n child.remove();\n }\n }\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { type ConfirmedCableSubscription, createConfirmedSubscription } from \"./consumer\";\n\n/**\n * Headless **typing indicator** — a *server-bound* behavior: a controller whose\n * state lives on the server stream, not in client memory. Typing in the composer\n * throttle-broadcasts a `typing` signal over an Action Cable channel; received\n * signals from *other* clients render \"X is typing…\" into a `status` live region\n * and auto-clear after `timeout` ms of silence. The whole behavior is HTML + a\n * broadcast — no app JS, no client store, no reconciler. Ships in the opt-in\n * `stimeo-ui/cable` subpath (`@rails/actioncable` optional peer); the core stays\n * zero-dep.\n *\n * Markup contract (identifier: `stimeo--typing-indicator`):\n * <div data-controller=\"stimeo--typing-indicator\"\n * data-stimeo--typing-indicator-channel-value=\"TypingChannel\"\n * data-stimeo--typing-indicator-params-value='{\"room\":\"chat_42\"}'\n * data-stimeo--typing-indicator-name-value=\"Alice\">\n * <textarea data-stimeo--typing-indicator-target=\"input\"></textarea>\n * <p role=\"status\" data-stimeo--typing-indicator-target=\"status\"\n * data-one=\"%{name} is typing…\" data-many=\"%{names} are typing…\"></p>\n * </div>\n *\n * Server contract (a trivial rebroadcast channel):\n * class TypingChannel < ApplicationCable::Channel\n * def subscribed = stream_from \"typing:#{params[:room]}\"\n * def typing(data) = ActionCable.server.broadcast(\"typing:#{params[:room]}\",\n * { name: data[\"name\"] })\n * end\n *\n * @remarks\n * Behavior only — the indicator's look is the consumer's CSS, keyed off the\n * `data-typing` hook; the copy is localizable through the `data-one` /\n * `data-many` templates (`%{name}` / `%{names}` / `%{count}`, terse English\n * fallback — the same channel design as `stimeo--sortable`). The own echo is\n * dropped by comparing the broadcast `name` against `name` (a same-name guard,\n * not authentication — identity belongs to the server). Sending tracks the full\n * subscription lifecycle (via the shared confirmation-aware subscription):\n * signals are dropped before confirmation and while the connection is down, so\n * an outage never burns the throttle window on undeliverable sends. A refused\n * subscription publishes the `data-typing-indicator-rejected` hook (cleared on\n * `connect()` — rejection is transient server state), mirroring\n * `data-live-counter-rejected`. Typing state is transient by nature:\n * `connect()` resets the hook and the live region (a Turbo cache snapshot must\n * not resurrect a stale \"X is typing…\"), and re-population happens naturally\n * from the stream. The subscription, the per-typer timers, and the delegated\n * `input` listener are all released on `disconnect()` (Turbo navigation\n * included).\n */\nexport class TypingIndicatorController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"status\"];\n static override values = {\n channel: { type: String, default: \"\" },\n params: { type: Object, default: {} },\n name: { type: String, default: \"\" },\n timeout: { type: Number, default: 3000 },\n // Must stay below `timeout`: the throttle is leading-edge only (no trailing\n // send), so a receiver's display survives continuous typing only while a\n // fresh signal lands within its timeout window.\n throttle: { type: Number, default: 2000 },\n };\n static events = [\"change\"] as const;\n\n declare readonly hasStatusTarget: boolean;\n declare readonly statusTarget: HTMLElement;\n declare channelValue: string;\n declare paramsValue: Record<string, unknown>;\n declare nameValue: string;\n declare timeoutValue: number;\n declare throttleValue: number;\n\n #subscription: ConfirmedCableSubscription | null = null;\n /** Names currently typing (other clients), each with its auto-clear timer id. */\n readonly #typers = new Map<string, number>();\n readonly #timers = new SafeTimeout();\n /** Epoch ms of the last broadcast, for leading-edge throttling. */\n #lastSentAt = 0;\n\n override connect(): void {\n // Typing state is transient: a Turbo cache snapshot must not resurrect a\n // stale indicator, and the live stream re-populates naturally. Rejection is\n // transient server state too — the fresh subscription re-decides the hook.\n this.element.removeAttribute(\"data-typing\");\n this.element.removeAttribute(\"data-typing-indicator-rejected\");\n if (this.hasStatusTarget) this.statusTarget.textContent = \"\";\n\n // Delegated on the container so the composer needs no per-input data-action\n // (and swapped/appended inputs keep working).\n this.element.addEventListener(\"input\", this.#onInput);\n if (this.channelValue) {\n // Confirmation tracking (connected / disconnected / rejected) lives in\n // the shared subscription; #onInput gates on its `confirmed` so an\n // outage doesn't burn the throttle window on dropped sends.\n this.#subscription = createConfirmedSubscription(\n { channel: this.channelValue, ...this.paramsValue },\n {\n // The server refused the subscription: the send gate stays shut for\n // good, and the hook lets the consumer's CSS reflect the dead stream.\n rejected: () => {\n this.element.setAttribute(\"data-typing-indicator-rejected\", \"true\");\n },\n received: (data: unknown) => this.#onReceived(data),\n },\n );\n }\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"input\", this.#onInput);\n this.#subscription?.unsubscribe();\n this.#subscription = null;\n this.#timers.clearAll();\n this.#typers.clear();\n this.element.removeAttribute(\"data-typing\");\n this.element.removeAttribute(\"data-typing-indicator-rejected\");\n if (this.hasStatusTarget) this.statusTarget.textContent = \"\";\n this.#lastSentAt = 0;\n }\n\n /** Throttled (leading-edge) broadcast of this client's typing signal. */\n readonly #onInput = (): void => {\n // Before the confirmed subscription a perform() is silently dropped by\n // Action Cable — and recording #lastSentAt would ALSO throttle away the\n // first real send, so bail without touching the throttle state.\n if (!this.#subscription?.confirmed) return;\n const now = Date.now();\n if (now - this.#lastSentAt < this.throttleValue) return;\n this.#lastSentAt = now;\n this.#subscription.perform(\"typing\", { name: this.nameValue });\n };\n\n /**\n * Tracks a broadcast typer. The own echo is dropped (same `name`); every\n * further signal from a name restarts its auto-clear timer, so the indicator\n * survives continuous typing and clears `timeout` ms after the last signal.\n */\n #onReceived(data: unknown): void {\n const name = (data as { name?: unknown } | null)?.name;\n if (typeof name !== \"string\" || name === \"\" || name === this.nameValue) return;\n\n const existing = this.#typers.get(name);\n if (existing !== undefined) this.#timers.clear(existing);\n const added = existing === undefined;\n this.#typers.set(\n name,\n this.#timers.set(() => this.#untrack(name), this.timeoutValue),\n );\n if (added) this.#render();\n }\n\n #untrack(name: string): void {\n this.#typers.delete(name);\n this.#render();\n }\n\n /** Reflects the typer set onto the hook + live region and emits `change`. */\n #render(): void {\n const names = [...this.#typers.keys()];\n this.element.setAttribute(\"data-typing\", names.length > 0 ? \"true\" : \"false\");\n if (this.hasStatusTarget) {\n this.statusTarget.textContent = this.#message(names);\n }\n this.dispatch(\"change\", { detail: { names } });\n }\n\n /**\n * Builds the live-region copy. Localizable through `data-one` / `data-many`\n * templates on the status target (`%{name}` / `%{names}` / `%{count}`); terse\n * English is the fallback.\n */\n #message(names: string[]): string {\n if (names.length === 0) return \"\";\n const joined = names.join(\", \");\n if (names.length === 1) {\n const template = this.statusTarget.dataset.one;\n const name = names[0] ?? \"\";\n return template ? template.replace(\"%{name}\", name) : `${name} is typing…`;\n }\n const template = this.statusTarget.dataset.many;\n return template\n ? template.replace(\"%{names}\", joined).replace(\"%{count}\", String(names.length))\n : `${joined} are typing…`;\n }\n}\n","import type { Application } from \"@hotwired/stimulus\";\nimport { LiveCounterController } from \"./live_counter_controller\";\nimport { PresenceController } from \"./presence_controller\";\nimport { TypingIndicatorController } from \"./typing_indicator_controller\";\n\nexport {\n type CableConsumer,\n type CableSubscription,\n type CableSubscriptionMixin,\n type ConfirmedCableSubscription,\n createConfirmedSubscription,\n getCableConsumer,\n setCableConsumer,\n} from \"./consumer\";\nexport { LiveCounterController, PresenceController, TypingIndicatorController };\n\n/**\n * Opt-in **server-bound** behaviors for Stimeo — controllers whose state lives\n * on an Action Cable / Turbo stream rather than in client memory: presence,\n * typing indicators, live counters.\n *\n * **Why this is a separate entry point.** The core library is zero-runtime-dep:\n * `import \"stimeo-ui\"` pulls in nothing but `@hotwired/stimulus`. Binding the\n * DOM to a server stream genuinely needs `@rails/actioncable`, so — exactly\n * like `stimeo-ui/positioning` and `@floating-ui/dom` — that cost is opt-in:\n * this module lives at `stimeo-ui/cable`, `@rails/actioncable` is an *optional*\n * peer, and nothing in the core imports it.\n *\n * When the app already owns a consumer (`app/javascript/channels/consumer.js`),\n * hand it over once at boot so no second websocket is opened:\n *\n * ```ts\n * import consumer from \"./channels/consumer\";\n * import { registerCable, setCableConsumer } from \"stimeo-ui/cable\";\n *\n * setCableConsumer(consumer);\n * registerCable(application);\n * ```\n */\nexport const cableControllers = {\n \"stimeo--live-counter\": LiveCounterController,\n \"stimeo--presence\": PresenceController,\n \"stimeo--typing-indicator\": TypingIndicatorController,\n} as const;\n\n/**\n * Registers the opt-in server-bound controllers on a Stimulus Application\n * (additive to `registerStimeo`, mirroring `registerPositioning`).\n */\nexport function registerCable(application: Application): void {\n for (const [identifier, controller] of Object.entries(cableControllers)) {\n application.register(identifier, controller);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/cable/consumer.ts","../../src/cable/live_counter_controller.ts","../../src/utils/safe_timeout.ts","../../src/cable/presence_controller.ts","../../src/utils/announce.ts","../../src/cable/typing_indicator_controller.ts","../../src/cable/index.ts"],"names":["Controller","template"],"mappings":"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","file":"index.js","sourcesContent":["import { createConsumer } from \"@rails/actioncable\";\n\n/**\n * Minimal structural view of an Action Cable subscription — the two members the\n * server-bound controllers use. Keeping our own narrow interface (instead of\n * re-exporting the full `@rails/actioncable` types) lets consumers hand us any\n * structurally compatible object, including test doubles.\n */\nexport interface CableSubscription {\n /** Invokes a channel action on the server (`ChannelName#action`). */\n perform(action: string, data?: Record<string, unknown>): void;\n /** Cancels the subscription (the channel's `unsubscribed` runs server-side). */\n unsubscribe(): void;\n}\n\n/** The mixin a controller passes to {@link CableConsumer.subscriptions}' create. */\nexport interface CableSubscriptionMixin {\n /** Called once the subscription is confirmed (`perform` is deliverable). */\n connected?(): void;\n /**\n * Called when the connection drops — perform() is silently undeliverable\n * until Action Cable reconnects and re-confirms (`connected` fires again).\n */\n disconnected?(): void;\n /** Called when the server refuses the subscription (it will never confirm). */\n rejected?(): void;\n /** Called with each broadcast the channel transmits to this client. */\n received?(data: unknown): void;\n}\n\n/** Minimal structural view of an Action Cable consumer (the websocket owner). */\nexport interface CableConsumer {\n subscriptions: {\n create(\n channel: string | Record<string, unknown>,\n mixin: CableSubscriptionMixin,\n ): CableSubscription;\n };\n}\n\n/**\n * Parses the extra identifier parameters a subscription is declared with.\n *\n * Stimulus offers an `Object` Value type, but its reader runs `JSON.parse` inside\n * the value observer — **before** the controller's `connect()` — and rethrows on\n * malformed text. The throw propagates out of the observer, so a single\n * unparseable attribute stops the subscription from ever being created and\n * leaves a controller that is connected but deaf, with nothing on the element to\n * say so. Declaring the attribute as a `String` Value and parsing it here keeps a\n * malformed declaration local to the value it declares: the identifier falls back\n * to the channel alone. The attribute text is identical either way\n * (`'{\"room\":\"chat_42\"}'`), so markup does not change with the declaration.\n *\n * Only a JSON object survives. An array, a bare number, a string or `null` cannot\n * name identifier parameters, and spreading one into the identifier would either\n * do nothing or produce index keys no channel can reproduce server-side.\n */\nexport function parseSubscriptionParams(raw: string): Record<string, unknown> {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return {};\n return parsed as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\n/**\n * The shared consumer. Deliberately module-scoped: an Action Cable consumer is\n * *connection infrastructure* (one websocket per app, the Rails\n * `channels/consumer.js` convention), not UI state — it must survive Turbo\n * navigations, so it is deliberately not rebuilt per `connect()` the way\n * DOM-derived state is.\n */\nlet sharedConsumer: CableConsumer | null = null;\n\n/**\n * Replaces (or clears, with `null`) the shared Action Cable consumer.\n *\n * Call this once at boot when the app already owns a consumer (the usual\n * `app/javascript/channels/consumer.js`), so the server-bound controllers reuse\n * its websocket instead of opening a second one. Tests use it to inject a\n * double. With `null`, the next {@link getCableConsumer} lazily re-creates one.\n */\nexport function setCableConsumer(consumer: CableConsumer | null): void {\n sharedConsumer = consumer;\n}\n\n/**\n * The shared Action Cable consumer, lazily created on first use via\n * `createConsumer()` (which reads the standard `action_cable_meta_tag` URL).\n */\nexport function getCableConsumer(): CableConsumer {\n if (!sharedConsumer) sharedConsumer = createConsumer();\n return sharedConsumer;\n}\n\n/**\n * A {@link CableSubscription} that also tracks its confirmation lifecycle.\n * Action Cable silently drops a `perform()` sent before the subscription is\n * confirmed or while the connection is down — every server-bound controller\n * must therefore gate its sends (and their local side effects: optimistic\n * updates, throttle bookkeeping) on {@link confirmed}.\n */\nexport interface ConfirmedCableSubscription extends CableSubscription {\n /** True while the subscription is confirmed — `perform()` is deliverable. */\n readonly confirmed: boolean;\n /** True once the server refused the subscription (it will never confirm). */\n readonly rejected: boolean;\n}\n\n/** One caller of {@link createConfirmedSubscription} riding a shared wire subscription. */\ninterface Member {\n readonly mixin: CableSubscriptionMixin;\n /**\n * True while a lifecycle callback the member missed (the identifier was already\n * confirmed or refused when it joined) is still owed to it. Any callback that arrives\n * from the wire in the meantime supersedes the owed one.\n */\n catchUp: boolean;\n}\n\n/** The one wire subscription an identifier has on a consumer, plus who rides it. */\ninterface SharedSubscription {\n readonly subscription: CableSubscription;\n readonly members: Set<Member>;\n confirmed: boolean;\n rejected: boolean;\n}\n\n/**\n * Wire subscriptions by identifier, per consumer. Keyed weakly so a replaced consumer\n * (see {@link setCableConsumer}) takes its bookkeeping with it.\n */\nconst sharedSubscriptions = new WeakMap<CableConsumer, Map<string, SharedSubscription>>();\n\n/**\n * The identifier Action Cable derives for a channel descriptor: the JSON of the\n * params object, a bare channel name standing for `{ channel }`.\n */\nfunction identifierOf(channel: string | Record<string, unknown>): string {\n return JSON.stringify(typeof channel === \"string\" ? { channel } : channel);\n}\n\n/**\n * Creates a subscription on the shared consumer with confirmation tracking\n * layered over the caller's mixin: `connected` / `disconnected` / `rejected`\n * flip the {@link ConfirmedCableSubscription.confirmed} flag *before* the\n * caller's own handler runs (so a handler reading `subscription.confirmed`\n * sees the post-transition state), and `received` passes straight through.\n *\n * **Callers with the same identifier (channel + params) share one wire\n * subscription.** The server confirms an identifier once and silently ignores a\n * repeated `subscribe` for it, so a second wire subscription would never be\n * confirmed — its sends would stay gated and the client would keep re-sending\n * `subscribe` until the next reconnect. Instead every caller is a member of the\n * identifier's one subscription: lifecycle callbacks and broadcasts fan out to all\n * members, a member joining an already confirmed (or refused) identifier is told\n * so on the next microtask — after its caller has stored the returned\n * subscription, the way a callback from the wire would arrive — and the wire is\n * unsubscribed when the last member leaves.\n *\n * This is deliberately a *tracker*, not an automatic `perform()` gate: the\n * controllers must skip the local side effects that surround a send (an\n * optimistic DOM bump, a throttle timestamp) together with the send itself,\n * which only the call site can decide — so they check `confirmed` and bail\n * before any of it.\n */\nexport function createConfirmedSubscription(\n channel: string | Record<string, unknown>,\n mixin: CableSubscriptionMixin,\n): ConfirmedCableSubscription {\n const consumer = getCableConsumer();\n let registry = sharedSubscriptions.get(consumer);\n if (!registry) {\n registry = new Map();\n sharedSubscriptions.set(consumer, registry);\n }\n const identifier = identifierOf(channel);\n const member: Member = { mixin, catchUp: false };\n\n let shared = registry.get(identifier);\n if (!shared) {\n shared = openSharedSubscription(consumer, channel);\n registry.set(identifier, shared);\n } else if (shared.rejected || shared.confirmed) {\n // Missed lifecycle: deliver it once the caller holds the returned subscription.\n const owed = shared;\n member.catchUp = true;\n queueMicrotask(() => {\n if (!member.catchUp) return;\n member.catchUp = false;\n if (owed.rejected) mixin.rejected?.();\n else mixin.connected?.();\n });\n }\n shared.members.add(member);\n const owner = shared;\n const registered = registry;\n let active = true;\n\n return {\n perform: (action, data) => owner.subscription.perform(action, data),\n unsubscribe: () => {\n if (!active) return;\n active = false;\n // Nothing is owed to a member that left: a catch-up still queued must not fire.\n member.catchUp = false;\n owner.members.delete(member);\n if (owner.members.size === 0) {\n registered.delete(identifier);\n owner.subscription.unsubscribe();\n }\n },\n get confirmed() {\n return owner.confirmed;\n },\n get rejected() {\n return owner.rejected;\n },\n };\n}\n\n/** Opens the wire subscription for an identifier and wires the fan-out to its members. */\nfunction openSharedSubscription(\n consumer: CableConsumer,\n channel: string | Record<string, unknown>,\n): SharedSubscription {\n const members = new Set<Member>();\n // A callback from the wire supersedes whatever a late member was still owed.\n const fanOut = (deliver: (mixin: CableSubscriptionMixin) => void): void => {\n for (const member of [...members]) {\n member.catchUp = false;\n deliver(member.mixin);\n }\n };\n const shared: SharedSubscription = {\n members,\n confirmed: false,\n rejected: false,\n subscription: consumer.subscriptions.create(channel, {\n // A refusal is final: Action Cable never confirms a rejected subscription,\n // so a late connected/disconnected (only possible from a misbehaving\n // consumer double) must not reopen the gate `rejected` promised shut.\n connected: () => {\n if (shared.rejected) return;\n shared.confirmed = true;\n fanOut((mixin) => mixin.connected?.());\n },\n disconnected: () => {\n if (shared.rejected) return;\n shared.confirmed = false;\n fanOut((mixin) => mixin.disconnected?.());\n },\n rejected: () => {\n shared.confirmed = false;\n shared.rejected = true;\n fanOut((mixin) => mixin.rejected?.());\n },\n received: (data: unknown) => fanOut((mixin) => mixin.received?.(data)),\n }),\n };\n return shared;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport {\n type ConfirmedCableSubscription,\n createConfirmedSubscription,\n parseSubscriptionParams,\n} from \"./consumer\";\n\n/**\n * Marker on triggers this controller disabled (mirroring submit-once's\n * marker): `disabled` is a shared attribute, so only marked ones are ever\n * re-enabled — an authored-disabled trigger stays untouched.\n */\nconst DISABLED_MARKER = \"data-live-counter-disabled\";\n\n/**\n * Headless **live counter** — a *server-bound* behavior: a number bound to an\n * Action Cable stream (likes, views, active users) that ticks across every\n * connected client. A local action applies an **optimistic** increment\n * immediately, then reconciles with the broadcast; the own echo of a delta\n * broadcast is deduped so the increment is never applied twice. Ships in the\n * opt-in `stimeo-ui/cable` subpath (`@rails/actioncable` optional peer).\n *\n * Markup contract (identifier: `stimeo--live-counter`):\n * <div data-controller=\"stimeo--live-counter\"\n * data-stimeo--live-counter-channel-value=\"LikesChannel\"\n * data-stimeo--live-counter-params-value='{\"post\":42}'\n * data-stimeo--live-counter-id-value=\"<%= SecureRandom.uuid %>\">\n * <span data-stimeo--live-counter-target=\"value\">128</span>\n * <button type=\"button\" data-action=\"stimeo--live-counter#increment\"\n * data-stimeo--live-counter-target=\"trigger\">♥</button>\n * </div>\n *\n * Wire contract — the broadcast is either **authoritative** or a **delta**:\n * `{ count: 129 }` sets the absolute value (naturally idempotent; preferred —\n * the server owns the number), while `{ delta: 1, by: \"17\" }` adds to it,\n * skipped when `by` matches this client's `id` (the optimistic increment\n * already applied it). `increment` performs `increment` on the channel with\n * `{ id, delta }`; the server persists and broadcasts. Without an own `id`\n * the delta echo is indistinguishable from a foreign delta, so the optimistic\n * bump is skipped and the broadcast applies the increment exactly once — set\n * `id` (any per-client string, e.g. `SecureRandom.uuid`) for optimistic UX.\n *\n * `change` dispatches `{ count }`.\n *\n * @remarks\n * Behavior only — the displayed number IS the state, and the **DOM is the\n * source of truth**: the server renders the initial count into the `value`\n * target (the element itself without one), so a Turbo cache restore needs no\n * reconciliation and `connect()` only (re)subscribes. Sending tracks the full\n * subscription lifecycle (via the shared confirmation-aware subscription):\n * increments are dropped before confirmation AND while the connection is down\n * (`disconnected` shuts the gate until Action Cable re-confirms), so the\n * display never advances past what the server can receive. A refused\n * subscription publishes the `data-live-counter-rejected` hook (cleared on\n * `connect()` — rejection is transient server state) so the consumer's CSS can\n * disable the trigger. Optional `trigger` targets make that declarative: they\n * carry the real `disabled` attribute exactly while an increment would be\n * dropped (before confirmation, during an outage, after a rejection) — no CSS\n * required, and a disabled form control is announced as such by AT. An\n * authored `disabled` (set by the consumer for its own reasons) is respected:\n * only a disabled this controller applied — tracked via a marker attribute —\n * is ever lifted. Channel-less (local-only) counters never disable their\n * triggers. There are no timers; the subscription is released on\n * `disconnect()` (Turbo navigation included).\n */\nexport class LiveCounterController extends Controller<HTMLElement> {\n static override targets = [\"value\", \"trigger\"];\n static override values = {\n channel: { type: String, default: \"\" },\n params: { type: String, default: \"\" },\n id: { type: String, default: \"\" },\n };\n static actions = [\"increment\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly hasValueTarget: boolean;\n declare readonly valueTarget: HTMLElement;\n declare readonly triggerTargets: HTMLElement[];\n declare channelValue: string;\n declare paramsValue: string;\n declare idValue: string;\n\n /** Identifier parameters parsed once from their declaration, never in the hot path. */\n #params: Record<string, unknown> = {};\n\n #subscription: ConfirmedCableSubscription | null = null;\n\n /**\n * Re-parses the identifier parameters when the declaration changes.\n *\n * A malformed declaration falls back to no parameters, so the identifier keeps\n * naming the channel instead of the subscription never being created at all.\n */\n paramsValueChanged(): void {\n this.#params = parseSubscriptionParams(this.paramsValue);\n }\n\n override connect(): void {\n // Rejection is transient server state: a Turbo cache snapshot must not\n // resurrect the hook — the fresh subscription below re-decides it.\n this.element.removeAttribute(\"data-live-counter-rejected\");\n if (this.channelValue) {\n this.#subscription = createConfirmedSubscription(\n { channel: this.channelValue, ...this.#params },\n {\n connected: () => this.#syncTriggers(),\n // A drop closes the send window (the shared subscription tracks it)\n // until Action Cable reconnects and re-confirms — an increment during\n // the outage would bump the display while its perform() is silently\n // discarded by the closed socket.\n disconnected: () => this.#syncTriggers(),\n // The server refused the subscription (auth, bad params): the gate\n // stays shut for good, and the hook lets the consumer's CSS disable\n // or hide the trigger instead of leaving a silently dead button.\n rejected: () => {\n this.element.setAttribute(\"data-live-counter-rejected\", \"true\");\n this.#syncTriggers();\n },\n received: (data: unknown) => this.#onReceived(data),\n },\n );\n }\n // Also covers a Turbo cache restore that snapshotted a disabled trigger:\n // the fresh (unconfirmed or absent) subscription re-decides the state.\n this.#syncTriggers();\n }\n\n override disconnect(): void {\n this.#subscription?.unsubscribe();\n this.#subscription = null;\n this.element.removeAttribute(\"data-live-counter-rejected\");\n }\n\n /** Late-added triggers (e.g. via a Turbo Stream) pick up the current gate. */\n triggerTargetConnected(target: HTMLElement): void {\n this.#syncTrigger(target);\n }\n\n /**\n * Optimistic local increment: bumps the display immediately, then asks the\n * server to persist and broadcast. The step comes from the action param\n * (`data-stimeo--live-counter-delta-param`), default 1. Bound via `data-action`.\n */\n increment(event?: Event & { params?: { delta?: number } }): void {\n // Before the confirmed subscription — and while the connection is down —\n // a perform() is silently dropped by Action Cable: bumping the display\n // then would diverge from the server, so the whole increment is dropped\n // (screen and server stay consistent).\n if (this.#subscription && !this.#subscription.confirmed) return;\n // Action params arrive as authored strings; normalize and ignore garbage.\n const raw = Number(event?.params?.delta ?? 1);\n const delta = Number.isFinite(raw) ? raw : 1;\n // Without an own `id` the delta echo cannot be deduped: skip the optimistic\n // bump and let the broadcast apply the increment exactly once. Channel-less\n // (local-only) counters have no echo, so they always bump.\n if (!this.#subscription || this.idValue !== \"\") {\n this.#write(this.#current + delta);\n }\n this.#subscription?.perform(\"increment\", { id: this.idValue, delta });\n }\n\n /** Reconciles a broadcast: absolute `count` wins; own-echo deltas are skipped. */\n #onReceived(data: unknown): void {\n const message = data as { count?: unknown; delta?: unknown; by?: unknown } | null;\n if (typeof message?.count === \"number\") {\n this.#write(message.count);\n return;\n }\n if (typeof message?.delta === \"number\") {\n // The own echo: this client already applied the delta optimistically.\n if (typeof message.by === \"string\" && message.by !== \"\" && message.by === this.idValue) {\n return;\n }\n this.#write(this.#current + message.delta);\n }\n }\n\n /** True while an increment would go through (channel-less counters always are). */\n get #ready(): boolean {\n return !this.#subscription || this.#subscription.confirmed;\n }\n\n /**\n * Reflects the send gate onto the optional `trigger` targets as the real\n * `disabled` attribute — the declarative alternative to styling off the\n * `data-live-counter-rejected` hook (a disabled control is also skipped by\n * keyboard focus and announced by AT, which CSS alone cannot do).\n */\n #syncTriggers(): void {\n for (const trigger of this.triggerTargets) this.#syncTrigger(trigger);\n }\n\n /**\n * `disabled` is a shared attribute: only disable what is currently enabled\n * (marking it ours), and only lift a disabled carrying our marker — so an\n * authored-disabled trigger (\"disabled until valid\", say) is never\n * re-enabled by the gate. A marked disabled restored from a Turbo cache\n * snapshot is recognized as ours and lifted once the gate opens.\n */\n #syncTrigger(trigger: HTMLElement): void {\n if (this.#ready) {\n if (trigger.hasAttribute(DISABLED_MARKER)) {\n trigger.removeAttribute(\"disabled\");\n trigger.removeAttribute(DISABLED_MARKER);\n }\n } else if (!trigger.hasAttribute(\"disabled\")) {\n trigger.setAttribute(\"disabled\", \"\");\n trigger.setAttribute(DISABLED_MARKER, \"\");\n }\n }\n\n /** The displayed element: the `value` target, else the controller element. */\n get #display(): HTMLElement {\n return this.hasValueTarget ? this.valueTarget : this.element;\n }\n\n /** The current count, parsed from the DOM (the single source of truth). */\n get #current(): number {\n // Strip separators/suffixes (\"1,200 likes\") like count-up, so a formatted\n // server-rendered value doesn't collapse to its first digit group.\n const parsed = Number.parseInt((this.#display.textContent ?? \"\").replace(/[^0-9-]/g, \"\"), 10);\n return Number.isNaN(parsed) ? 0 : parsed;\n }\n\n #write(count: number): void {\n if (count === this.#current) return;\n this.#display.textContent = String(count);\n this.dispatch(\"change\", { detail: { count } });\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeInterval, SafeTimeout } from \"../utils/safe_timeout\";\nimport {\n type ConfirmedCableSubscription,\n createConfirmedSubscription,\n parseSubscriptionParams,\n} from \"./consumer\";\n\n/** A peer currently present (another client in the same room). */\ninterface Peer {\n name: string;\n /** Auto-expiry timer id, restarted by every beacon. */\n timer: number;\n}\n\n/**\n * Minimum ms between outgoing beacons. The steady state is the `heartbeat`\n * interval; this guard only bounds the *extra* beacons sent in response to\n * newly seen peers (see the roster-convergence note in the class doc), so a\n * burst of joiners cannot make this client flood the channel.\n */\nconst BEACON_THROTTLE_MS = 2000;\n\n/**\n * Headless **presence** — a *server-bound* behavior: online dots / a\n * \"who's viewing this\" stack, bound to an Action Cable channel. Like\n * `stimeo--typing-indicator`, the server stays a trivial rebroadcast channel\n * with **zero presence state**: every client heartbeats an `appear` beacon\n * (`{ id, name }`), and each client expires peers it has not heard from for\n * `timeout` ms. Leaving (`{ id, leaving: true }`) is broadcast best-effort on\n * `disconnect()` and on `pagehide` (tab close / hard navigation, where\n * `disconnect()` never runs); a lost notice is caught by the expiry. Ships in\n * the opt-in `stimeo-ui/cable` subpath (`@rails/actioncable` optional peer).\n *\n * Roster convergence: a late joiner would otherwise see peers only as their\n * next heartbeats arrive, so on hearing a beacon from an *unknown* peer, each\n * client re-announces itself (at most one such answer every 2s) — the\n * roster converges in one round-trip instead of one heartbeat period.\n *\n * Markup contract (identifier: `stimeo--presence`):\n * <div data-controller=\"stimeo--presence\"\n * data-stimeo--presence-channel-value=\"PresenceChannel\"\n * data-stimeo--presence-params-value='{\"room\":\"doc_7\"}'\n * data-stimeo--presence-id-value=\"17\" data-stimeo--presence-name-value=\"Alice\">\n * <span data-stimeo--presence-target=\"count\" data-other=\"%{count} viewing\"></span>\n * <ul data-stimeo--presence-target=\"list\"></ul>\n * <template data-stimeo--presence-target=\"template\">\n * <li><span data-presence-name></span></li>\n * </template>\n * </div>\n *\n * Server contract (a trivial rebroadcast channel):\n * class PresenceChannel < ApplicationCable::Channel\n * def subscribed = stream_from \"presence:#{params[:room]}\"\n * def appear(data) = ActionCable.server.broadcast(\"presence:#{params[:room]}\",\n * { id: data[\"id\"], name: data[\"name\"], leaving: data[\"leaving\"] })\n * end\n *\n * `join` dispatches `{ id, name }`; `leave` dispatches `{ id }`; `change` dispatches\n * `{ users }`.\n *\n * @remarks\n * Behavior only — the dot/stack look is the consumer's CSS, keyed off the\n * `data-present` / `data-present-count` hooks. The roster counts and renders\n * **other** clients (`id` ≠ own `id`); rendering is optional: given a `list` +\n * `template` pair, one clone per peer is appended (elements marked\n * `data-presence-name` receive the name; the clone root is tagged\n * `data-presence-id`), and the `count` target renders through localizable\n * `data-zero` / `data-one` / `data-other` templates (`%{count}`). Richer\n * per-user rendering (avatars, links) belongs to the consumer via the `join` /\n * `leave` / `change` events or a server-rendered Turbo Stream. The `id`\n * comparison is display-level echo suppression, not authentication — identity\n * belongs to the server. Sending tracks the full subscription lifecycle (via\n * the shared confirmation-aware subscription): beacons — heartbeats, the\n * convergence answer, the leaving notice — are dropped before confirmation and\n * while the connection is down, where Action Cable would discard them anyway\n * (and a discarded beacon must not burn the convergence throttle); `connected`\n * re-fires on every reconnect and force-beacons, so the roster self-heals. A\n * refused subscription publishes the `data-presence-rejected` hook (cleared on\n * `connect()` — rejection is transient server state), mirroring\n * `data-live-counter-rejected`. Presence is transient: `connect()` clears\n * whatever a Turbo cache snapshot preserved (hooks + rendered clones), renders\n * the known-empty count (the `data-present*` hooks stay absent until the first\n * beacon), and the stream re-populates; the subscription, heartbeat interval,\n * per-peer expiry timers, and the `pagehide` listener are all released on\n * `disconnect()` (Turbo navigation included).\n */\nexport class PresenceController extends Controller<HTMLElement> {\n static override targets = [\"count\", \"list\", \"template\"];\n static override values = {\n channel: { type: String, default: \"\" },\n params: { type: String, default: \"\" },\n id: { type: String, default: \"\" },\n name: { type: String, default: \"\" },\n heartbeat: { type: Number, default: 15_000 },\n timeout: { type: Number, default: 40_000 },\n };\n static events = [\"join\", \"leave\", \"change\"] as const;\n\n declare readonly hasCountTarget: boolean;\n declare readonly countTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly listTarget: HTMLElement;\n declare readonly hasTemplateTarget: boolean;\n declare readonly templateTarget: HTMLTemplateElement;\n declare channelValue: string;\n declare paramsValue: string;\n declare idValue: string;\n declare nameValue: string;\n declare heartbeatValue: number;\n declare timeoutValue: number;\n\n /** Identifier parameters parsed once from their declaration, never in the hot path. */\n #params: Record<string, unknown> = {};\n\n #subscription: ConfirmedCableSubscription | null = null;\n /** Present peers keyed by id (insertion order = join order). */\n readonly #peers = new Map<string, Peer>();\n readonly #timers = new SafeTimeout();\n readonly #intervals = new SafeInterval();\n /** Epoch ms of the last outgoing beacon, for the convergence throttle. */\n #lastBeaconAt = 0;\n /** Pending trailing-edge convergence beacon (at most one queued). */\n #pendingBeacon: number | null = null;\n\n /**\n * Re-parses the identifier parameters when the declaration changes.\n *\n * A malformed declaration falls back to no parameters, so the identifier keeps\n * naming the channel instead of the subscription never being created at all.\n */\n paramsValueChanged(): void {\n this.#params = parseSubscriptionParams(this.paramsValue);\n }\n\n override connect(): void {\n // Presence is transient: drop whatever a Turbo cache snapshot preserved\n // (hooks + rendered clones); the live stream re-populates the roster.\n // Rejection is transient server state too — the fresh subscription below\n // re-decides the hook.\n this.#reset();\n this.element.removeAttribute(\"data-presence-rejected\");\n // The roster is known-empty here, so the count target can say so right\n // away instead of sitting blank until the first roster change. The\n // data-present* hooks intentionally stay absent until the first beacon.\n if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(0);\n\n if (!this.channelValue) return;\n this.#subscription = createConfirmedSubscription(\n { channel: this.channelValue, ...this.#params },\n {\n // The first beacon must wait for the confirmed subscription — a\n // perform() before that is silently dropped by Action Cable. Fires\n // again on every reconnect, so the roster self-heals after an outage.\n connected: () => this.#beacon(true),\n // The server refused the subscription: no beacon will ever go through,\n // and the hook lets the consumer's CSS reflect the dead stream.\n rejected: () => {\n this.element.setAttribute(\"data-presence-rejected\", \"true\");\n },\n received: (data: unknown) => this.#onReceived(data),\n },\n );\n this.#intervals.set(() => this.#beacon(true), this.heartbeatValue);\n window.addEventListener(\"pagehide\", this.#onPageHide);\n }\n\n override disconnect(): void {\n window.removeEventListener(\"pagehide\", this.#onPageHide);\n // Best-effort graceful leave; a lost notice is caught by peers' expiry\n // timers instead.\n this.#sendLeaveNotice();\n this.#subscription?.unsubscribe();\n this.#subscription = null;\n this.#intervals.clearAll();\n this.#reset();\n this.element.removeAttribute(\"data-presence-rejected\");\n }\n\n /**\n * Sends the best-effort leaving notice (skipped without an own `id`, and\n * outside the confirmed window, where Action Cable would discard it anyway).\n */\n #sendLeaveNotice(): void {\n if (!this.#subscription?.confirmed || !this.idValue) return;\n this.#subscription.perform(\"appear\", { id: this.idValue, leaving: true });\n }\n\n /**\n * `pagehide` covers the leaves `disconnect()` cannot see: closing the tab or\n * a hard (non-Turbo) navigation destroys the page without running Stimulus\n * teardown, so this listener is the only chance to announce them. Best-effort\n * by nature (the socket may close before the frame flushes); peers' expiry\n * timers stay the safety net. If the page enters the bfcache and is restored\n * instead, the next heartbeat re-announces this client, so an over-eager\n * leave self-heals.\n */\n readonly #onPageHide = (): void => {\n this.#sendLeaveNotice();\n };\n\n /**\n * Broadcasts this client's beacon (skipped without an own `id`). A throttled\n * convergence answer is deferred to the trailing edge rather than dropped —\n * otherwise a peer joining right after a heartbeat would not learn about\n * this client until the next full heartbeat period.\n *\n * Gated on the confirmed subscription: before confirmation and during an\n * outage Action Cable silently discards perform(), so a beacon sent then is\n * pure waste — worse, it would burn `#lastBeaconAt` and delay the next real\n * convergence answer by up to the throttle window. The heartbeat interval\n * keeps ticking regardless; `connected` re-fires on reconfirm and\n * force-beacons immediately, so a gated tick is never missed for long.\n */\n #beacon(force: boolean): void {\n if (!this.#subscription?.confirmed || !this.idValue) return;\n const now = Date.now();\n const wait = BEACON_THROTTLE_MS - (now - this.#lastBeaconAt);\n if (!force && wait > 0) {\n if (this.#pendingBeacon === null) {\n this.#pendingBeacon = this.#timers.set(() => {\n this.#pendingBeacon = null;\n this.#beacon(true);\n }, wait);\n }\n return;\n }\n this.#lastBeaconAt = now;\n this.#subscription.perform(\"appear\", { id: this.idValue, name: this.nameValue });\n }\n\n /**\n * Tracks a broadcast beacon: upserts the peer (restarting its expiry timer),\n * removes it on a `leaving` notice, and re-announces this client when the\n * peer was unknown (roster convergence — see the class doc).\n */\n #onReceived(data: unknown): void {\n const beacon = data as { id?: unknown; name?: unknown; leaving?: unknown } | null;\n const id = beacon?.id;\n if (typeof id !== \"string\" || id === \"\" || id === this.idValue) return;\n\n if (beacon?.leaving === true) {\n this.#drop(id);\n return;\n }\n\n const name = typeof beacon?.name === \"string\" ? beacon.name : \"\";\n const existing = this.#peers.get(id);\n if (existing !== undefined) this.#timers.clear(existing.timer);\n const timer = this.#timers.set(() => this.#drop(id), this.timeoutValue);\n this.#peers.set(id, { name, timer });\n\n if (existing === undefined) {\n this.#appendClone(id, name);\n this.#render();\n this.dispatch(\"join\", { detail: { id, name } });\n this.#beacon(false); // answer an unknown peer so its roster converges\n } else if (existing.name !== name) {\n this.#updateClone(id, name);\n this.#render();\n }\n }\n\n /** Removes a peer (expiry or graceful leave) and reflects the change. */\n #drop(id: string): void {\n const peer = this.#peers.get(id);\n if (peer === undefined) return;\n this.#timers.clear(peer.timer);\n this.#peers.delete(id);\n this.#removeClone(id);\n this.#render();\n this.dispatch(\"leave\", { detail: { id } });\n }\n\n /** Reflects the roster onto the hooks + count target and emits `change`. */\n #render(): void {\n const users = [...this.#peers.entries()].map(([id, peer]) => ({ id, name: peer.name }));\n this.element.setAttribute(\"data-present\", users.length > 0 ? \"true\" : \"false\");\n this.element.setAttribute(\"data-present-count\", String(users.length));\n if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(users.length);\n this.dispatch(\"change\", { detail: { users } });\n }\n\n /**\n * Builds the count copy. Localizable through `data-zero` / `data-one` /\n * `data-other` templates on the count target (`%{count}`); the bare number is\n * the fallback (copy-free, so nothing to localize by default).\n */\n #countMessage(count: number): string {\n const templates = this.countTarget.dataset;\n const template =\n (count === 0 ? templates.zero : count === 1 ? templates.one : templates.other) ??\n templates.other;\n return template ? template.replace(\"%{count}\", String(count)) : String(count);\n }\n\n /** Appends one template clone for a newly present peer (list + template only). */\n #appendClone(id: string, name: string): void {\n if (!this.hasListTarget || !this.hasTemplateTarget) return;\n const clone = this.templateTarget.content.cloneNode(true) as DocumentFragment;\n const root = clone.firstElementChild;\n if (!root) return;\n root.setAttribute(\"data-presence-id\", id);\n this.#fillName(root, name);\n this.listTarget.appendChild(clone);\n }\n\n #updateClone(id: string, name: string): void {\n const root = this.#cloneFor(id);\n if (root) this.#fillName(root, name);\n }\n\n #removeClone(id: string): void {\n this.#cloneFor(id)?.remove();\n }\n\n #cloneFor(id: string): Element | null {\n if (!this.hasListTarget) return null;\n // Attribute selectors cannot escape arbitrary ids reliably; match manually.\n for (const child of this.listTarget.querySelectorAll(\"[data-presence-id]\")) {\n if (child.getAttribute(\"data-presence-id\") === id) return child;\n }\n return null;\n }\n\n /** Writes the peer's name into the clone's `data-presence-name` slots. */\n #fillName(root: Element, name: string): void {\n const slots = root.querySelectorAll(\"[data-presence-name]\");\n for (const slot of slots) slot.textContent = name;\n if (slots.length === 0 && root.hasAttribute(\"data-presence-name\")) {\n root.textContent = name;\n }\n }\n\n /** Clears the transient roster state (connect reset + disconnect teardown). */\n #reset(): void {\n this.#timers.clearAll();\n this.#pendingBeacon = null;\n this.#peers.clear();\n this.#lastBeaconAt = 0;\n this.element.removeAttribute(\"data-present\");\n this.element.removeAttribute(\"data-present-count\");\n if (this.hasCountTarget) this.countTarget.textContent = \"\";\n if (this.hasListTarget) {\n for (const child of this.listTarget.querySelectorAll(\"[data-presence-id]\")) {\n child.remove();\n }\n }\n }\n}\n","/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport {\n type ConfirmedCableSubscription,\n createConfirmedSubscription,\n parseSubscriptionParams,\n} from \"./consumer\";\n\n/** Milliseconds of silence after which a typer is dropped. */\nconst DEFAULT_TIMEOUT = 3000;\n/** Minimum milliseconds between outgoing typing signals. */\nconst DEFAULT_THROTTLE = 2000;\n\n/**\n * Fills `%{token}` placeholders in a status template, in one pass.\n *\n * The values come off the wire — another client's display name — so they must never\n * reach a replacement *string*, where `$&`, `` $` ``, `$'` and `$$` expand into the\n * template's own text. A single pass also stops a name that happens to contain a token\n * from being re-substituted by a later one. Every occurrence is filled; a token this\n * call has no value for is left as authored, which keeps a typo visible instead of\n * blanking the word.\n */\nconst fillTokens = (template: string, values: Record<string, string>): string =>\n template.replace(/%\\{(name|names|count)\\}/g, (match, token: string) => values[token] ?? match);\n\n/**\n * Headless **typing indicator** — a *server-bound* behavior: a controller whose\n * state lives on the server stream, not in client memory. Typing in the composer\n * throttle-broadcasts a `typing` signal over an Action Cable channel; received\n * signals from *other* clients render \"X is typing…\" into a visible `status` slot\n * and auto-clear after `timeout` ms of silence. The whole behavior is HTML + a\n * broadcast — no app JS, no client store, no reconciler. Ships in the opt-in\n * `stimeo-ui/cable` subpath (`@rails/actioncable` optional peer); the core stays\n * zero-dep.\n *\n * Markup contract (identifier: `stimeo--typing-indicator`):\n * <div data-controller=\"stimeo--typing-indicator\"\n * data-stimeo--typing-indicator-channel-value=\"TypingChannel\"\n * data-stimeo--typing-indicator-params-value='{\"room\":\"chat_42\"}'\n * data-stimeo--typing-indicator-name-value=\"Alice\"\n * data-stimeo--typing-indicator-announce-one-text-value=\"{name} is typing\">\n * <textarea data-stimeo--typing-indicator-target=\"input\"></textarea>\n * <p data-stimeo--typing-indicator-target=\"status\"\n * data-one=\"%{name} is typing…\" data-many=\"%{names} are typing…\"></p>\n * </div>\n *\n * Server contract (a trivial rebroadcast channel):\n * class TypingChannel < ApplicationCable::Channel\n * def subscribed = stream_from \"typing:#{params[:room]}\"\n * def typing(data) = ActionCable.server.broadcast(\"typing:#{params[:room]}\",\n * { name: data[\"name\"] })\n * end\n *\n * The `status` target is a plain visible slot, not a live region: assistive tech is\n * reached through the page's shared announcer instead, so a set that settles is sent\n * there as `announceOneText` / `announceManyText` (`{name}` / `{names}` / `{count}`,\n * debounced). Both default to empty, and an empty message announces nothing — a page\n * that wants the indicator seen but not heard simply leaves them off.\n *\n * `change` dispatches `{ names }`.\n *\n * @remarks\n * Behavior only — the indicator's look is the consumer's CSS, keyed off the\n * `data-typing` hook; the copy is localizable through the `data-one` /\n * `data-many` templates (`%{name}` / `%{names}` / `%{count}`, terse English\n * fallback — the same channel design as `stimeo--sortable`). The own echo is\n * dropped by comparing the broadcast `name` against `name` (a same-name guard,\n * not authentication — identity belongs to the server). Sending tracks the full\n * subscription lifecycle (via the shared confirmation-aware subscription):\n * signals are dropped before confirmation and while the connection is down, so\n * an outage never burns the throttle window on undeliverable sends. A refused\n * subscription publishes the `data-typing-indicator-rejected` hook (cleared on\n * `connect()` — rejection is transient server state), mirroring\n * `data-live-counter-rejected`. Typing state is transient by nature:\n * `connect()` resets the hook and the status slot (a Turbo cache snapshot must\n * not resurrect a stale \"X is typing…\"), and re-population happens naturally\n * from the stream. The subscription, the per-typer timers, and the delegated\n * `input` listener are all released on `disconnect()` (Turbo navigation\n * included).\n */\nexport class TypingIndicatorController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"status\"];\n static override values = {\n channel: { type: String, default: \"\" },\n params: { type: String, default: \"\" },\n name: { type: String, default: \"\" },\n timeout: { type: Number, default: DEFAULT_TIMEOUT },\n // Must stay below `timeout`: the throttle is leading-edge only (no trailing\n // send), so a receiver's display survives continuous typing only while a\n // fresh signal lands within its timeout window.\n throttle: { type: Number, default: DEFAULT_THROTTLE },\n announceOneText: { type: String, default: \"\" },\n announceManyText: { type: String, default: \"\" },\n };\n static events = [\"change\"] as const;\n\n declare readonly hasStatusTarget: boolean;\n declare readonly statusTarget: HTMLElement;\n declare channelValue: string;\n declare paramsValue: string;\n declare nameValue: string;\n declare timeoutValue: number;\n declare throttleValue: number;\n declare announceOneTextValue: string;\n declare announceManyTextValue: string;\n\n /** Delay (ms) before one settled typer set is sent to the shared announcer. */\n static readonly #announceDelay = 200;\n\n /** Identifier parameters parsed once from their declaration, never in the hot path. */\n #params: Record<string, unknown> = {};\n\n #subscription: ConfirmedCableSubscription | null = null;\n /** Names currently typing (other clients), each with its auto-clear timer id. */\n readonly #typers = new Map<string, number>();\n readonly #timers = new SafeTimeout();\n /** Epoch ms of the last broadcast, for leading-edge throttling. */\n #lastSentAt = 0;\n /** The one outstanding announcement, so a newer set supersedes it. */\n #announceId: number | null = null;\n\n /**\n * Re-parses the identifier parameters when the declaration changes.\n *\n * A malformed declaration falls back to no parameters, so the identifier keeps\n * naming the channel instead of the subscription never being created at all.\n */\n paramsValueChanged(): void {\n this.#params = parseSubscriptionParams(this.paramsValue);\n }\n\n /**\n * Paints the current copy into a `status` target that arrived at runtime.\n *\n * No event: the set of typers did not change, only the element that displays it.\n * Without this a region swapped in mid-conversation stays empty while `data-typing`\n * still says someone is typing, leaving the state in the visual hook alone. An empty\n * set needs no paint — a fresh region already shows it.\n */\n statusTargetConnected(): void {\n if (this.#typers.size > 0) this.#paint();\n }\n\n override connect(): void {\n // Typing state is transient: a Turbo cache snapshot must not resurrect a\n // stale indicator, and the live stream re-populates naturally. Rejection is\n // transient server state too — the fresh subscription re-decides the hook.\n this.element.removeAttribute(\"data-typing\");\n this.element.removeAttribute(\"data-typing-indicator-rejected\");\n if (this.hasStatusTarget) this.statusTarget.textContent = \"\";\n\n // Delegated on the container so the composer needs no per-input data-action\n // (and swapped/appended inputs keep working).\n this.element.addEventListener(\"input\", this.#onInput);\n if (this.channelValue) {\n // Confirmation tracking (connected / disconnected / rejected) lives in\n // the shared subscription; #onInput gates on its `confirmed` so an\n // outage doesn't burn the throttle window on dropped sends.\n this.#subscription = createConfirmedSubscription(\n { channel: this.channelValue, ...this.#params },\n {\n // The server refused the subscription: the send gate stays shut for\n // good, and the hook lets the consumer's CSS reflect the dead stream.\n rejected: () => {\n this.element.setAttribute(\"data-typing-indicator-rejected\", \"true\");\n },\n received: (data: unknown) => this.#onReceived(data),\n },\n );\n }\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"input\", this.#onInput);\n this.#subscription?.unsubscribe();\n this.#subscription = null;\n this.#timers.clearAll();\n this.#announceId = null;\n this.#typers.clear();\n this.element.removeAttribute(\"data-typing\");\n this.element.removeAttribute(\"data-typing-indicator-rejected\");\n if (this.hasStatusTarget) this.statusTarget.textContent = \"\";\n this.#lastSentAt = 0;\n }\n\n /** Throttled (leading-edge) broadcast of this client's typing signal. */\n readonly #onInput = (): void => {\n // Before the confirmed subscription a perform() is silently dropped by\n // Action Cable — and recording #lastSentAt would ALSO throttle away the\n // first real send, so bail without touching the throttle state.\n if (!this.#subscription?.confirmed) return;\n const now = Date.now();\n if (now - this.#lastSentAt < this.#throttle) return;\n this.#lastSentAt = now;\n this.#subscription.perform(\"typing\", { name: this.nameValue });\n };\n\n /**\n * Tracks a broadcast typer. The own echo is dropped (same `name`); every\n * further signal from a name restarts its auto-clear timer, so the indicator\n * survives continuous typing and clears `timeout` ms after the last signal.\n */\n #onReceived(data: unknown): void {\n const name = (data as { name?: unknown } | null)?.name;\n if (typeof name !== \"string\" || name === \"\" || name === this.nameValue) return;\n\n const existing = this.#typers.get(name);\n if (existing !== undefined) this.#timers.clear(existing);\n const added = existing === undefined;\n this.#typers.set(\n name,\n this.#timers.set(() => this.#untrack(name), this.#timeout),\n );\n if (added) this.#render();\n }\n\n /**\n * The silence after which a typer is dropped, in ms: a finite, non-negative number.\n * Anything else names no delay — `setTimeout` reads `NaN`, a negative value and\n * `Infinity` alike as \"now\", so the typer would vanish in the same task it appeared\n * and the indicator could never be seen. Such a declaration falls back to the default.\n */\n get #timeout(): number {\n const declared = this.timeoutValue;\n return Number.isFinite(declared) && declared >= 0 ? declared : DEFAULT_TIMEOUT;\n }\n\n /**\n * The minimum gap between outgoing signals, in ms: a finite, non-negative number.\n * Anything else names no interval, and the gate then settles the same way at every\n * keystroke — `NaN` and a negative gap leave it open, so every keystroke broadcasts,\n * while `Infinity` is never exceeded, so nothing is ever sent. Such a declaration\n * falls back to the default.\n */\n get #throttle(): number {\n const declared = this.throttleValue;\n return Number.isFinite(declared) && declared >= 0 ? declared : DEFAULT_THROTTLE;\n }\n\n #untrack(name: string): void {\n this.#typers.delete(name);\n this.#render();\n }\n\n /** Reflects the typer set onto the display, announces it, and emits `change`. */\n #render(): void {\n const names = this.#paint();\n this.#announce(names);\n this.dispatch(\"change\", { detail: { names } });\n }\n\n /**\n * Hands the settled typer set to the page's shared announcer.\n *\n * Debounced: a burst of arrivals is one announcement, not one per name. Only a\n * non-empty set is announced — that typing stopped is not news worth interrupting\n * a reader for, and the visible copy already clears. Wording comes from the\n * consumer, and {@link announce} drops an empty message, so an undeclared template\n * announces nothing at all.\n */\n #announce(names: string[]): void {\n if (this.#announceId !== null) this.#timers.clear(this.#announceId);\n this.#announceId = null;\n if (names.length === 0) return;\n\n const message =\n names.length === 1\n ? fillTemplate(this.announceOneTextValue, { name: names[0] ?? \"\" })\n : fillTemplate(this.announceManyTextValue, {\n names: names.join(\", \"),\n count: names.length,\n });\n\n this.#announceId = this.#timers.set(() => {\n announce(message);\n this.#announceId = null;\n }, TypingIndicatorController.#announceDelay);\n }\n\n /** Writes the current typer set onto the hook and the status slot. */\n #paint(): string[] {\n const names = [...this.#typers.keys()];\n this.element.setAttribute(\"data-typing\", names.length > 0 ? \"true\" : \"false\");\n if (this.hasStatusTarget) {\n this.statusTarget.textContent = this.#message(names);\n }\n return names;\n }\n\n /**\n * Builds the status copy. Localizable through `data-one` / `data-many`\n * templates on the status target (`%{name}` / `%{names}` / `%{count}`); terse\n * English is the fallback.\n */\n #message(names: string[]): string {\n if (names.length === 0) return \"\";\n const joined = names.join(\", \");\n if (names.length === 1) {\n const template = this.statusTarget.dataset.one;\n const name = names[0] ?? \"\";\n return template ? fillTokens(template, { name }) : `${name} is typing…`;\n }\n const template = this.statusTarget.dataset.many;\n return template\n ? fillTokens(template, { names: joined, count: String(names.length) })\n : `${joined} are typing…`;\n }\n}\n","import type { Application } from \"@hotwired/stimulus\";\nimport { LiveCounterController } from \"./live_counter_controller\";\nimport { PresenceController } from \"./presence_controller\";\nimport { TypingIndicatorController } from \"./typing_indicator_controller\";\n\nexport {\n type CableConsumer,\n type CableSubscription,\n type CableSubscriptionMixin,\n type ConfirmedCableSubscription,\n createConfirmedSubscription,\n getCableConsumer,\n setCableConsumer,\n} from \"./consumer\";\nexport { LiveCounterController, PresenceController, TypingIndicatorController };\n\n/**\n * Opt-in **server-bound** behaviors for Stimeo — controllers whose state lives\n * on an Action Cable / Turbo stream rather than in client memory: presence,\n * typing indicators, live counters.\n *\n * **Why this is a separate entry point.** The core library is zero-runtime-dep:\n * `import \"stimeo-ui\"` pulls in nothing but `@hotwired/stimulus`. Binding the\n * DOM to a server stream genuinely needs `@rails/actioncable`, so — exactly\n * like `stimeo-ui/positioning` and `@floating-ui/dom` — that cost is opt-in:\n * this module lives at `stimeo-ui/cable`, `@rails/actioncable` is an *optional*\n * peer, and nothing in the core imports it.\n *\n * When the app already owns a consumer (`app/javascript/channels/consumer.js`),\n * hand it over once at boot so no second websocket is opened:\n *\n * ```ts\n * import consumer from \"./channels/consumer\";\n * import { registerCable, setCableConsumer } from \"stimeo-ui/cable\";\n *\n * setCableConsumer(consumer);\n * registerCable(application);\n * ```\n */\nexport const cableControllers = {\n \"stimeo--live-counter\": LiveCounterController,\n \"stimeo--presence\": PresenceController,\n \"stimeo--typing-indicator\": TypingIndicatorController,\n} as const;\n\n/**\n * Registers the opt-in server-bound controllers on a Stimulus Application\n * (additive to `registerStimeo`, mirroring `registerPositioning`).\n */\nexport function registerCable(application: Application): void {\n for (const [identifier, controller] of Object.entries(cableControllers)) {\n application.register(identifier, controller);\n }\n}\n"]}
@@ -75,6 +75,7 @@ declare class PointerDragController extends Controller<HTMLElement> {
75
75
  default: boolean;
76
76
  };
77
77
  };
78
+ static actions: readonly ["reset"];
78
79
  static events: readonly ["start", "move", "end", "cancel"];
79
80
  readonly handleTargets: HTMLElement[];
80
81
  readonly hasHandleTarget: boolean;
@@ -85,11 +86,27 @@ declare class PointerDragController extends Controller<HTMLElement> {
85
86
  followValue: boolean;
86
87
  connect(): void;
87
88
  disconnect(): void;
88
- /** Prepares handles added at runtime (touch-action + focusability). */
89
+ /**
90
+ * Returns the element to its origin: drops the committed follow offset and the
91
+ * inline `translate` that carries it.
92
+ *
93
+ * In follow mode the inline `translate` belongs to this controller, and the
94
+ * committed offset lives in a field the DOM cannot reach — so a consumer that
95
+ * wants the element back at the start, or that has written a position of its
96
+ * own, needs this to make the two agree again. An in-flight drag is cancelled
97
+ * first, or its deltas would land on top of the offset just cleared.
98
+ */
99
+ reset(): void;
89
100
  handleTargetConnected(handle: HTMLElement): void;
90
101
  handleTargetDisconnected(handle: HTMLElement): void;
91
- /** Re-derives the handles' touch-action when the axis changes. */
92
- axisValueChanged(): void;
102
+ /**
103
+ * Re-derives the handles' touch-action when the axis changes.
104
+ *
105
+ * Two layers, like the `tabindex` loan: the marker says the value was once
106
+ * ours, and the current inline value says it still is. A consumer that wrote
107
+ * its own `touch-action` after connect keeps it.
108
+ */
109
+ axisValueChanged(_value: string, previousValue: string | undefined): void;
93
110
  /** Cancels any in-flight session when the controller is disabled mid-drag. */
94
111
  disabledValueChanged(): void;
95
112
  }
@@ -64,6 +64,7 @@ var DetachGate = class _DetachGate {
64
64
  };
65
65
 
66
66
  // src/controllers/pointer_drag_controller.ts
67
+ var NATIVE_KEY_OWNERS = "input, textarea, select, button, a[href], summary, [contenteditable]";
67
68
  var PointerDragController = class _PointerDragController extends Controller {
68
69
  static targets = ["handle"];
69
70
  static values = {
@@ -73,6 +74,7 @@ var PointerDragController = class _PointerDragController extends Controller {
73
74
  disabled: { type: Boolean, default: false },
74
75
  follow: { type: Boolean, default: false }
75
76
  };
77
+ static actions = ["reset"];
76
78
  static events = ["start", "move", "end", "cancel"];
77
79
  /** Marker attribute recording that this controller set the touch-action. */
78
80
  static #TOUCH_ACTION_MARKER = "data-pointer-drag-touch-action";
@@ -106,23 +108,49 @@ var PointerDragController = class _PointerDragController extends Controller {
106
108
  }
107
109
  this.#teardown();
108
110
  }
109
- /** Prepares handles added at runtime (touch-action + focusability). */
111
+ /**
112
+ * Returns the element to its origin: drops the committed follow offset and the
113
+ * inline `translate` that carries it.
114
+ *
115
+ * In follow mode the inline `translate` belongs to this controller, and the
116
+ * committed offset lives in a field the DOM cannot reach — so a consumer that
117
+ * wants the element back at the start, or that has written a position of its
118
+ * own, needs this to make the two agree again. An in-flight drag is cancelled
119
+ * first, or its deltas would land on top of the offset just cleared.
120
+ */
121
+ reset() {
122
+ const live = this.#pointer?.started ? this.#pointer.pointerType : this.#keyboard ? "keyboard" : null;
123
+ if (this.#pointer || this.#keyboard) this.#teardownSessions();
124
+ if (live) this.#dispatchCancel(live);
125
+ this.#followBase = { x: 0, y: 0 };
126
+ this.#applyFollow(0, 0);
127
+ }
110
128
  handleTargetConnected(handle) {
129
+ if (handle !== this.element) this.#restoreHandle(this.element);
111
130
  this.#prepareHandle(handle);
112
131
  }
113
132
  handleTargetDisconnected(handle) {
114
- if (!this.element.contains(handle)) {
115
- if (this.#pointer?.handle === handle) this.#endPointerSession();
116
- if (this.#keyboard?.handle === handle) this.#clearKeyboardSession();
117
- }
118
133
  this.#restoreHandle(handle);
134
+ if (this.element.contains(handle)) return;
135
+ const interrupted = this.#pointer?.handle === handle && this.#pointer.started ? this.#pointer.pointerType : this.#keyboard?.handle === handle ? "keyboard" : null;
136
+ if (this.#pointer?.handle === handle) this.#endPointerSession();
137
+ if (this.#keyboard?.handle === handle) this.#clearKeyboardSession();
138
+ if (!this.hasHandleTarget) this.#prepareHandle(this.element);
139
+ if (interrupted) this.#dispatchCancel(interrupted);
119
140
  }
120
- /** Re-derives the handles' touch-action when the axis changes. */
121
- axisValueChanged() {
141
+ /**
142
+ * Re-derives the handles' touch-action when the axis changes.
143
+ *
144
+ * Two layers, like the `tabindex` loan: the marker says the value was once
145
+ * ours, and the current inline value says it still is. A consumer that wrote
146
+ * its own `touch-action` after connect keeps it.
147
+ */
148
+ axisValueChanged(_value, previousValue) {
149
+ const lent = this.#touchActionFor(previousValue ?? this.axisValue);
122
150
  for (const handle of this.#handles()) {
123
- if (handle.hasAttribute(_PointerDragController.#TOUCH_ACTION_MARKER)) {
124
- handle.style.touchAction = this.#touchActionForAxis();
125
- }
151
+ if (!handle.hasAttribute(_PointerDragController.#TOUCH_ACTION_MARKER)) continue;
152
+ if (handle.style.touchAction !== lent) continue;
153
+ handle.style.touchAction = this.#touchActionForAxis();
126
154
  }
127
155
  }
128
156
  /** Cancels any in-flight session when the controller is disabled mid-drag. */
@@ -203,9 +231,10 @@ var PointerDragController = class _PointerDragController extends Controller {
203
231
  const handle = this.#handleFor(event.target);
204
232
  if (!handle) return;
205
233
  if (event.defaultPrevented) return;
234
+ if (event.isComposing) return;
235
+ if (this.#ownsNativeKeys(event.target, handle)) return;
206
236
  if (isReservedArrowChord(event)) return;
207
237
  if (event.key === "Escape") {
208
- if (event.isComposing) return;
209
238
  if (this.#pointer?.started) {
210
239
  const { pointerType } = this.#pointer;
211
240
  this.#teardownSessions();
@@ -314,6 +343,7 @@ var PointerDragController = class _PointerDragController extends Controller {
314
343
  const interrupted = this.#pointer?.started ? this.#pointer.pointerType : this.#keyboard ? "keyboard" : null;
315
344
  this.#teardownSessions();
316
345
  for (const handle of this.#handles()) this.#restoreHandle(handle);
346
+ this.#followReset();
317
347
  if (interrupted && this.element.isConnected) this.#dispatchCancel(interrupted);
318
348
  }
319
349
  /**
@@ -383,8 +413,12 @@ var PointerDragController = class _PointerDragController extends Controller {
383
413
  }
384
414
  /** `touch-action` that lets the page keep panning on the locked axis only. */
385
415
  #touchActionForAxis() {
386
- if (this.axisValue === "x") return "pan-y";
387
- if (this.axisValue === "y") return "pan-x";
416
+ return this.#touchActionFor(this.axisValue);
417
+ }
418
+ /** The `touch-action` an `axis` declaration lends, for any axis value. */
419
+ #touchActionFor(axis) {
420
+ if (axis === "x") return "pan-y";
421
+ if (axis === "y") return "pan-x";
388
422
  return "none";
389
423
  }
390
424
  /**
@@ -395,8 +429,10 @@ var PointerDragController = class _PointerDragController extends Controller {
395
429
  * are marker-owned so `#restoreHandle` reverts them symmetrically on teardown.
396
430
  */
397
431
  #prepareHandle(handle) {
398
- if (handle.style.touchAction === "" || handle.hasAttribute(_PointerDragController.#TOUCH_ACTION_MARKER)) {
399
- handle.style.touchAction = this.#touchActionForAxis();
432
+ const lent = this.#touchActionForAxis();
433
+ const marked = handle.hasAttribute(_PointerDragController.#TOUCH_ACTION_MARKER);
434
+ if (handle.style.touchAction === "" || marked && handle.style.touchAction === lent) {
435
+ handle.style.touchAction = lent;
400
436
  handle.setAttribute(_PointerDragController.#TOUCH_ACTION_MARKER, "true");
401
437
  }
402
438
  if (handle.tabIndex < 0 && !handle.hasAttribute("tabindex")) {
@@ -404,17 +440,27 @@ var PointerDragController = class _PointerDragController extends Controller {
404
440
  handle.setAttribute(_PointerDragController.#TABINDEX_MARKER, "true");
405
441
  }
406
442
  }
443
+ /** Whether the key belongs to a native control or editing surface in the handle. */
444
+ #ownsNativeKeys(target, handle) {
445
+ const owner = target?.closest(NATIVE_KEY_OWNERS) ?? null;
446
+ return owner !== null && owner !== handle && handle.contains(owner);
447
+ }
407
448
  /** Reverts the marker-owned touch-action + tabindex (authored values untouched). */
408
449
  #restoreHandle(handle) {
409
450
  if (handle.hasAttribute(_PointerDragController.#TOUCH_ACTION_MARKER)) {
410
- handle.style.touchAction = "";
451
+ if (handle.style.touchAction === this.#touchActionForAxis()) {
452
+ handle.style.touchAction = "";
453
+ }
411
454
  handle.removeAttribute(_PointerDragController.#TOUCH_ACTION_MARKER);
412
455
  }
413
456
  if (handle.hasAttribute(_PointerDragController.#TABINDEX_MARKER)) {
414
- handle.removeAttribute(_PointerDragController.#TABINDEX_MARKER);
415
- if (handle.getAttribute("tabindex") === "0" && document.activeElement !== handle) {
457
+ const ours = handle.getAttribute("tabindex") === "0";
458
+ if (ours && document.activeElement !== handle) {
416
459
  handle.removeAttribute("tabindex");
460
+ handle.removeAttribute(_PointerDragController.#TABINDEX_MARKER);
461
+ return;
417
462
  }
463
+ if (!ours) handle.removeAttribute(_PointerDragController.#TABINDEX_MARKER);
418
464
  }
419
465
  }
420
466
  };