stimeo-ui 0.1.0-beta.1 → 0.1.0-beta.2

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +63 -3
  3. package/dist/cable/index.d.ts +391 -0
  4. package/dist/cable/index.js +563 -0
  5. package/dist/cable/index.js.map +1 -0
  6. package/dist/controllers/combobox_controller.js +15 -0
  7. package/dist/controllers/combobox_controller.js.map +1 -1
  8. package/dist/controllers/confirm_controller.d.ts +5 -0
  9. package/dist/controllers/confirm_controller.js.map +1 -1
  10. package/dist/controllers/count_up_controller.d.ts +64 -0
  11. package/dist/controllers/count_up_controller.js +86 -0
  12. package/dist/controllers/count_up_controller.js.map +1 -0
  13. package/dist/controllers/intersection_controller.d.ts +85 -0
  14. package/dist/controllers/intersection_controller.js +173 -0
  15. package/dist/controllers/intersection_controller.js.map +1 -0
  16. package/dist/controllers/lazy_frame_controller.js +57 -9
  17. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  18. package/dist/controllers/listbox_controller.js +13 -0
  19. package/dist/controllers/listbox_controller.js.map +1 -1
  20. package/dist/controllers/optimistic_controller.d.ts +48 -0
  21. package/dist/controllers/optimistic_controller.js +58 -0
  22. package/dist/controllers/optimistic_controller.js.map +1 -0
  23. package/dist/controllers/pointer_drag_controller.d.ts +94 -0
  24. package/dist/controllers/pointer_drag_controller.js +406 -0
  25. package/dist/controllers/pointer_drag_controller.js.map +1 -0
  26. package/dist/controllers/portal_controller.d.ts +5 -1
  27. package/dist/controllers/portal_controller.js +62 -6
  28. package/dist/controllers/portal_controller.js.map +1 -1
  29. package/dist/controllers/reading_progress_controller.d.ts +42 -0
  30. package/dist/controllers/reading_progress_controller.js +49 -0
  31. package/dist/controllers/reading_progress_controller.js.map +1 -0
  32. package/dist/controllers/roving_controller.js +1 -0
  33. package/dist/controllers/roving_controller.js.map +1 -1
  34. package/dist/controllers/scrollspy_controller.js +65 -19
  35. package/dist/controllers/scrollspy_controller.js.map +1 -1
  36. package/dist/controllers/smart_sticky_header_controller.d.ts +56 -0
  37. package/dist/controllers/smart_sticky_header_controller.js +76 -0
  38. package/dist/controllers/smart_sticky_header_controller.js.map +1 -0
  39. package/dist/controllers/sortable_controller.d.ts +80 -0
  40. package/dist/controllers/sortable_controller.js +167 -0
  41. package/dist/controllers/sortable_controller.js.map +1 -0
  42. package/dist/controllers/sticky_observer_controller.js +60 -15
  43. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  44. package/dist/controllers/tabs_controller.d.ts +5 -2
  45. package/dist/controllers/tabs_controller.js +1 -1
  46. package/dist/controllers/tabs_controller.js.map +1 -1
  47. package/dist/index.d.ts +50 -2
  48. package/dist/index.js +1124 -68
  49. package/dist/index.js.map +1 -1
  50. package/dist/inspector/cli.d.ts +228 -4
  51. package/dist/inspector/cli.js +500 -66
  52. package/dist/inspector/cli.js.map +1 -1
  53. package/dist/inspector/cli_bin.js +1250 -69
  54. package/dist/inspector/cli_bin.js.map +1 -1
  55. package/dist/inspector/examples.json +449 -0
  56. package/dist/inspector/manifest.json +1555 -124
  57. package/package.json +17 -5
