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.
- package/CHANGELOG.md +39 -0
- package/README.md +63 -3
- package/dist/cable/index.d.ts +391 -0
- package/dist/cable/index.js +563 -0
- package/dist/cable/index.js.map +1 -0
- package/dist/controllers/combobox_controller.js +15 -0
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +5 -0
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +64 -0
- package/dist/controllers/count_up_controller.js +86 -0
- package/dist/controllers/count_up_controller.js.map +1 -0
- package/dist/controllers/intersection_controller.d.ts +85 -0
- package/dist/controllers/intersection_controller.js +173 -0
- package/dist/controllers/intersection_controller.js.map +1 -0
- package/dist/controllers/lazy_frame_controller.js +57 -9
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js +13 -0
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +48 -0
- package/dist/controllers/optimistic_controller.js +58 -0
- package/dist/controllers/optimistic_controller.js.map +1 -0
- package/dist/controllers/pointer_drag_controller.d.ts +94 -0
- package/dist/controllers/pointer_drag_controller.js +406 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -0
- package/dist/controllers/portal_controller.d.ts +5 -1
- package/dist/controllers/portal_controller.js +62 -6
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +42 -0
- package/dist/controllers/reading_progress_controller.js +49 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -0
- package/dist/controllers/roving_controller.js +1 -0
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js +65 -19
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +56 -0
- package/dist/controllers/smart_sticky_header_controller.js +76 -0
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -0
- package/dist/controllers/sortable_controller.d.ts +80 -0
- package/dist/controllers/sortable_controller.js +167 -0
- package/dist/controllers/sortable_controller.js.map +1 -0
- package/dist/controllers/sticky_observer_controller.js +60 -15
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +5 -2
- package/dist/controllers/tabs_controller.js +1 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/index.d.ts +50 -2
- package/dist/index.js +1124 -68
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +228 -4
- package/dist/inspector/cli.js +500 -66
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +1250 -69
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +449 -0
- package/dist/inspector/manifest.json +1555 -124
- 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.
|
|
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
|
-
|
|
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 };
|