stimeo-ui 0.11.0 → 0.12.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.
- package/CHANGELOG.md +70 -0
- package/dist/cable/index.js +23 -2
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +12 -10
- package/dist/controllers/count_up_controller.js +74 -35
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +20 -7
- package/dist/controllers/intersection_controller.js +55 -8
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +31 -9
- package/dist/controllers/lazy_frame_controller.js +81 -19
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +4 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +18 -9
- package/dist/controllers/reading_progress_controller.js +177 -6
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js +13 -2
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +17 -8
- package/dist/controllers/smart_sticky_header_controller.js +52 -10
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +38 -8
- package/dist/controllers/sortable_controller.js +241 -67
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js +13 -2
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/index.js +486 -145
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.js +1 -0
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +1 -0
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +1 -1
- package/dist/inspector/manifest.json +8 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
While the version is `0.x`, the public API (the `stimeo--*` data attributes) may
|
|
8
8
|
change between releases.
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-09-06
|
|
11
|
+
|
|
12
|
+
Minor release with no new components. Six existing ones are reworked — count-up,
|
|
13
|
+
intersection, lazy-frame, reading-progress, smart-sticky-header, and sortable —
|
|
14
|
+
and the number reading count-up gained reaches live-counter too. Most of them
|
|
15
|
+
changed a contract, so read Removed and Changed before upgrading. The Inspector
|
|
16
|
+
manifest stays on schema v12.
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
|
|
20
|
+
- sortable: the `status` target, its `data-grabbed` / `data-moved` /
|
|
21
|
+
`data-dropped` / `data-canceled` templates, and the English fallback wording.
|
|
22
|
+
Seat a `stimeo--announcer` on the page and set `announceGrabbedText` /
|
|
23
|
+
`announceMovedText` / `announceDroppedText` / `announceCanceledText`
|
|
24
|
+
(`{name}` / `{position}` / `{total}`); an unset one announces nothing.
|
|
25
|
+
`stimeo check` stops asking for the live region and warns when no announcer
|
|
26
|
+
is seated.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- count-up: only the text node holding the number is animated, and while it
|
|
31
|
+
ticks it is wrapped in a `role="img"` element named with the authored text —
|
|
32
|
+
the host's own `aria-label` and `role` are never touched, and sibling markup
|
|
33
|
+
stays where it was.
|
|
34
|
+
- count-up and live-counter read a formatted number by one rule: the first
|
|
35
|
+
numeric token, group separators dropped, the fraction truncated, and a hyphen
|
|
36
|
+
a sign only where it opens the token — `"Sign-ups: 1,200"` reads 1200, not
|
|
37
|
+
-1200.
|
|
38
|
+
- intersection: `0` is always among the observed lines, so a non-zero
|
|
39
|
+
`threshold` also reports the element leaving for good (one more `change`;
|
|
40
|
+
`data-passed` and the ratio property follow the departure to its end), and an
|
|
41
|
+
exit across the start edge reports `position: "before"` while the element
|
|
42
|
+
still overlaps the root.
|
|
43
|
+
- lazy-frame: a re-fetch needs a genuine re-entry — the frame is seen inside the
|
|
44
|
+
observed area, leaves it, and comes back; where a connection or a
|
|
45
|
+
focus-started load first finds it is the baseline. A frame with `once` off is
|
|
46
|
+
armed again after a cache restore, and `load` names the URL that was fetched.
|
|
47
|
+
- reading-progress: an article with no layout box is not measured, the
|
|
48
|
+
article's own box is watched so late-settling content re-measures, and the
|
|
49
|
+
connect frame is the baseline — a restored scroll position does not fire
|
|
50
|
+
`complete`. Both faces of the custom property are leased: the element's copy
|
|
51
|
+
is returned on `disconnect()` too, an authored `:root` declaration comes back,
|
|
52
|
+
and a later writer is left alone.
|
|
53
|
+
- smart-sticky-header: `change` fires on transitions only (connecting is
|
|
54
|
+
silent), and the `offset` zone is decided ahead of the jitter guard.
|
|
55
|
+
- sortable: insertion is relative to the neighbouring item — the `list`-less
|
|
56
|
+
markup works and the last slot is after the last item — and a drag signal from
|
|
57
|
+
a pointer-drag nested inside an item no longer moves the card. The pickup slot
|
|
58
|
+
is remembered as neighbours, so rows inserted or removed mid-drag shift neither
|
|
59
|
+
the restore nor `from`; only laid-out siblings take part in pointer following;
|
|
60
|
+
and a session whose item leaves the item set ends instead of refusing every
|
|
61
|
+
later grab. pointer-drag consumes `Home` / `End` while grabbed.
|
|
62
|
+
- A declaration that cannot be read falls back to its default: count-up's
|
|
63
|
+
`duration` / `from`, smart-sticky-header's `offset`, and a `rootSelector` /
|
|
64
|
+
`containerSelector` that does not parse (intersection, sticky-observer,
|
|
65
|
+
smart-sticky-header) observes the viewport or the window. Runtime changes to
|
|
66
|
+
intersection's `threshold`, lazy-frame's `url` / `rootMargin`, and
|
|
67
|
+
smart-sticky-header's `offset` are followed.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- lazy-frame: focus moving inside a loaded frame no longer re-fetches it, the
|
|
72
|
+
first intersection after a focus-started load is not a re-entry, and an empty
|
|
73
|
+
`url` is never written to `src`.
|
|
74
|
+
- intersection: with `once`, a handler calling `refresh()` from `enter` no longer
|
|
75
|
+
clears the one-shot marker, so a cache restore does not fire `enter` again.
|
|
76
|
+
- reading-progress: hiding a half-read article no longer publishes `1` and
|
|
77
|
+
`complete`.
|
|
78
|
+
|
|
10
79
|
## [0.11.0] - 2026-09-05
|
|
11
80
|
|
|
12
81
|
Minor release with no new components. Eight existing ones are reworked — the three
|
|
@@ -930,6 +999,7 @@ Initial public alpha: 101 behavior-only, accessible Stimulus controllers driven
|
|
|
930
999
|
by `data-*` attributes, shipping no CSS. Published to npm (with provenance) and
|
|
931
1000
|
RubyGems.
|
|
932
1001
|
|
|
1002
|
+
[0.12.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.12.0
|
|
933
1003
|
[0.11.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.11.0
|
|
934
1004
|
[0.10.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.10.0
|
|
935
1005
|
[0.9.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.9.0
|
package/dist/cable/index.js
CHANGED
|
@@ -2,6 +2,28 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
import { createConsumer } from '@rails/actioncable';
|
|
3
3
|
|
|
4
4
|
// src/cable/live_counter_controller.ts
|
|
5
|
+
|
|
6
|
+
// src/utils/authored_integer.ts
|
|
7
|
+
var TOKEN_BODY = /\d[\d.,'’’ _]*\d|\d/;
|
|
8
|
+
var SIGN_OPENER = /[\s([]/;
|
|
9
|
+
function authoredInteger(text) {
|
|
10
|
+
const firstDigit = /\d/.exec(text);
|
|
11
|
+
if (firstDigit === null) return null;
|
|
12
|
+
const at = firstDigit.index;
|
|
13
|
+
const signed = at > 0 && text[at - 1] === "-" && (at === 1 || SIGN_OPENER.test(text[at - 2]));
|
|
14
|
+
const body = TOKEN_BODY.exec(text.slice(at));
|
|
15
|
+
const runs = body[0].split(/\D+/);
|
|
16
|
+
const separators = body[0].match(/\D+/g) ?? [];
|
|
17
|
+
const lead = runs[0];
|
|
18
|
+
const heads = lead.length <= 3 && !lead.startsWith("0");
|
|
19
|
+
let digits = lead;
|
|
20
|
+
for (let i = 1; heads && i < runs.length; i += 1) {
|
|
21
|
+
if (separators[i - 1]?.length !== 1 || runs[i]?.length !== 3) break;
|
|
22
|
+
digits += runs[i];
|
|
23
|
+
}
|
|
24
|
+
const magnitude = Number.parseInt(digits, 10);
|
|
25
|
+
return signed ? -magnitude || 0 : magnitude;
|
|
26
|
+
}
|
|
5
27
|
function parseSubscriptionParams(raw) {
|
|
6
28
|
try {
|
|
7
29
|
const parsed = JSON.parse(raw);
|
|
@@ -229,8 +251,7 @@ var LiveCounterController = class extends Controller {
|
|
|
229
251
|
}
|
|
230
252
|
/** The current count, parsed from the DOM (the single source of truth). */
|
|
231
253
|
get #current() {
|
|
232
|
-
|
|
233
|
-
return Number.isNaN(parsed) ? 0 : parsed;
|
|
254
|
+
return authoredInteger(this.#display.textContent ?? "") ?? 0;
|
|
234
255
|
}
|
|
235
256
|
#write(count) {
|
|
236
257
|
if (count === this.#current) return;
|
package/dist/cable/index.js.map
CHANGED
|
@@ -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/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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/authored_integer.ts","../../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":["/** Digits plus the characters that may separate them inside one number. */\nconst TOKEN_BODY = /\\d[\\d.,'’’ _]*\\d|\\d/;\n/** A `-` opens a token only against one of these on its left. */\nconst SIGN_OPENER = /[\\s([]/;\n\n/**\n * Reads the integer that a human-formatted string displays.\n *\n * Takes the first numeric token, drops group separators, and truncates any\n * fractional part toward zero — so `\"1,200 users\"`, `\"$1,299.99\"` and\n * `\"1.299,99\"` read 1200, 1299 and 1299 without asking the platform which\n * locale is in play. A separator counts as grouping only where the digits that\n * follow it form a run of exactly three and the leading run could head a\n * grouped number; anything else is the decimal point, and the digits after it\n * are the fraction.\n *\n * `-` counts as a sign only where it opens the token — at the start of the\n * string, or after whitespace or an opening bracket — so a hyphen inside a\n * label (`\"Sign-ups: 1,200\"`, `\"Top-10 users\"`) never becomes one.\n *\n * `\"1.200\"` is genuinely ambiguous (1200 grouped, 1.2 with a decimal point) and\n * reads as 1200: display numbers group far more often than they carry a\n * fraction in exactly three digits.\n *\n * Returns `null` when the string holds no digits at all. What that means is the\n * caller's to decide — starting nothing, or falling back to a base value.\n */\nexport function authoredInteger(text: string): number | null {\n const firstDigit = /\\d/.exec(text);\n if (firstDigit === null) return null;\n\n const at = firstDigit.index;\n const signed =\n at > 0 && text[at - 1] === \"-\" && (at === 1 || SIGN_OPENER.test(text[at - 2] as string));\n\n const body = TOKEN_BODY.exec(text.slice(at)) as RegExpExecArray;\n const runs = body[0].split(/\\D+/);\n const separators = body[0].match(/\\D+/g) ?? [];\n\n // The leading run heads a grouped number only when it is short enough to be a\n // group itself and carries no leading zero; \"0.500\" and \"012,3\" are decimals.\n const lead = runs[0] as string;\n const heads = lead.length <= 3 && !lead.startsWith(\"0\");\n let digits = lead;\n for (let i = 1; heads && i < runs.length; i += 1) {\n if (separators[i - 1]?.length !== 1 || runs[i]?.length !== 3) break;\n digits += runs[i];\n }\n\n const magnitude = Number.parseInt(digits, 10);\n return signed ? -magnitude || 0 : magnitude;\n}\n","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 { authoredInteger } from \"../utils/authored_integer\";\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 // A formatted server-rendered value (\"1,200 likes\") reads as the number it\n // displays; a display holding no number at all counts as zero.\n return authoredInteger(this.#display.textContent ?? \"\") ?? 0;\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"]}
|
|
@@ -18,19 +18,21 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
18
18
|
* With `once` (default) later starts are ignored (`data-count-up-done` records
|
|
19
19
|
* a finished run across Turbo cache restores).
|
|
20
20
|
*
|
|
21
|
+
* Only the text node holding the number is animated, so sibling markup — a unit
|
|
22
|
+
* in a `<small>`, a label in a `<b>` — is left where the author put it.
|
|
23
|
+
*
|
|
21
24
|
* `end` dispatches `{ value }`.
|
|
22
25
|
*
|
|
23
26
|
* @remarks
|
|
24
|
-
* Behavior only — no formatting is imposed: the authored text is
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* snapshot taken mid-animation. The animation frame is canceled on
|
|
27
|
+
* Behavior only — no formatting is imposed: the authored text is read for the
|
|
28
|
+
* integer it displays and restored verbatim at the end; intermediate frames
|
|
29
|
+
* render plain integers. Accessibility: when the user prefers reduced motion the
|
|
30
|
+
* animation is skipped entirely (the value just stays final — WCAG 2.3.3).
|
|
31
|
+
* During a run the ticking number is wrapped in a `role="img"` element named
|
|
32
|
+
* with the authored text, which is where a name is allowed to live — the host
|
|
33
|
+
* keeps whatever semantics it had, so a `<dd>` stays a definition. That wrapper
|
|
34
|
+
* doubles as the interrupted-run record `connect()` restores from after a Turbo
|
|
35
|
+
* cache snapshot taken mid-animation. The animation frame is canceled on
|
|
34
36
|
* `disconnect()` (Turbo navigation included) and the authored text restored.
|
|
35
37
|
*/
|
|
36
38
|
declare class CountUpController extends Controller<HTMLElement> {
|