package/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ 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.1.0-beta.2] - 2026-07-12
11
+
12
+ The Inspector grows two new faces: an MCP server for AI coding agents and a
13
+ VS Code extension that checks markup as you type.
14
+
15
+ ### Added
16
+
17
+ - **MCP server** (`stimeo-ui mcp`): the Inspector engine now runs as a Model
18
+ Context Protocol server, so AI coding agents (Claude Code, Cursor, …) can
19
+ discover the catalog, fetch a controller's exact contract and verified
20
+ example markup, and check generated HTML/ERB before presenting it. Four
21
+ read-only tools (`stimeo_check`, `stimeo_catalog`, `stimeo_controller`,
22
+ `stimeo_example`), preloadable resources (`stimeo://manifest`,
23
+ `stimeo://examples/<id>`), and two prompts (`stimeo_build_ui`,
24
+ `stimeo_fix_markup`) — hand-written like the rest of the Inspector, adding
25
+ zero runtime dependencies.
26
+ - **VS Code extension — Stimeo UI Inspector**: live diagnostics, quick fixes,
27
+ manifest-driven completions, and contract hovers for `stimeo--*` markup in
28
+ HTML/ERB, published on the
29
+ [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=stimeo-labs.stimeo-ui)
30
+ and [Open VSX](https://open-vsx.org/extension/stimeo-labs/stimeo-ui).
31
+ - Ten new components — the catalog now spans **111 controllers**: count-up,
32
+ intersection, optimistic, pointer-drag, reading-progress,
33
+ smart-sticky-header, sortable, and — backed by Action Cable — live-counter,
34
+ presence, and typing-indicator.
35
+ - pointer-drag: opt-in follow mode that translates the dragged element while
36
+ the pointer moves.
37
+ - Inspector: checks now also cover keyboard-focusability prerequisites,
38
+ controller-managed ARIA that must not be hardcoded, and conditional
39
+ cross-controller composition rules (manifest schema v5). Verified example
40
+ markup for every component ships with the package.
41
+
42
+ ### Fixed
43
+
44
+ - listbox, combobox, and multi-select: the active option now scrolls into
45
+ view while navigating with the keyboard (`aria-activedescendant` lists
46
+ keep the highlighted option visible).
47
+
10
48
  ## [0.1.0-beta.1] - 2026-06-30
11
49
 
12
50
  First beta. The 101 core components meet the accessibility quality bar, so the
@@ -32,5 +70,6 @@ Initial public alpha: 101 behavior-only, accessible Stimulus controllers driven
32
70
  by `data-*` attributes, shipping no CSS. Published to npm (with provenance) and
33
71
  RubyGems.
34
72
 
73
+ [0.1.0-beta.2]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.1.0-beta.2
35
74
  [0.1.0-beta.1]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.1.0-beta.1
36
75
  [0.1.0-alpha.1]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.1.0-alpha.1
package/README.md CHANGED
@@ -31,7 +31,7 @@ owns the look entirely.
31
31
  ### Rails with importmap (recommended)
32
32
 
33
33
  ```bash
34
- bundle add stimeo-ui --version "0.1.0.pre.beta.1"
34
+ bundle add stimeo-ui --version "0.1.0.pre.beta.2"
35
35
  bin/rails generate stimeo:install
36
36
  ```
37
37
 
@@ -114,12 +114,72 @@ The `eslint-plugin-jsx-a11y` equivalents are
114
114
  `no-noninteractive-tabindex`. These components' real accessibility is exercised
115
115
  with axe-core and real screen readers in this project's own test suite.
116
116
 
117
+ ## Inspector CLI & MCP server
118
+
119
+ Stimeo UI bundles a zero-dependency static checker for its own markup contract
120
+ — spelling of controllers/targets/values, required structure, and the ARIA
121
+ attributes you (the author) must supply:
122
+
123
+ ```bash
124
+ npx stimeo-ui check app/views # check your templates (exit 1 on errors)
125
+ npx stimeo-ui catalog # list every controller's public API
126
+ ```
127
+
128
+ Both commands accept `--json` for machine-readable output, so `check` drops
129
+ straight into CI.
130
+
131
+ The same engine runs as a **Model Context Protocol** server, so AI coding
132
+ agents (Claude Code, Cursor, …) can discover the catalog, fetch verified
133
+ reference markup, and validate generated HTML/ERB before presenting it:
134
+
135
+ ```bash
136
+ claude mcp add stimeo -- npx -y stimeo-ui mcp
137
+ ```
138
+
139
+ or in `.mcp.json` (Claude Code) / `.cursor/mcp.json` (Cursor):
140
+
141
+ ```json
142
+ {
143
+ "mcpServers": {
144
+ "stimeo": {
145
+ "command": "npx",
146
+ "args": ["-y", "stimeo-ui", "mcp"]
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ It exposes four read-only tools — `stimeo_check` (validate a source string),
153
+ `stimeo_catalog`, `stimeo_controller` (one controller's full contract,
154
+ accessibility requirements included), and `stimeo_example` (verified example
155
+ markup: the official catalog demo under [`examples/`](examples/), bundled at
156
+ build time and guaranteed to pass the checker) — plus MCP resources
157
+ (`stimeo://manifest`, `stimeo://examples/<id>`) for preloading context without
158
+ a tool round-trip. The server reads only its bundled manifest and example
159
+ index; there are no write-capable tools.
160
+
161
+ The same checks also run **live in your editor**: the **Stimeo UI Inspector**
162
+ extension on the
163
+ [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=stimeo-labs.stimeo-ui)
164
+ and [Open VSX](https://open-vsx.org/extension/stimeo-labs/stimeo-ui) (for Cursor /
165
+ VSCodium / Windsurf) gives as-you-type diagnostics, quick fixes, completions,
166
+ and contract hovers. No setup: the engine and a manifest snapshot are bundled,
167
+ and when your workspace installs `stimeo-ui`, the nearest installed version
168
+ wins — so diagnostics always match what you run.
169
+
117
170
  ## Contributing
118
171
 
119
172
  Bug reports and feature requests are very welcome — please open a GitHub issue.
120
173
  For code changes, open an issue first to discuss direction; see
121
174
  [`CONTRIBUTING.md`](CONTRIBUTING.md).
122
175
 
123
- ## License
176
+ ## License & Pro
177
+
178
+ Free and open source under the [MIT License](LICENSE) © Stimeo Labs. Every
179
+ component in this repository is Core, and the MIT grant is irrevocable.
124
180
 
125
- Released under the [MIT License](LICENSE) © Stimeo Labs.
181
+ **Stimeo UI Pro** advanced behavior components that are the most work to
182
+ build yourself — is planned around 1.0 as a separately licensed commercial
183
+ track, built alongside (never carved out of) Core. For release news and
184
+ early access, join the waitlist at
185
+ [stimeo-labs.com/waitlist](https://stimeo-labs.com/waitlist).
@@ -0,0 +1,391 @@
1
+ import { Controller, Application } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless **live counter** — the third *server-bound* behavior: a
5
+ * number bound to an Action Cable stream (likes, views, active users) that
6
+ * ticks across every connected client. A local action applies an **optimistic**
7
+ * increment immediately, then reconciles with the broadcast; the own echo of a
8
+ * delta broadcast is deduped so the increment is never applied twice. Ships in
9
+ * the opt-in `stimeo-ui/cable` subpath (`@rails/actioncable` optional peer).
10
+ *
11
+ * Markup contract (identifier: `stimeo--live-counter`):
12
+ * <div data-controller="stimeo--live-counter"
13
+ * data-stimeo--live-counter-channel-value="LikesChannel"
14
+ * data-stimeo--live-counter-params-value='{"post":42}'
15
+ * data-stimeo--live-counter-id-value="<%= SecureRandom.uuid %>">
16
+ * <span data-stimeo--live-counter-target="value">128</span>
17
+ * <button type="button" data-action="stimeo--live-counter#increment"
18
+ * data-stimeo--live-counter-target="trigger">♥</button>
19
+ * </div>
20
+ *
21
+ * Wire contract — the broadcast is either **authoritative** or a **delta**:
22
+ * `{ count: 129 }` sets the absolute value (naturally idempotent; preferred —
23
+ * the server owns the number), while `{ delta: 1, by: "17" }` adds to it,
24
+ * skipped when `by` matches this client's `id` (the optimistic increment
25
+ * already applied it). `increment` performs `increment` on the channel with
26
+ * `{ id, delta }`; the server persists and broadcasts. Without an own `id`
27
+ * the delta echo is indistinguishable from a foreign delta, so the optimistic
28
+ * bump is skipped and the broadcast applies the increment exactly once — set
29
+ * `id` (any per-client string, e.g. `SecureRandom.uuid`) for optimistic UX.
30
+ *
31
+ * @remarks
32
+ * Behavior only — the displayed number IS the state, and the **DOM is the
33
+ * source of truth**: the server renders the initial count into the `value`
34
+ * target (the element itself without one), so a Turbo cache restore needs no
35
+ * reconciliation and `connect()` only (re)subscribes. Sending tracks the full
36
+ * subscription lifecycle (via the shared confirmation-aware subscription):
37
+ * increments are dropped before confirmation AND while the connection is down
38
+ * (`disconnected` shuts the gate until Action Cable re-confirms), so the
39
+ * display never advances past what the server can receive. A refused
40
+ * subscription publishes the `data-live-counter-rejected` hook (cleared on
41
+ * `connect()` — rejection is transient server state) so the consumer's CSS can
42
+ * disable the trigger. Optional `trigger` targets make that declarative: they
43
+ * carry the real `disabled` attribute exactly while an increment would be
44
+ * dropped (before confirmation, during an outage, after a rejection) — no CSS
45
+ * required, and a disabled form control is announced as such by AT. An
46
+ * authored `disabled` (set by the consumer for its own reasons) is respected:
47
+ * only a disabled this controller applied — tracked via a marker attribute —
48
+ * is ever lifted. Channel-less (local-only) counters never disable their
49
+ * triggers. There are no timers; the subscription is released on
50
+ * `disconnect()` (Turbo navigation included).
51
+ */
52
+ declare class LiveCounterController extends Controller<HTMLElement> {
53
+ #private;
54
+ static targets: string[];
55
+ static values: {
56
+ channel: {
57
+ type: StringConstructor;
58
+ default: string;
59
+ };
60
+ params: {
61
+ type: ObjectConstructor;
62
+ default: {};
63
+ };
64
+ id: {
65
+ type: StringConstructor;
66
+ default: string;
67
+ };
68
+ };
69
+ static actions: readonly ["increment"];
70
+ static events: readonly ["change"];
71
+ readonly hasValueTarget: boolean;
72
+ readonly valueTarget: HTMLElement;
73
+ readonly triggerTargets: HTMLElement[];
74
+ channelValue: string;
75
+ paramsValue: Record<string, unknown>;
76
+ idValue: string;
77
+ connect(): void;
78
+ disconnect(): void;
79
+ /** Late-added triggers (e.g. via a Turbo Stream) pick up the current gate. */
80
+ triggerTargetConnected(target: HTMLElement): void;
81
+ /**
82
+ * Optimistic local increment: bumps the display immediately, then asks the
83
+ * server to persist and broadcast. The step comes from the action param
84
+ * (`data-stimeo--live-counter-delta-param`), default 1. Bound via `data-action`.
85
+ */
86
+ increment(event?: Event & {
87
+ params?: {
88
+ delta?: number;
89
+ };
90
+ }): void;
91
+ }
92
+
93
+ /**
94
+ * Headless **presence** — the second *server-bound* behavior: online
95
+ * dots / a "who's viewing this" stack, bound to an Action Cable channel. Like
96
+ * `stimeo--typing-indicator`, the server stays a trivial rebroadcast channel
97
+ * with **zero presence state**: every client heartbeats an `appear` beacon
98
+ * (`{ id, name }`), and each client expires peers it has not heard from for
99
+ * `timeout` ms. Leaving (`{ id, leaving: true }`) is broadcast best-effort on
100
+ * `disconnect()` and on `pagehide` (tab close / hard navigation, where
101
+ * `disconnect()` never runs); a lost notice is caught by the expiry. Ships in
102
+ * the opt-in `stimeo-ui/cable` subpath (`@rails/actioncable` optional peer).
103
+ *
104
+ * Roster convergence: a late joiner would otherwise see peers only as their
105
+ * next heartbeats arrive, so on hearing a beacon from an *unknown* peer, each
106
+ * client re-announces itself (throttled to {@link BEACON_THROTTLE_MS}) — the
107
+ * roster converges in one round-trip instead of one heartbeat period.
108
+ *
109
+ * Markup contract (identifier: `stimeo--presence`):
110
+ * <div data-controller="stimeo--presence"
111
+ * data-stimeo--presence-channel-value="PresenceChannel"
112
+ * data-stimeo--presence-params-value='{"room":"doc_7"}'
113
+ * data-stimeo--presence-id-value="17" data-stimeo--presence-name-value="Alice">
114
+ * <span data-stimeo--presence-target="count" data-other="%{count} viewing"></span>
115
+ * <ul data-stimeo--presence-target="list"></ul>
116
+ * <template data-stimeo--presence-target="template">
117
+ * <li><span data-presence-name></span></li>
118
+ * </template>
119
+ * </div>
120
+ *
121
+ * Server contract (a trivial rebroadcast channel):
122
+ * class PresenceChannel < ApplicationCable::Channel
123
+ * def subscribed = stream_from "presence:#{params[:room]}"
124
+ * def appear(data) = ActionCable.server.broadcast("presence:#{params[:room]}",
125
+ * { id: data["id"], name: data["name"], leaving: data["leaving"] })
126
+ * end
127
+ *
128
+ * @remarks
129
+ * Behavior only — the dot/stack look is the consumer's CSS, keyed off the
130
+ * `data-present` / `data-present-count` hooks. The roster counts and renders
131
+ * **other** clients (`id` ≠ own `id`); rendering is optional: given a `list` +
132
+ * `template` pair, one clone per peer is appended (elements marked
133
+ * `data-presence-name` receive the name; the clone root is tagged
134
+ * `data-presence-id`), and the `count` target renders through localizable
135
+ * `data-zero` / `data-one` / `data-other` templates (`%{count}`). Richer
136
+ * per-user rendering (avatars, links) belongs to the consumer via the `join` /
137
+ * `leave` / `change` events or a server-rendered Turbo Stream. The `id`
138
+ * comparison is display-level echo suppression, not authentication — identity
139
+ * belongs to the server. Sending tracks the full subscription lifecycle (via
140
+ * the shared confirmation-aware subscription): beacons — heartbeats, the
141
+ * convergence answer, the leaving notice — are dropped before confirmation and
142
+ * while the connection is down, where Action Cable would discard them anyway
143
+ * (and a discarded beacon must not burn the convergence throttle); `connected`
144
+ * re-fires on every reconnect and force-beacons, so the roster self-heals. A
145
+ * refused subscription publishes the `data-presence-rejected` hook (cleared on
146
+ * `connect()` — rejection is transient server state), mirroring
147
+ * `data-live-counter-rejected`. Presence is transient: `connect()` clears
148
+ * whatever a Turbo cache snapshot preserved (hooks + rendered clones), renders
149
+ * the known-empty count (the `data-present*` hooks stay absent until the first
150
+ * beacon), and the stream re-populates; the subscription, heartbeat interval,
151
+ * per-peer expiry timers, and the `pagehide` listener are all released on
152
+ * `disconnect()` (Turbo navigation included).
153
+ */
154
+ declare class PresenceController extends Controller<HTMLElement> {
155
+ #private;
156
+ static targets: string[];
157
+ static values: {
158
+ channel: {
159
+ type: StringConstructor;
160
+ default: string;
161
+ };
162
+ params: {
163
+ type: ObjectConstructor;
164
+ default: {};
165
+ };
166
+ id: {
167
+ type: StringConstructor;
168
+ default: string;
169
+ };
170
+ name: {
171
+ type: StringConstructor;
172
+ default: string;
173
+ };
174
+ heartbeat: {
175
+ type: NumberConstructor;
176
+ default: number;
177
+ };
178
+ timeout: {
179
+ type: NumberConstructor;
180
+ default: number;
181
+ };
182
+ };
183
+ static events: readonly ["join", "leave", "change"];
184
+ readonly hasCountTarget: boolean;
185
+ readonly countTarget: HTMLElement;
186
+ readonly hasListTarget: boolean;
187
+ readonly listTarget: HTMLElement;
188
+ readonly hasTemplateTarget: boolean;
189
+ readonly templateTarget: HTMLTemplateElement;
190
+ channelValue: string;
191
+ paramsValue: Record<string, unknown>;
192
+ idValue: string;
193
+ nameValue: string;
194
+ heartbeatValue: number;
195
+ timeoutValue: number;
196
+ connect(): void;
197
+ disconnect(): void;
198
+ }
199
+
200
+ /**
201
+ * Headless **typing indicator** — the first *server-bound* behavior: a
202
+ * controller whose state lives on the server stream, not in client
203
+ * memory. Typing in the composer throttle-broadcasts a `typing` signal over an
204
+ * Action Cable channel; received signals from *other* clients render "X is
205
+ * typing…" into a `status` live region and auto-clear after `timeout` ms of
206
+ * silence. The whole behavior is HTML + a broadcast — no app JS, no client
207
+ * store, no reconciler. Ships in the opt-in `stimeo-ui/cable` subpath
208
+ * (`@rails/actioncable` optional peer); the core stays zero-dep.
209
+ *
210
+ * Markup contract (identifier: `stimeo--typing-indicator`):
211
+ * <div data-controller="stimeo--typing-indicator"
212
+ * data-stimeo--typing-indicator-channel-value="TypingChannel"
213
+ * data-stimeo--typing-indicator-params-value='{"room":"chat_42"}'
214
+ * data-stimeo--typing-indicator-name-value="Alice">
215
+ * <textarea data-stimeo--typing-indicator-target="input"></textarea>
216
+ * <p role="status" data-stimeo--typing-indicator-target="status"
217
+ * data-one="%{name} is typing…" data-many="%{names} are typing…"></p>
218
+ * </div>
219
+ *
220
+ * Server contract (a trivial rebroadcast channel):
221
+ * class TypingChannel < ApplicationCable::Channel
222
+ * def subscribed = stream_from "typing:#{params[:room]}"
223
+ * def typing(data) = ActionCable.server.broadcast("typing:#{params[:room]}",
224
+ * { name: data["name"] })
225
+ * end
226
+ *
227
+ * @remarks
228
+ * Behavior only — the indicator's look is the consumer's CSS, keyed off the
229
+ * `data-typing` hook; the copy is localizable through the `data-one` /
230
+ * `data-many` templates (`%{name}` / `%{names}` / `%{count}`, terse English
231
+ * fallback — the same channel design as `stimeo--sortable`). The own echo is
232
+ * dropped by comparing the broadcast `name` against `name` (a same-name guard,
233
+ * not authentication — identity belongs to the server). Sending tracks the full
234
+ * subscription lifecycle (via the shared confirmation-aware subscription):
235
+ * signals are dropped before confirmation and while the connection is down, so
236
+ * an outage never burns the throttle window on undeliverable sends. A refused
237
+ * subscription publishes the `data-typing-indicator-rejected` hook (cleared on
238
+ * `connect()` — rejection is transient server state), mirroring
239
+ * `data-live-counter-rejected`. Typing state is transient by nature:
240
+ * `connect()` resets the hook and the live region (a Turbo cache snapshot must
241
+ * not resurrect a stale "X is typing…"), and re-population happens naturally
242
+ * from the stream. The subscription, the per-typer timers, and the delegated
243
+ * `input` listener are all released on `disconnect()` (Turbo navigation
244
+ * included).
245
+ */
246
+ declare class TypingIndicatorController extends Controller<HTMLElement> {
247
+ #private;
248
+ static targets: string[];
249
+ static values: {
250
+ channel: {
251
+ type: StringConstructor;
252
+ default: string;
253
+ };
254
+ params: {
255
+ type: ObjectConstructor;
256
+ default: {};
257
+ };
258
+ name: {
259
+ type: StringConstructor;
260
+ default: string;
261
+ };
262
+ timeout: {
263
+ type: NumberConstructor;
264
+ default: number;
265
+ };
266
+ throttle: {
267
+ type: NumberConstructor;
268
+ default: number;
269
+ };
270
+ };
271
+ static events: readonly ["change"];
272
+ readonly hasStatusTarget: boolean;
273
+ readonly statusTarget: HTMLElement;
274
+ channelValue: string;
275
+ paramsValue: Record<string, unknown>;
276
+ nameValue: string;
277
+ timeoutValue: number;
278
+ throttleValue: number;
279
+ connect(): void;
280
+ disconnect(): void;
281
+ }
282
+
283
+ /**
284
+ * Minimal structural view of an Action Cable subscription — the two members the
285
+ * server-bound controllers use. Keeping our own narrow interface (instead of
286
+ * re-exporting the full `@rails/actioncable` types) lets consumers hand us any
287
+ * structurally compatible object, including test doubles.
288
+ */
289
+ interface CableSubscription {
290
+ /** Invokes a channel action on the server (`ChannelName#action`). */
291
+ perform(action: string, data?: Record<string, unknown>): void;
292
+ /** Cancels the subscription (the channel's `unsubscribed` runs server-side). */
293
+ unsubscribe(): void;
294
+ }
295
+ /** The mixin a controller passes to {@link CableConsumer.subscriptions}' create. */
296
+ interface CableSubscriptionMixin {
297
+ /** Called once the subscription is confirmed (`perform` is deliverable). */
298
+ connected?(): void;
299
+ /**
300
+ * Called when the connection drops — perform() is silently undeliverable
301
+ * until Action Cable reconnects and re-confirms (`connected` fires again).
302
+ */
303
+ disconnected?(): void;
304
+ /** Called when the server refuses the subscription (it will never confirm). */
305
+ rejected?(): void;
306
+ /** Called with each broadcast the channel transmits to this client. */
307
+ received?(data: unknown): void;
308
+ }
309
+ /** Minimal structural view of an Action Cable consumer (the websocket owner). */
310
+ interface CableConsumer {
311
+ subscriptions: {
312
+ create(channel: string | Record<string, unknown>, mixin: CableSubscriptionMixin): CableSubscription;
313
+ };
314
+ }
315
+ /**
316
+ * Replaces (or clears, with `null`) the shared Action Cable consumer.
317
+ *
318
+ * Call this once at boot when the app already owns a consumer (the usual
319
+ * `app/javascript/channels/consumer.js`), so the server-bound controllers reuse
320
+ * its websocket instead of opening a second one. Tests use it to inject a
321
+ * double. With `null`, the next {@link getCableConsumer} lazily re-creates one.
322
+ */
323
+ declare function setCableConsumer(consumer: CableConsumer | null): void;
324
+ /**
325
+ * The shared Action Cable consumer, lazily created on first use via
326
+ * `createConsumer()` (which reads the standard `action_cable_meta_tag` URL).
327
+ */
328
+ declare function getCableConsumer(): CableConsumer;
329
+ /**
330
+ * A {@link CableSubscription} that also tracks its confirmation lifecycle.
331
+ * Action Cable silently drops a `perform()` sent before the subscription is
332
+ * confirmed or while the connection is down — every server-bound controller
333
+ * must therefore gate its sends (and their local side effects: optimistic
334
+ * updates, throttle bookkeeping) on {@link confirmed}.
335
+ */
336
+ interface ConfirmedCableSubscription extends CableSubscription {
337
+ /** True while the subscription is confirmed — `perform()` is deliverable. */
338
+ readonly confirmed: boolean;
339
+ /** True once the server refused the subscription (it will never confirm). */
340
+ readonly rejected: boolean;
341
+ }
342
+ /**
343
+ * Creates a subscription on the shared consumer with confirmation tracking
344
+ * layered over the caller's mixin: `connected` / `disconnected` / `rejected`
345
+ * flip the {@link ConfirmedCableSubscription.confirmed} flag *before* the
346
+ * caller's own handler runs (so a handler reading `subscription.confirmed`
347
+ * sees the post-transition state), and `received` passes straight through.
348
+ *
349
+ * This is deliberately a *tracker*, not an automatic `perform()` gate: the
350
+ * controllers must skip the local side effects that surround a send (an
351
+ * optimistic DOM bump, a throttle timestamp) together with the send itself,
352
+ * which only the call site can decide — so they check `confirmed` and bail
353
+ * before any of it.
354
+ */
355
+ declare function createConfirmedSubscription(channel: string | Record<string, unknown>, mixin: CableSubscriptionMixin): ConfirmedCableSubscription;
356
+
357
+ /**
358
+ * Opt-in **server-bound** behaviors for Stimeo — controllers whose state lives
359
+ * on an Action Cable / Turbo stream rather than in client memory: presence,
360
+ * typing indicators, live counters.
361
+ *
362
+ * **Why this is a separate entry point.** The core library is zero-runtime-dep:
363
+ * `import "stimeo-ui"` pulls in nothing but `@hotwired/stimulus`. Binding the
364
+ * DOM to a server stream genuinely needs `@rails/actioncable`, so — exactly
365
+ * like `stimeo-ui/positioning` and `@floating-ui/dom` — that cost is opt-in:
366
+ * this module lives at `stimeo-ui/cable`, `@rails/actioncable` is an *optional*
367
+ * peer, and nothing in the core imports it.
368
+ *
369
+ * When the app already owns a consumer (`app/javascript/channels/consumer.js`),
370
+ * hand it over once at boot so no second websocket is opened:
371
+ *
372
+ * ```ts
373
+ * import consumer from "./channels/consumer";
374
+ * import { registerCable, setCableConsumer } from "stimeo-ui/cable";
375
+ *
376
+ * setCableConsumer(consumer);
377
+ * registerCable(application);
378
+ * ```
379
+ */
380
+ declare const cableControllers: {
381
+ readonly "stimeo--live-counter": typeof LiveCounterController;
382
+ readonly "stimeo--presence": typeof PresenceController;
383
+ readonly "stimeo--typing-indicator": typeof TypingIndicatorController;
384
+ };
385
+ /**
386
+ * Registers the opt-in server-bound controllers on a Stimulus Application
387
+ * (additive to `registerStimeo`, mirroring `registerPositioning`).
388
+ */
389
+ declare function registerCable(application: Application): void;
390
+
391
+ export { type CableConsumer, type CableSubscription, type CableSubscriptionMixin, type ConfirmedCableSubscription, LiveCounterController, PresenceController, TypingIndicatorController, cableControllers, createConfirmedSubscription, getCableConsumer, registerCable, setCableConsumer };