@wcstack/idle 1.16.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/README.ja.md ADDED
@@ -0,0 +1,115 @@
1
+ # @wcstack/idle
2
+
3
+ `@wcstack/idle` は wcstack エコシステム向けのヘッドレスな Idle Detection コンポーネントです。
4
+
5
+ 視覚的な UI ウィジェットではありません。`IdleDetector`のlive なユーザー/画面状態をリアクティブな state に変える**非同期プリミティブノード**で、明示的でgesture駆動な権限コマンドの後段に位置します。
6
+
7
+ `@wcstack/state` と組み合わせると、`<wcs-idle>` はパス契約で直接バインドできます:
8
+
9
+ - **入力サーフェス**: `threshold`(ms、最小60000)
10
+ - **出力 state サーフェス**: `userState`、`screenState`、`active`、`error`
11
+
12
+ ## なぜ存在するか — gesture-gated permission パターンの参照実装
13
+
14
+ `IdleDetector.requestPermission()`は**静的メソッド**で、実際のuser gesture内から呼ぶ必要があります。`connectedCallback`はこの文脈の外なので、**本ノードはconnect時に自動startしません** — 呼び出し元が`requestPermission()` → `start()`を明示的に、典型的にはクリックハンドラから駆動します。
15
+
16
+ > **`@wcstack/permission`との合成を推奨。** `navigator.permissions.query({name:"idle-detection"})`が既に存在するため、`<wcs-idle>`は`<wcs-permission name="idle-detection">`と併置して`granted`/`denied`/`prompt`状態を得てください。`<wcs-idle>`自身は実際のアイドル状態と一回限りの`requestPermission()`アクションだけを公開し、4値permission状態は重複実装しません。
17
+
18
+ > **Chromium限定。** Firefox と Safari は`IdleDetector`を実装していません。
19
+
20
+ ## インストール
21
+
22
+ ```bash
23
+ npm install @wcstack/idle
24
+ ```
25
+
26
+ ## クイックスタート
27
+
28
+ ```html
29
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
30
+ <script type="module" src="https://esm.run/@wcstack/permission/auto"></script>
31
+ <script type="module" src="https://esm.run/@wcstack/idle/auto"></script>
32
+
33
+ <wcs-state>
34
+ <script type="module">
35
+ export default {
36
+ // start()前は在席とみなす: `wcs-idle:change`はstart()前は発火しないため、
37
+ // ここを`false`にすると実際には在席なのに初回ロード時点で常に「離席中」が
38
+ // 表示されてしまう(在席が単に未検知なだけ)。
39
+ presenceActive: true,
40
+ idleGranted: false,
41
+ async enableIdleDetection() {
42
+ const el = document.querySelector("wcs-idle");
43
+ const result = await el.requestPermission();
44
+ if (result === "granted") await el.start();
45
+ },
46
+ };
47
+ </script>
48
+ </wcs-state>
49
+
50
+ <wcs-permission name="idle-detection" data-wcs="granted: idleGranted"></wcs-permission>
51
+ <wcs-idle threshold="60000" data-wcs="active: presenceActive"></wcs-idle>
52
+
53
+ <!-- 注意: ここに `disabled: idleGranted` をバインドしてはいけません——permission の
54
+ 許可はページロードを跨いで永続化されるため、再訪問時にボタンが最初から無効になり
55
+ start()(このクリックが唯一の到達経路)が二度と実行できなくなります。許可済みでの
56
+ 再クリックは無害です: requestPermission() は即座に "granted" を返し start() に進みます。 -->
57
+ <button data-wcs="onclick: enableIdleDetection">離席検知を有効にする</button>
58
+ <p>許可状態: <span data-wcs="textContent: idleGranted"></span></p>
59
+ <template data-wcs="if: presenceActive|not">
60
+ <span class="badge">離席中</span>
61
+ </template>
62
+ ```
63
+
64
+ ## 観測可能プロパティ(出力)
65
+
66
+ | プロパティ | イベント | 説明 |
67
+ | ------------- | ------------------ | ---- |
68
+ | `userState` | `wcs-idle:change` | `"active"` \| `"idle"`、`start()`前は`null`。 |
69
+ | `screenState` | `wcs-idle:change` | `"locked"` \| `"unlocked"`、`start()`前は`null`。 |
70
+ | `active` | `wcs-idle:change` | `userState === "active"`のとき`true`。 |
71
+ | `error` | `wcs-idle:error` | 直近の`requestPermission()`/`start()`の失敗、無ければ`null`。 |
72
+
73
+ ## コマンド
74
+
75
+ | コマンド | 非同期 | 説明 |
76
+ | -------------------- | ------ | ---- |
77
+ | `requestPermission` | はい | 静的でgesture-gatedな`IdleDetector.requestPermission()`をラップ。**実際のuser gestureハンドラ内から呼ぶ必要があります。** never-throw: rejectは`"denied"`に倒れます。 |
78
+ | `start` | はい | アイドル検知セッションを開始(`threshold`はms、最小60000)。後続の`start()`/`stop()`で上書きされます。 |
79
+ | `stop` | いいえ | 現在のセッションを停止。未開始でも安全に呼べます。 |
80
+
81
+ ## 属性 / 入力
82
+
83
+ | 属性 | 型 | 既定値 | 説明 |
84
+ | ----------- | ------ | ------- | ---- |
85
+ | `threshold` | number | `60000` | `userState`が`"idle"`になるまでの最小アイドル時間(ms)。バリデーションなし——範囲外の値はブラウザ自身のrejectに委ねます。 |
86
+
87
+ ## 注意・制限
88
+
89
+ - **connect時に自動startしません。** 上記「なぜ存在するか」を参照。
90
+ - **permission状態を重複実装しません。** `<wcs-permission name="idle-detection">`と合成してください。
91
+ - **Chromium限定、かつ secure context 限定です。** Firefox と Safari は`IdleDetector`自体を実装していません。Chromiumであっても`IdleDetector`は`[SecureContext]`専用インターフェースのため、平文の`http://`(`localhost`を除く)では`window.IdleDetector`自体が`undefined`になり——非対応ブラウザと同じ`unsupported`(`error`経由)の分岐に落ちます。
92
+ - **Permissions-Policyでゲートされます。** アイドル検知は`idle-detection`のPermissions-Policyディレクティブ(既定allowlist: `self`)に支配されます。クロスオリジンの`<iframe>`内で`<wcs-idle>`を使うには、その`<iframe>`要素に`allow="idle-detection"`が必要です——無いと`requestPermission()`/`start()`は非対応ブラウザと同様に失敗します。
93
+ - **`stop()`/切断では`userState`/`screenState`/`active`をリセットしません。** 次に`start()`が成功するまで直近の観測値を保持します——Generic Sensor族(`<wcs-gyroscope>`等)と同じ「直近の読み取り値を保持する」挙動です。
94
+
95
+ ## ヘッドレス利用(`IdleCore`)
96
+
97
+ ```typescript
98
+ import { IdleCore } from "@wcstack/idle";
99
+
100
+ const core = new IdleCore();
101
+ core.addEventListener("wcs-idle:change", (e) => {
102
+ console.log((e as CustomEvent).detail); // { userState, screenState }
103
+ });
104
+
105
+ // 実際のuser gestureハンドラ内から:
106
+ const result = await core.requestPermission();
107
+ if (result === "granted") await core.start(60000);
108
+
109
+ // 後始末:
110
+ core.dispose();
111
+ ```
112
+
113
+ ## ライセンス
114
+
115
+ MIT
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @wcstack/idle
2
+
3
+ `@wcstack/idle` is a headless Idle Detection component for the wcstack ecosystem.
4
+
5
+ It is not a visual UI widget.
6
+ It is an **async primitive node** that turns `IdleDetector`'s live user/screen state into reactive state, gated behind an explicit, gesture-driven permission command.
7
+
8
+ With `@wcstack/state`, `<wcs-idle>` can be bound directly through path contracts:
9
+
10
+ - **input surface**: `threshold` (ms, minimum 60000)
11
+ - **output state surface**: `userState`, `screenState`, `active`, `error`
12
+
13
+ ## Why this exists — the reference implementation for gesture-gated permission
14
+
15
+ `IdleDetector.requestPermission()` is a **static method** that must be called from within a real user gesture. `connectedCallback` runs outside that context, so **this node never auto-starts on connect** — the caller drives `requestPermission()` → `start()` explicitly, typically from a click handler.
16
+
17
+ > **Compose with `@wcstack/permission`.** `navigator.permissions.query({name:"idle-detection"})` already exists — pair `<wcs-idle>` with `<wcs-permission name="idle-detection">` for `granted`/`denied`/`prompt` status. `<wcs-idle>` itself only exposes the actual idle state plus the one-time `requestPermission()` action; it does not duplicate the 4-value permission state.
18
+
19
+ > **Chromium-only.** Firefox and Safari do not implement `IdleDetector`.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install @wcstack/idle
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```html
30
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
31
+ <script type="module" src="https://esm.run/@wcstack/permission/auto"></script>
32
+ <script type="module" src="https://esm.run/@wcstack/idle/auto"></script>
33
+
34
+ <wcs-state>
35
+ <script type="module">
36
+ export default {
37
+ // Assume present until told otherwise: `wcs-idle:change` never fires
38
+ // before start(), so a `false` default here would show "Away" on
39
+ // every initial load even though presence is simply unknown yet.
40
+ presenceActive: true,
41
+ idleGranted: false,
42
+ async enableIdleDetection() {
43
+ const el = document.querySelector("wcs-idle");
44
+ const result = await el.requestPermission();
45
+ if (result === "granted") await el.start();
46
+ },
47
+ };
48
+ </script>
49
+ </wcs-state>
50
+
51
+ <wcs-permission name="idle-detection" data-wcs="granted: idleGranted"></wcs-permission>
52
+ <wcs-idle threshold="60000" data-wcs="active: presenceActive"></wcs-idle>
53
+
54
+ <!-- Note: don't bind `disabled: idleGranted` here — the grant persists across
55
+ page loads, so on a revisit the button would start out disabled and start()
56
+ (only reachable through this click) could never run. Re-clicking when
57
+ already granted is harmless: requestPermission() resolves "granted"
58
+ immediately and start() proceeds. -->
59
+ <button data-wcs="onclick: enableIdleDetection">Enable presence detection</button>
60
+ <p>Permission granted: <span data-wcs="textContent: idleGranted"></span></p>
61
+ <template data-wcs="if: presenceActive|not">
62
+ <span class="badge">Away</span>
63
+ </template>
64
+ ```
65
+
66
+ ## Observable Properties (outputs)
67
+
68
+ | Property | Event | Description |
69
+ | ------------- | ----------------- | ------------ |
70
+ | `userState` | `wcs-idle:change` | `"active"` \| `"idle"`, or `null` before `start()`. |
71
+ | `screenState` | `wcs-idle:change` | `"locked"` \| `"unlocked"`, or `null` before `start()`. |
72
+ | `active` | `wcs-idle:change` | `true` when `userState === "active"`. |
73
+ | `error` | `wcs-idle:error` | The last `requestPermission()`/`start()` failure, or `null`. |
74
+
75
+ ## Commands
76
+
77
+ | Command | Async | Description |
78
+ | ------------------- | ----- | ------------ |
79
+ | `requestPermission` | yes | Wraps the static, gesture-gated `IdleDetector.requestPermission()`. **Must be called from within a real user gesture handler.** Never-throw: a rejection resolves to `"denied"`. |
80
+ | `start` | yes | Begin an idle-detection session (`threshold` in ms, minimum 60000). Superseded by a later `start()`/`stop()`. |
81
+ | `stop` | no | Stop the current session. Safe to call when not started. |
82
+
83
+ ## Attributes / Inputs
84
+
85
+ | Attribute | Type | Default | Description |
86
+ | ----------- | ------ | ------- | ------------ |
87
+ | `threshold` | number | `60000` | Minimum idle time (ms) before `userState` becomes `"idle"`. Not validated — an out-of-range value is left to the browser's own rejection. |
88
+
89
+ ## Notes & limitations
90
+
91
+ - **Does not auto-start on connect.** See "Why this exists" above.
92
+ - **Does not duplicate permission state.** Compose with `<wcs-permission name="idle-detection">`.
93
+ - **Chromium-only, and secure-context-only.** Firefox and Safari do not implement `IdleDetector` at all. Even in Chromium, `IdleDetector` is a `[SecureContext]`-only interface, so over plain `http://` (other than `localhost`) `window.IdleDetector` itself is `undefined` — same as an unsupported browser, this falls into the `unsupported` (via `error`) path.
94
+ - **Permissions-Policy gate.** Idle detection is governed by the `idle-detection` Permissions-Policy directive (default allowlist: `self`). Using `<wcs-idle>` inside a cross-origin `<iframe>` requires `allow="idle-detection"` on that `<iframe>` element — otherwise `requestPermission()`/`start()` fail the same way as an unsupported browser.
95
+ - **`stop()`/disconnect does not reset `userState`/`screenState`/`active`.** They keep their last observed value until the next successful `start()` — the same "retain the last reading" behavior as the Generic Sensor family (`<wcs-gyroscope>` et al.).
96
+
97
+ ## Headless usage (`IdleCore`)
98
+
99
+ ```typescript
100
+ import { IdleCore } from "@wcstack/idle";
101
+
102
+ const core = new IdleCore();
103
+ core.addEventListener("wcs-idle:change", (e) => {
104
+ console.log((e as CustomEvent).detail); // { userState, screenState }
105
+ });
106
+
107
+ // from within a real user gesture handler:
108
+ const result = await core.requestPermission();
109
+ if (result === "granted") await core.start(60000);
110
+
111
+ // later:
112
+ core.dispose();
113
+ ```
114
+
115
+ ## License
116
+
117
+ MIT
package/dist/auto.js ADDED
@@ -0,0 +1,3 @@
1
+ import { bootstrapIdle } from "./index.esm.js";
2
+
3
+ bootstrapIdle();
@@ -0,0 +1 @@
1
+ import{bootstrapIdle}from"./index.esm.min.js";bootstrapIdle();
@@ -0,0 +1,158 @@
1
+ interface IWcBindableProperty {
2
+ readonly name: string;
3
+ readonly event: string;
4
+ readonly getter?: (event: Event) => any;
5
+ }
6
+ interface IWcBindableInput {
7
+ readonly name: string;
8
+ readonly attribute?: string;
9
+ }
10
+ interface IWcBindableCommand {
11
+ readonly name: string;
12
+ readonly async?: boolean;
13
+ }
14
+ interface IWcBindable {
15
+ readonly protocol: "wc-bindable";
16
+ readonly version: 1;
17
+ readonly properties: readonly IWcBindableProperty[];
18
+ readonly inputs?: readonly IWcBindableInput[];
19
+ readonly commands?: readonly IWcBindableCommand[];
20
+ }
21
+
22
+ interface ITagNames {
23
+ readonly idle: string;
24
+ }
25
+ interface IWritableTagNames {
26
+ idle?: string;
27
+ }
28
+ interface IConfig {
29
+ readonly tagNames: ITagNames;
30
+ }
31
+ interface IWritableConfig {
32
+ tagNames?: IWritableTagNames;
33
+ }
34
+
35
+ type IdleUserState = "active" | "idle";
36
+ type IdleScreenState = "locked" | "unlocked";
37
+ /**
38
+ * Value types for IdleCore (headless) — the observable state properties.
39
+ * Permission state (granted/denied/prompt) is intentionally NOT included here
40
+ * — compose with `<wcs-permission name="idle-detection">` instead
41
+ * (docs/idle-detection-tag-design.md §0/§2).
42
+ */
43
+ interface WcsIdleCoreValues {
44
+ userState: IdleUserState | null;
45
+ screenState: IdleScreenState | null;
46
+ active: boolean;
47
+ error: any;
48
+ }
49
+ /**
50
+ * Value types for the Shell (`<wcs-idle>`) — identical observable surface to
51
+ * the Core.
52
+ */
53
+ type WcsIdleValues = WcsIdleCoreValues;
54
+
55
+ declare function bootstrapIdle(userConfig?: IWritableConfig): void;
56
+
57
+ declare function getConfig(): IConfig;
58
+
59
+ /**
60
+ * Headless Idle Detection primitive. A thin, framework-agnostic wrapper around
61
+ * `IdleDetector` exposed through the wc-bindable protocol.
62
+ *
63
+ * Reference implementation for batch2's "gesture-gated permission" archetype
64
+ * (docs/idle-detection-tag-design.md). `requestPermission()` wraps the static,
65
+ * user-gesture-gated `IdleDetector.requestPermission()` — this Core never
66
+ * calls it automatically; the caller must invoke it from within a real
67
+ * gesture handler.
68
+ *
69
+ * Deliberately does NOT track the 4-value permission state (prompt/granted/
70
+ * denied/unsupported) itself: `navigator.permissions.query({name:
71
+ * "idle-detection"})` exists, so compose with `<wcs-permission
72
+ * name="idle-detection">` for that instead (§0). This Core only exposes the
73
+ * actual idle state (userState/screenState) plus the one-time
74
+ * requestPermission()/start()/stop() actions.
75
+ */
76
+ declare class IdleCore extends EventTarget {
77
+ static wcBindable: IWcBindable;
78
+ private _target;
79
+ private _userState;
80
+ private _screenState;
81
+ private _error;
82
+ private _detector;
83
+ private _abortController;
84
+ private _gen;
85
+ private _ready;
86
+ constructor(target?: EventTarget);
87
+ get ready(): Promise<void>;
88
+ get userState(): IdleUserState | null;
89
+ get screenState(): IdleScreenState | null;
90
+ get active(): boolean;
91
+ get error(): any;
92
+ observe(): Promise<void>;
93
+ dispose(): void;
94
+ private _api;
95
+ private _setState;
96
+ private _setError;
97
+ /**
98
+ * Wraps the static, user-gesture-gated `IdleDetector.requestPermission()`.
99
+ * MUST be invoked from within a real user gesture handler by the caller —
100
+ * this Core cannot manufacture one. never-throw: a gesture-context
101
+ * rejection resolves to `"denied"` and lands in `error`. Gesture violation
102
+ * and an actual "denied" outcome are not distinguished — both mean "not
103
+ * usable right now" (§4.1).
104
+ */
105
+ requestPermission(): Promise<"granted" | "denied">;
106
+ /**
107
+ * Start an idle-detection session. `threshold` (ms) must be >= 60000 per
108
+ * spec — not validated here (§3): an out-of-range value is left to the
109
+ * browser's own TypeError, which never-throw absorbs into `error`.
110
+ */
111
+ start(threshold?: number): Promise<void>;
112
+ /** Stop the current session (if any) and detach its listener. Safe to call when not started. */
113
+ stop(): void;
114
+ private _onChange;
115
+ }
116
+
117
+ /**
118
+ * `<wcs-idle>` — declarative Idle Detection API primitive.
119
+ *
120
+ * Does NOT auto-start on connect (docs/idle-detection-tag-design.md §6): the
121
+ * permission gate sits in front of `start()`, so an unconditional
122
+ * connectedCallback start would be guaranteed to fail before permission is
123
+ * granted. Callers drive `requestPermission()` → `start()` explicitly, e.g.
124
+ * from a click handler.
125
+ *
126
+ * Compose with `<wcs-permission name="idle-detection">` for prompt/granted/
127
+ * denied status — this Shell only exposes the actual idle state.
128
+ */
129
+ declare class WcsIdle extends HTMLElement {
130
+ static hasConnectedCallbackPromise: boolean;
131
+ static wcBindable: IWcBindable;
132
+ private _core;
133
+ private _connectedCallbackPromise;
134
+ constructor();
135
+ /**
136
+ * Minimum idle time (ms) before `userState` becomes `"idle"`. This value is
137
+ * read only at `start()` time — there is no `attributeChangedCallback`
138
+ * (deliberately not declared in `observedAttributes`, mirroring
139
+ * `<wcs-gyroscope>`'s `frequency`), so mutating the attribute/property on an
140
+ * already-running session has no effect until the caller `stop()`s and
141
+ * `start()`s again.
142
+ */
143
+ get threshold(): number;
144
+ set threshold(value: number);
145
+ get userState(): IdleUserState | null;
146
+ get screenState(): IdleScreenState | null;
147
+ get active(): boolean;
148
+ get error(): any;
149
+ get connectedCallbackPromise(): Promise<void>;
150
+ requestPermission(): Promise<"granted" | "denied">;
151
+ start(threshold?: number): Promise<void>;
152
+ stop(): void;
153
+ connectedCallback(): void;
154
+ disconnectedCallback(): void;
155
+ }
156
+
157
+ export { IdleCore, WcsIdle, bootstrapIdle, getConfig };
158
+ export type { IWritableConfig, IWritableTagNames, IdleScreenState, IdleUserState, WcsIdleCoreValues, WcsIdleValues };
@@ -0,0 +1,338 @@
1
+ const _config = {
2
+ tagNames: {
3
+ idle: "wcs-idle",
4
+ },
5
+ };
6
+ function deepFreeze(obj) {
7
+ if (obj === null || typeof obj !== "object")
8
+ return obj;
9
+ Object.freeze(obj);
10
+ for (const key of Object.keys(obj)) {
11
+ deepFreeze(obj[key]);
12
+ }
13
+ return obj;
14
+ }
15
+ function deepClone(obj) {
16
+ if (obj === null || typeof obj !== "object")
17
+ return obj;
18
+ const clone = {};
19
+ for (const key of Object.keys(obj)) {
20
+ clone[key] = deepClone(obj[key]);
21
+ }
22
+ return clone;
23
+ }
24
+ let frozenConfig = null;
25
+ const config = _config;
26
+ function getConfig() {
27
+ if (!frozenConfig) {
28
+ frozenConfig = deepFreeze(deepClone(_config));
29
+ }
30
+ return frozenConfig;
31
+ }
32
+ function setConfig(partialConfig) {
33
+ if (partialConfig.tagNames) {
34
+ Object.assign(_config.tagNames, partialConfig.tagNames);
35
+ }
36
+ frozenConfig = null;
37
+ }
38
+
39
+ const MIN_THRESHOLD = 60000;
40
+ /**
41
+ * Headless Idle Detection primitive. A thin, framework-agnostic wrapper around
42
+ * `IdleDetector` exposed through the wc-bindable protocol.
43
+ *
44
+ * Reference implementation for batch2's "gesture-gated permission" archetype
45
+ * (docs/idle-detection-tag-design.md). `requestPermission()` wraps the static,
46
+ * user-gesture-gated `IdleDetector.requestPermission()` — this Core never
47
+ * calls it automatically; the caller must invoke it from within a real
48
+ * gesture handler.
49
+ *
50
+ * Deliberately does NOT track the 4-value permission state (prompt/granted/
51
+ * denied/unsupported) itself: `navigator.permissions.query({name:
52
+ * "idle-detection"})` exists, so compose with `<wcs-permission
53
+ * name="idle-detection">` for that instead (§0). This Core only exposes the
54
+ * actual idle state (userState/screenState) plus the one-time
55
+ * requestPermission()/start()/stop() actions.
56
+ */
57
+ class IdleCore extends EventTarget {
58
+ static wcBindable = {
59
+ protocol: "wc-bindable",
60
+ version: 1,
61
+ properties: [
62
+ { name: "userState", event: "wcs-idle:change", getter: (e) => e.detail.userState },
63
+ { name: "screenState", event: "wcs-idle:change", getter: (e) => e.detail.screenState },
64
+ {
65
+ name: "active",
66
+ event: "wcs-idle:change",
67
+ getter: (e) => e.detail.userState === "active",
68
+ },
69
+ // never-throw (§3.6): requestPermission()/start() failures land here
70
+ // instead of rejecting/throwing. Mirrors every other bidirectional IO
71
+ // node in this batch (fetch, share, screen-orientation).
72
+ { name: "error", event: "wcs-idle:error" },
73
+ ],
74
+ // No `inputs`: the Core has no settable `threshold` state — `threshold` is a
75
+ // per-call argument to `start(threshold)`, not a property/setter. The DOM-driven
76
+ // `threshold` input surface belongs to the Shell (which declares it and backs it
77
+ // with the `threshold` attribute), mirroring geolocation/intersection where the
78
+ // Core declares no inputs and the Shell adds them.
79
+ commands: [
80
+ { name: "requestPermission", async: true },
81
+ { name: "start", async: true },
82
+ { name: "stop" },
83
+ ],
84
+ };
85
+ _target;
86
+ _userState = null;
87
+ _screenState = null;
88
+ _error = null;
89
+ _detector = null;
90
+ _abortController = null;
91
+ // Generation guard (§3.4): bumped on dispose()/stop() and each start().
92
+ _gen = 0;
93
+ // SSR (§3.8): never auto-starts on connect, so there is no probe to await —
94
+ // readiness is always immediate (docs/idle-detection-tag-design.md §7).
95
+ _ready = Promise.resolve();
96
+ constructor(target) {
97
+ super();
98
+ this._target = target ?? this;
99
+ }
100
+ get ready() {
101
+ return this._ready;
102
+ }
103
+ get userState() {
104
+ return this._userState;
105
+ }
106
+ get screenState() {
107
+ return this._screenState;
108
+ }
109
+ get active() {
110
+ return this._userState === "active";
111
+ }
112
+ get error() {
113
+ return this._error;
114
+ }
115
+ // Lifecycle (§3.5). observe() is a synchronous no-op: unlike most IO nodes,
116
+ // this Core deliberately does NOT auto-start on connect (§6) — permission
117
+ // is gesture-gated, so attempting start() before it is granted is
118
+ // guaranteed to fail.
119
+ observe() {
120
+ return this._ready;
121
+ }
122
+ dispose() {
123
+ this.stop();
124
+ }
125
+ _api() {
126
+ const g = globalThis;
127
+ return typeof g.IdleDetector === "function" ? g.IdleDetector : undefined;
128
+ }
129
+ _setState(userState, screenState) {
130
+ if (this._userState === userState && this._screenState === screenState)
131
+ return;
132
+ this._userState = userState;
133
+ this._screenState = screenState;
134
+ this._target.dispatchEvent(new CustomEvent("wcs-idle:change", {
135
+ detail: { userState, screenState },
136
+ bubbles: true,
137
+ }));
138
+ }
139
+ _setError(error) {
140
+ if (this._error === error)
141
+ return;
142
+ this._error = error;
143
+ this._target.dispatchEvent(new CustomEvent("wcs-idle:error", {
144
+ detail: error,
145
+ bubbles: true,
146
+ }));
147
+ }
148
+ /**
149
+ * Wraps the static, user-gesture-gated `IdleDetector.requestPermission()`.
150
+ * MUST be invoked from within a real user gesture handler by the caller —
151
+ * this Core cannot manufacture one. never-throw: a gesture-context
152
+ * rejection resolves to `"denied"` and lands in `error`. Gesture violation
153
+ * and an actual "denied" outcome are not distinguished — both mean "not
154
+ * usable right now" (§4.1).
155
+ */
156
+ async requestPermission() {
157
+ const Ctor = this._api();
158
+ if (!Ctor) {
159
+ this._setError({ message: "IdleDetector is not supported in this browser" });
160
+ return "denied";
161
+ }
162
+ try {
163
+ const result = await Ctor.requestPermission();
164
+ // Symmetric with start()'s success path: any settled (non-throwing)
165
+ // outcome — granted or a plain "denied" — supersedes a stale error from
166
+ // an earlier attempt (e.g. a prior gesture-context rejection).
167
+ this._setError(null);
168
+ return result === "granted" ? "granted" : "denied";
169
+ }
170
+ catch (e) {
171
+ this._setError({ error: e });
172
+ return "denied";
173
+ }
174
+ }
175
+ /**
176
+ * Start an idle-detection session. `threshold` (ms) must be >= 60000 per
177
+ * spec — not validated here (§3): an out-of-range value is left to the
178
+ * browser's own TypeError, which never-throw absorbs into `error`.
179
+ */
180
+ async start(threshold = MIN_THRESHOLD) {
181
+ this.stop(); // supersede any in-flight session (mirrors FetchCore's "cancel then start")
182
+ const Ctor = this._api();
183
+ if (!Ctor) {
184
+ this._setError({ message: "IdleDetector is not supported in this browser" });
185
+ return;
186
+ }
187
+ const ac = new AbortController();
188
+ this._abortController = ac;
189
+ const gen = ++this._gen;
190
+ try {
191
+ const detector = new Ctor();
192
+ detector.addEventListener("change", this._onChange);
193
+ this._detector = detector;
194
+ await detector.start({ threshold, signal: ac.signal });
195
+ if (gen !== this._gen)
196
+ return; // stale (stop()/dispose() ran during the await)
197
+ this._setError(null);
198
+ this._setState(detector.userState, detector.screenState);
199
+ }
200
+ catch (e) {
201
+ // No separate AbortError check: stop()/dispose() bump `_gen` *before*
202
+ // calling `ac.abort()` (see stop() below), so a stop()-triggered
203
+ // AbortError always has a stale `gen` here and is already caught by
204
+ // the check above. The signal is private and never exposed, so an
205
+ // AbortError from any other source cannot occur.
206
+ if (gen !== this._gen)
207
+ return;
208
+ // Tear down the failed session's listener/controller (mirrors stop()):
209
+ // without this, the failed `_detector` stays wired to `_onChange` and a
210
+ // later `change` on that same (never-truly-started) instance would
211
+ // still write state, contradicting the error just recorded.
212
+ this._detector?.removeEventListener("change", this._onChange);
213
+ this._detector = null;
214
+ this._abortController = null;
215
+ this._setError({ error: e });
216
+ }
217
+ }
218
+ /** Stop the current session (if any) and detach its listener. Safe to call when not started. */
219
+ stop() {
220
+ this._gen++;
221
+ this._abortController?.abort();
222
+ this._abortController = null;
223
+ if (this._detector) {
224
+ this._detector.removeEventListener("change", this._onChange);
225
+ this._detector = null;
226
+ }
227
+ }
228
+ _onChange = (event) => {
229
+ const detector = event.target;
230
+ this._setState(detector.userState, detector.screenState);
231
+ };
232
+ }
233
+
234
+ /**
235
+ * `<wcs-idle>` — declarative Idle Detection API primitive.
236
+ *
237
+ * Does NOT auto-start on connect (docs/idle-detection-tag-design.md §6): the
238
+ * permission gate sits in front of `start()`, so an unconditional
239
+ * connectedCallback start would be guaranteed to fail before permission is
240
+ * granted. Callers drive `requestPermission()` → `start()` explicitly, e.g.
241
+ * from a click handler.
242
+ *
243
+ * Compose with `<wcs-permission name="idle-detection">` for prompt/granted/
244
+ * denied status — this Shell only exposes the actual idle state.
245
+ */
246
+ class WcsIdle extends HTMLElement {
247
+ static hasConnectedCallbackPromise = true;
248
+ static wcBindable = {
249
+ ...IdleCore.wcBindable,
250
+ inputs: [
251
+ { name: "threshold", attribute: "threshold" },
252
+ ],
253
+ // Core の commands をそのまま継承(単一情報源)。
254
+ commands: IdleCore.wcBindable.commands,
255
+ };
256
+ _core;
257
+ _connectedCallbackPromise = Promise.resolve();
258
+ constructor() {
259
+ super();
260
+ this._core = new IdleCore(this);
261
+ }
262
+ // --- Attribute accessors ---
263
+ /**
264
+ * Minimum idle time (ms) before `userState` becomes `"idle"`. This value is
265
+ * read only at `start()` time — there is no `attributeChangedCallback`
266
+ * (deliberately not declared in `observedAttributes`, mirroring
267
+ * `<wcs-gyroscope>`'s `frequency`), so mutating the attribute/property on an
268
+ * already-running session has no effect until the caller `stop()`s and
269
+ * `start()`s again.
270
+ */
271
+ get threshold() {
272
+ const attr = this.getAttribute("threshold");
273
+ // An absent, empty, or whitespace-only attribute all mean "no value
274
+ // supplied" and must fall back to the default — without this check,
275
+ // `Number("")`/`Number(" ")` coerce to `0` (finite), which would slip
276
+ // past the `Number.isFinite` fallback below and silently return `0`
277
+ // instead of the documented 60000ms default.
278
+ if (attr === null || attr.trim() === "")
279
+ return 60000;
280
+ const n = Number(attr);
281
+ return Number.isFinite(n) ? n : 60000;
282
+ }
283
+ set threshold(value) {
284
+ this.setAttribute("threshold", String(value));
285
+ }
286
+ // --- Core delegated getters ---
287
+ get userState() {
288
+ return this._core.userState;
289
+ }
290
+ get screenState() {
291
+ return this._core.screenState;
292
+ }
293
+ get active() {
294
+ return this._core.active;
295
+ }
296
+ get error() {
297
+ return this._core.error;
298
+ }
299
+ get connectedCallbackPromise() {
300
+ return this._connectedCallbackPromise;
301
+ }
302
+ // --- Commands (delegated to Core) ---
303
+ requestPermission() {
304
+ return this._core.requestPermission();
305
+ }
306
+ start(threshold) {
307
+ return this._core.start(threshold ?? this.threshold);
308
+ }
309
+ stop() {
310
+ this._core.stop();
311
+ }
312
+ // --- Lifecycle ---
313
+ connectedCallback() {
314
+ this.style.display = "none";
315
+ // No auto-start (§6) — observe() is a synchronous no-op, kept only for
316
+ // API uniformity with other IO nodes' lifecycle.
317
+ this._connectedCallbackPromise = this._core.observe();
318
+ }
319
+ disconnectedCallback() {
320
+ this._core.dispose();
321
+ }
322
+ }
323
+
324
+ function registerComponents() {
325
+ if (!customElements.get(config.tagNames.idle)) {
326
+ customElements.define(config.tagNames.idle, WcsIdle);
327
+ }
328
+ }
329
+
330
+ function bootstrapIdle(userConfig) {
331
+ if (userConfig) {
332
+ setConfig(userConfig);
333
+ }
334
+ registerComponents();
335
+ }
336
+
337
+ export { IdleCore, WcsIdle, bootstrapIdle, getConfig };
338
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":["../src/config.ts","../src/core/IdleCore.ts","../src/components/Idle.ts","../src/registerComponents.ts","../src/bootstrapIdle.ts"],"sourcesContent":["import { IConfig, IWritableConfig } from \"./types.js\";\n\ninterface IInternalConfig extends IConfig {\n tagNames: {\n idle: string;\n };\n}\n\nconst _config: IInternalConfig = {\n tagNames: {\n idle: \"wcs-idle\",\n },\n};\n\nfunction deepFreeze<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n Object.freeze(obj);\n for (const key of Object.keys(obj)) {\n deepFreeze((obj as Record<string, unknown>)[key]);\n }\n return obj;\n}\n\nfunction deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n const clone: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n clone[key] = deepClone((obj as Record<string, unknown>)[key]);\n }\n return clone as T;\n}\n\nlet frozenConfig: IConfig | null = null;\n\nexport const config: IConfig = _config as IConfig;\n\nexport function getConfig(): IConfig {\n if (!frozenConfig) {\n frozenConfig = deepFreeze(deepClone(_config));\n }\n return frozenConfig;\n}\n\nexport function setConfig(partialConfig: IWritableConfig): void {\n if (partialConfig.tagNames) {\n Object.assign(_config.tagNames, partialConfig.tagNames);\n }\n frozenConfig = null;\n}\n","import { IdleScreenState, IdleUserState, IWcBindable } from \"../types.js\";\n\ninterface IdleDetectorLike extends EventTarget {\n userState: IdleUserState;\n screenState: IdleScreenState;\n start(options: { threshold: number; signal: AbortSignal }): Promise<void>;\n}\n\ninterface IdleDetectorCtor {\n new (): IdleDetectorLike;\n requestPermission(): Promise<\"granted\" | \"denied\">;\n}\n\nconst MIN_THRESHOLD = 60000;\n\n/**\n * Headless Idle Detection primitive. A thin, framework-agnostic wrapper around\n * `IdleDetector` exposed through the wc-bindable protocol.\n *\n * Reference implementation for batch2's \"gesture-gated permission\" archetype\n * (docs/idle-detection-tag-design.md). `requestPermission()` wraps the static,\n * user-gesture-gated `IdleDetector.requestPermission()` — this Core never\n * calls it automatically; the caller must invoke it from within a real\n * gesture handler.\n *\n * Deliberately does NOT track the 4-value permission state (prompt/granted/\n * denied/unsupported) itself: `navigator.permissions.query({name:\n * \"idle-detection\"})` exists, so compose with `<wcs-permission\n * name=\"idle-detection\">` for that instead (§0). This Core only exposes the\n * actual idle state (userState/screenState) plus the one-time\n * requestPermission()/start()/stop() actions.\n */\nexport class IdleCore extends EventTarget {\n static wcBindable: IWcBindable = {\n protocol: \"wc-bindable\",\n version: 1,\n properties: [\n { name: \"userState\", event: \"wcs-idle:change\", getter: (e: Event) => (e as CustomEvent).detail.userState },\n { name: \"screenState\", event: \"wcs-idle:change\", getter: (e: Event) => (e as CustomEvent).detail.screenState },\n {\n name: \"active\",\n event: \"wcs-idle:change\",\n getter: (e: Event) => (e as CustomEvent).detail.userState === \"active\",\n },\n // never-throw (§3.6): requestPermission()/start() failures land here\n // instead of rejecting/throwing. Mirrors every other bidirectional IO\n // node in this batch (fetch, share, screen-orientation).\n { name: \"error\", event: \"wcs-idle:error\" },\n ],\n // No `inputs`: the Core has no settable `threshold` state — `threshold` is a\n // per-call argument to `start(threshold)`, not a property/setter. The DOM-driven\n // `threshold` input surface belongs to the Shell (which declares it and backs it\n // with the `threshold` attribute), mirroring geolocation/intersection where the\n // Core declares no inputs and the Shell adds them.\n commands: [\n { name: \"requestPermission\", async: true },\n { name: \"start\", async: true },\n { name: \"stop\" },\n ],\n };\n\n private _target: EventTarget;\n private _userState: IdleUserState | null = null;\n private _screenState: IdleScreenState | null = null;\n private _error: any = null;\n private _detector: IdleDetectorLike | null = null;\n private _abortController: AbortController | null = null;\n\n // Generation guard (§3.4): bumped on dispose()/stop() and each start().\n private _gen = 0;\n\n // SSR (§3.8): never auto-starts on connect, so there is no probe to await —\n // readiness is always immediate (docs/idle-detection-tag-design.md §7).\n private _ready: Promise<void> = Promise.resolve();\n\n constructor(target?: EventTarget) {\n super();\n this._target = target ?? this;\n }\n\n get ready(): Promise<void> {\n return this._ready;\n }\n\n get userState(): IdleUserState | null {\n return this._userState;\n }\n\n get screenState(): IdleScreenState | null {\n return this._screenState;\n }\n\n get active(): boolean {\n return this._userState === \"active\";\n }\n\n get error(): any {\n return this._error;\n }\n\n // Lifecycle (§3.5). observe() is a synchronous no-op: unlike most IO nodes,\n // this Core deliberately does NOT auto-start on connect (§6) — permission\n // is gesture-gated, so attempting start() before it is granted is\n // guaranteed to fail.\n observe(): Promise<void> {\n return this._ready;\n }\n\n dispose(): void {\n this.stop();\n }\n\n private _api(): IdleDetectorCtor | undefined {\n const g = globalThis as any;\n return typeof g.IdleDetector === \"function\" ? g.IdleDetector : undefined;\n }\n\n private _setState(userState: IdleUserState, screenState: IdleScreenState): void {\n if (this._userState === userState && this._screenState === screenState) return;\n this._userState = userState;\n this._screenState = screenState;\n this._target.dispatchEvent(new CustomEvent(\"wcs-idle:change\", {\n detail: { userState, screenState },\n bubbles: true,\n }));\n }\n\n private _setError(error: any): void {\n if (this._error === error) return;\n this._error = error;\n this._target.dispatchEvent(new CustomEvent(\"wcs-idle:error\", {\n detail: error,\n bubbles: true,\n }));\n }\n\n /**\n * Wraps the static, user-gesture-gated `IdleDetector.requestPermission()`.\n * MUST be invoked from within a real user gesture handler by the caller —\n * this Core cannot manufacture one. never-throw: a gesture-context\n * rejection resolves to `\"denied\"` and lands in `error`. Gesture violation\n * and an actual \"denied\" outcome are not distinguished — both mean \"not\n * usable right now\" (§4.1).\n */\n async requestPermission(): Promise<\"granted\" | \"denied\"> {\n const Ctor = this._api();\n if (!Ctor) {\n this._setError({ message: \"IdleDetector is not supported in this browser\" });\n return \"denied\";\n }\n try {\n const result = await Ctor.requestPermission();\n // Symmetric with start()'s success path: any settled (non-throwing)\n // outcome — granted or a plain \"denied\" — supersedes a stale error from\n // an earlier attempt (e.g. a prior gesture-context rejection).\n this._setError(null);\n return result === \"granted\" ? \"granted\" : \"denied\";\n } catch (e) {\n this._setError({ error: e });\n return \"denied\";\n }\n }\n\n /**\n * Start an idle-detection session. `threshold` (ms) must be >= 60000 per\n * spec — not validated here (§3): an out-of-range value is left to the\n * browser's own TypeError, which never-throw absorbs into `error`.\n */\n async start(threshold: number = MIN_THRESHOLD): Promise<void> {\n this.stop(); // supersede any in-flight session (mirrors FetchCore's \"cancel then start\")\n\n const Ctor = this._api();\n if (!Ctor) {\n this._setError({ message: \"IdleDetector is not supported in this browser\" });\n return;\n }\n\n const ac = new AbortController();\n this._abortController = ac;\n const gen = ++this._gen;\n\n try {\n const detector = new Ctor();\n detector.addEventListener(\"change\", this._onChange);\n this._detector = detector;\n await detector.start({ threshold, signal: ac.signal });\n if (gen !== this._gen) return; // stale (stop()/dispose() ran during the await)\n this._setError(null);\n this._setState(detector.userState, detector.screenState);\n } catch (e: any) {\n // No separate AbortError check: stop()/dispose() bump `_gen` *before*\n // calling `ac.abort()` (see stop() below), so a stop()-triggered\n // AbortError always has a stale `gen` here and is already caught by\n // the check above. The signal is private and never exposed, so an\n // AbortError from any other source cannot occur.\n if (gen !== this._gen) return;\n // Tear down the failed session's listener/controller (mirrors stop()):\n // without this, the failed `_detector` stays wired to `_onChange` and a\n // later `change` on that same (never-truly-started) instance would\n // still write state, contradicting the error just recorded.\n this._detector?.removeEventListener(\"change\", this._onChange);\n this._detector = null;\n this._abortController = null;\n this._setError({ error: e });\n }\n }\n\n /** Stop the current session (if any) and detach its listener. Safe to call when not started. */\n stop(): void {\n this._gen++;\n this._abortController?.abort();\n this._abortController = null;\n if (this._detector) {\n this._detector.removeEventListener(\"change\", this._onChange);\n this._detector = null;\n }\n }\n\n private _onChange = (event: Event): void => {\n const detector = event.target as IdleDetectorLike;\n this._setState(detector.userState, detector.screenState);\n };\n}\n","import { IdleScreenState, IdleUserState, IWcBindable } from \"../types.js\";\nimport { IdleCore } from \"../core/IdleCore.js\";\n\n/**\n * `<wcs-idle>` — declarative Idle Detection API primitive.\n *\n * Does NOT auto-start on connect (docs/idle-detection-tag-design.md §6): the\n * permission gate sits in front of `start()`, so an unconditional\n * connectedCallback start would be guaranteed to fail before permission is\n * granted. Callers drive `requestPermission()` → `start()` explicitly, e.g.\n * from a click handler.\n *\n * Compose with `<wcs-permission name=\"idle-detection\">` for prompt/granted/\n * denied status — this Shell only exposes the actual idle state.\n */\nexport class WcsIdle extends HTMLElement {\n static hasConnectedCallbackPromise = true;\n\n static wcBindable: IWcBindable = {\n ...IdleCore.wcBindable,\n inputs: [\n { name: \"threshold\", attribute: \"threshold\" },\n ],\n // Core の commands をそのまま継承(単一情報源)。\n commands: IdleCore.wcBindable.commands,\n };\n\n private _core: IdleCore;\n private _connectedCallbackPromise: Promise<void> = Promise.resolve();\n\n constructor() {\n super();\n this._core = new IdleCore(this);\n }\n\n // --- Attribute accessors ---\n\n /**\n * Minimum idle time (ms) before `userState` becomes `\"idle\"`. This value is\n * read only at `start()` time — there is no `attributeChangedCallback`\n * (deliberately not declared in `observedAttributes`, mirroring\n * `<wcs-gyroscope>`'s `frequency`), so mutating the attribute/property on an\n * already-running session has no effect until the caller `stop()`s and\n * `start()`s again.\n */\n get threshold(): number {\n const attr = this.getAttribute(\"threshold\");\n // An absent, empty, or whitespace-only attribute all mean \"no value\n // supplied\" and must fall back to the default — without this check,\n // `Number(\"\")`/`Number(\" \")` coerce to `0` (finite), which would slip\n // past the `Number.isFinite` fallback below and silently return `0`\n // instead of the documented 60000ms default.\n if (attr === null || attr.trim() === \"\") return 60000;\n const n = Number(attr);\n return Number.isFinite(n) ? n : 60000;\n }\n\n set threshold(value: number) {\n this.setAttribute(\"threshold\", String(value));\n }\n\n // --- Core delegated getters ---\n\n get userState(): IdleUserState | null {\n return this._core.userState;\n }\n\n get screenState(): IdleScreenState | null {\n return this._core.screenState;\n }\n\n get active(): boolean {\n return this._core.active;\n }\n\n get error(): any {\n return this._core.error;\n }\n\n get connectedCallbackPromise(): Promise<void> {\n return this._connectedCallbackPromise;\n }\n\n // --- Commands (delegated to Core) ---\n\n requestPermission(): Promise<\"granted\" | \"denied\"> {\n return this._core.requestPermission();\n }\n\n start(threshold?: number): Promise<void> {\n return this._core.start(threshold ?? this.threshold);\n }\n\n stop(): void {\n this._core.stop();\n }\n\n // --- Lifecycle ---\n\n connectedCallback(): void {\n this.style.display = \"none\";\n // No auto-start (§6) — observe() is a synchronous no-op, kept only for\n // API uniformity with other IO nodes' lifecycle.\n this._connectedCallbackPromise = this._core.observe();\n }\n\n disconnectedCallback(): void {\n this._core.dispose();\n }\n}\n","import { WcsIdle } from \"./components/Idle.js\";\nimport { config } from \"./config.js\";\n\nexport function registerComponents(): void {\n if (!customElements.get(config.tagNames.idle)) {\n customElements.define(config.tagNames.idle, WcsIdle);\n }\n}\n","import { setConfig } from \"./config.js\";\nimport { registerComponents } from \"./registerComponents.js\";\nimport { IWritableConfig } from \"./types.js\";\n\nexport function bootstrapIdle(userConfig?: IWritableConfig): void {\n if (userConfig) {\n setConfig(userConfig);\n }\n registerComponents();\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e={tagNames:{idle:"wcs-idle"}};function t(e){if(null===e||"object"!=typeof e)return e;Object.freeze(e);for(const r of Object.keys(e))t(e[r]);return e}function r(e){if(null===e||"object"!=typeof e)return e;const t={};for(const s of Object.keys(e))t[s]=r(e[s]);return t}let s=null;const n=e;function i(){return s||(s=t(r(e))),s}class o extends EventTarget{static wcBindable={protocol:"wc-bindable",version:1,properties:[{name:"userState",event:"wcs-idle:change",getter:e=>e.detail.userState},{name:"screenState",event:"wcs-idle:change",getter:e=>e.detail.screenState},{name:"active",event:"wcs-idle:change",getter:e=>"active"===e.detail.userState},{name:"error",event:"wcs-idle:error"}],commands:[{name:"requestPermission",async:!0},{name:"start",async:!0},{name:"stop"}]};_target;_userState=null;_screenState=null;_error=null;_detector=null;_abortController=null;_gen=0;_ready=Promise.resolve();constructor(e){super(),this._target=e??this}get ready(){return this._ready}get userState(){return this._userState}get screenState(){return this._screenState}get active(){return"active"===this._userState}get error(){return this._error}observe(){return this._ready}dispose(){this.stop()}_api(){const e=globalThis;return"function"==typeof e.IdleDetector?e.IdleDetector:void 0}_setState(e,t){this._userState===e&&this._screenState===t||(this._userState=e,this._screenState=t,this._target.dispatchEvent(new CustomEvent("wcs-idle:change",{detail:{userState:e,screenState:t},bubbles:!0})))}_setError(e){this._error!==e&&(this._error=e,this._target.dispatchEvent(new CustomEvent("wcs-idle:error",{detail:e,bubbles:!0})))}async requestPermission(){const e=this._api();if(!e)return this._setError({message:"IdleDetector is not supported in this browser"}),"denied";try{const t=await e.requestPermission();return this._setError(null),"granted"===t?"granted":"denied"}catch(e){return this._setError({error:e}),"denied"}}async start(e=6e4){this.stop();const t=this._api();if(!t)return void this._setError({message:"IdleDetector is not supported in this browser"});const r=new AbortController;this._abortController=r;const s=++this._gen;try{const n=new t;if(n.addEventListener("change",this._onChange),this._detector=n,await n.start({threshold:e,signal:r.signal}),s!==this._gen)return;this._setError(null),this._setState(n.userState,n.screenState)}catch(e){if(s!==this._gen)return;this._detector?.removeEventListener("change",this._onChange),this._detector=null,this._abortController=null,this._setError({error:e})}}stop(){this._gen++,this._abortController?.abort(),this._abortController=null,this._detector&&(this._detector.removeEventListener("change",this._onChange),this._detector=null)}_onChange=e=>{const t=e.target;this._setState(t.userState,t.screenState)}}class a extends HTMLElement{static hasConnectedCallbackPromise=!0;static wcBindable={...o.wcBindable,inputs:[{name:"threshold",attribute:"threshold"}],commands:o.wcBindable.commands};_core;_connectedCallbackPromise=Promise.resolve();constructor(){super(),this._core=new o(this)}get threshold(){const e=this.getAttribute("threshold");if(null===e||""===e.trim())return 6e4;const t=Number(e);return Number.isFinite(t)?t:6e4}set threshold(e){this.setAttribute("threshold",String(e))}get userState(){return this._core.userState}get screenState(){return this._core.screenState}get active(){return this._core.active}get error(){return this._core.error}get connectedCallbackPromise(){return this._connectedCallbackPromise}requestPermission(){return this._core.requestPermission()}start(e){return this._core.start(e??this.threshold)}stop(){this._core.stop()}connectedCallback(){this.style.display="none",this._connectedCallbackPromise=this._core.observe()}disconnectedCallback(){this._core.dispose()}}function c(t){var r;t&&((r=t).tagNames&&Object.assign(e.tagNames,r.tagNames),s=null),customElements.get(n.tagNames.idle)||customElements.define(n.tagNames.idle,a)}export{o as IdleCore,a as WcsIdle,c as bootstrapIdle,i as getConfig};
2
+ //# sourceMappingURL=index.esm.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.min.js","sources":["../src/config.ts","../src/core/IdleCore.ts","../src/components/Idle.ts","../src/bootstrapIdle.ts","../src/registerComponents.ts"],"sourcesContent":["import { IConfig, IWritableConfig } from \"./types.js\";\n\ninterface IInternalConfig extends IConfig {\n tagNames: {\n idle: string;\n };\n}\n\nconst _config: IInternalConfig = {\n tagNames: {\n idle: \"wcs-idle\",\n },\n};\n\nfunction deepFreeze<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n Object.freeze(obj);\n for (const key of Object.keys(obj)) {\n deepFreeze((obj as Record<string, unknown>)[key]);\n }\n return obj;\n}\n\nfunction deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n const clone: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n clone[key] = deepClone((obj as Record<string, unknown>)[key]);\n }\n return clone as T;\n}\n\nlet frozenConfig: IConfig | null = null;\n\nexport const config: IConfig = _config as IConfig;\n\nexport function getConfig(): IConfig {\n if (!frozenConfig) {\n frozenConfig = deepFreeze(deepClone(_config));\n }\n return frozenConfig;\n}\n\nexport function setConfig(partialConfig: IWritableConfig): void {\n if (partialConfig.tagNames) {\n Object.assign(_config.tagNames, partialConfig.tagNames);\n }\n frozenConfig = null;\n}\n","import { IdleScreenState, IdleUserState, IWcBindable } from \"../types.js\";\n\ninterface IdleDetectorLike extends EventTarget {\n userState: IdleUserState;\n screenState: IdleScreenState;\n start(options: { threshold: number; signal: AbortSignal }): Promise<void>;\n}\n\ninterface IdleDetectorCtor {\n new (): IdleDetectorLike;\n requestPermission(): Promise<\"granted\" | \"denied\">;\n}\n\nconst MIN_THRESHOLD = 60000;\n\n/**\n * Headless Idle Detection primitive. A thin, framework-agnostic wrapper around\n * `IdleDetector` exposed through the wc-bindable protocol.\n *\n * Reference implementation for batch2's \"gesture-gated permission\" archetype\n * (docs/idle-detection-tag-design.md). `requestPermission()` wraps the static,\n * user-gesture-gated `IdleDetector.requestPermission()` — this Core never\n * calls it automatically; the caller must invoke it from within a real\n * gesture handler.\n *\n * Deliberately does NOT track the 4-value permission state (prompt/granted/\n * denied/unsupported) itself: `navigator.permissions.query({name:\n * \"idle-detection\"})` exists, so compose with `<wcs-permission\n * name=\"idle-detection\">` for that instead (§0). This Core only exposes the\n * actual idle state (userState/screenState) plus the one-time\n * requestPermission()/start()/stop() actions.\n */\nexport class IdleCore extends EventTarget {\n static wcBindable: IWcBindable = {\n protocol: \"wc-bindable\",\n version: 1,\n properties: [\n { name: \"userState\", event: \"wcs-idle:change\", getter: (e: Event) => (e as CustomEvent).detail.userState },\n { name: \"screenState\", event: \"wcs-idle:change\", getter: (e: Event) => (e as CustomEvent).detail.screenState },\n {\n name: \"active\",\n event: \"wcs-idle:change\",\n getter: (e: Event) => (e as CustomEvent).detail.userState === \"active\",\n },\n // never-throw (§3.6): requestPermission()/start() failures land here\n // instead of rejecting/throwing. Mirrors every other bidirectional IO\n // node in this batch (fetch, share, screen-orientation).\n { name: \"error\", event: \"wcs-idle:error\" },\n ],\n // No `inputs`: the Core has no settable `threshold` state — `threshold` is a\n // per-call argument to `start(threshold)`, not a property/setter. The DOM-driven\n // `threshold` input surface belongs to the Shell (which declares it and backs it\n // with the `threshold` attribute), mirroring geolocation/intersection where the\n // Core declares no inputs and the Shell adds them.\n commands: [\n { name: \"requestPermission\", async: true },\n { name: \"start\", async: true },\n { name: \"stop\" },\n ],\n };\n\n private _target: EventTarget;\n private _userState: IdleUserState | null = null;\n private _screenState: IdleScreenState | null = null;\n private _error: any = null;\n private _detector: IdleDetectorLike | null = null;\n private _abortController: AbortController | null = null;\n\n // Generation guard (§3.4): bumped on dispose()/stop() and each start().\n private _gen = 0;\n\n // SSR (§3.8): never auto-starts on connect, so there is no probe to await —\n // readiness is always immediate (docs/idle-detection-tag-design.md §7).\n private _ready: Promise<void> = Promise.resolve();\n\n constructor(target?: EventTarget) {\n super();\n this._target = target ?? this;\n }\n\n get ready(): Promise<void> {\n return this._ready;\n }\n\n get userState(): IdleUserState | null {\n return this._userState;\n }\n\n get screenState(): IdleScreenState | null {\n return this._screenState;\n }\n\n get active(): boolean {\n return this._userState === \"active\";\n }\n\n get error(): any {\n return this._error;\n }\n\n // Lifecycle (§3.5). observe() is a synchronous no-op: unlike most IO nodes,\n // this Core deliberately does NOT auto-start on connect (§6) — permission\n // is gesture-gated, so attempting start() before it is granted is\n // guaranteed to fail.\n observe(): Promise<void> {\n return this._ready;\n }\n\n dispose(): void {\n this.stop();\n }\n\n private _api(): IdleDetectorCtor | undefined {\n const g = globalThis as any;\n return typeof g.IdleDetector === \"function\" ? g.IdleDetector : undefined;\n }\n\n private _setState(userState: IdleUserState, screenState: IdleScreenState): void {\n if (this._userState === userState && this._screenState === screenState) return;\n this._userState = userState;\n this._screenState = screenState;\n this._target.dispatchEvent(new CustomEvent(\"wcs-idle:change\", {\n detail: { userState, screenState },\n bubbles: true,\n }));\n }\n\n private _setError(error: any): void {\n if (this._error === error) return;\n this._error = error;\n this._target.dispatchEvent(new CustomEvent(\"wcs-idle:error\", {\n detail: error,\n bubbles: true,\n }));\n }\n\n /**\n * Wraps the static, user-gesture-gated `IdleDetector.requestPermission()`.\n * MUST be invoked from within a real user gesture handler by the caller —\n * this Core cannot manufacture one. never-throw: a gesture-context\n * rejection resolves to `\"denied\"` and lands in `error`. Gesture violation\n * and an actual \"denied\" outcome are not distinguished — both mean \"not\n * usable right now\" (§4.1).\n */\n async requestPermission(): Promise<\"granted\" | \"denied\"> {\n const Ctor = this._api();\n if (!Ctor) {\n this._setError({ message: \"IdleDetector is not supported in this browser\" });\n return \"denied\";\n }\n try {\n const result = await Ctor.requestPermission();\n // Symmetric with start()'s success path: any settled (non-throwing)\n // outcome — granted or a plain \"denied\" — supersedes a stale error from\n // an earlier attempt (e.g. a prior gesture-context rejection).\n this._setError(null);\n return result === \"granted\" ? \"granted\" : \"denied\";\n } catch (e) {\n this._setError({ error: e });\n return \"denied\";\n }\n }\n\n /**\n * Start an idle-detection session. `threshold` (ms) must be >= 60000 per\n * spec — not validated here (§3): an out-of-range value is left to the\n * browser's own TypeError, which never-throw absorbs into `error`.\n */\n async start(threshold: number = MIN_THRESHOLD): Promise<void> {\n this.stop(); // supersede any in-flight session (mirrors FetchCore's \"cancel then start\")\n\n const Ctor = this._api();\n if (!Ctor) {\n this._setError({ message: \"IdleDetector is not supported in this browser\" });\n return;\n }\n\n const ac = new AbortController();\n this._abortController = ac;\n const gen = ++this._gen;\n\n try {\n const detector = new Ctor();\n detector.addEventListener(\"change\", this._onChange);\n this._detector = detector;\n await detector.start({ threshold, signal: ac.signal });\n if (gen !== this._gen) return; // stale (stop()/dispose() ran during the await)\n this._setError(null);\n this._setState(detector.userState, detector.screenState);\n } catch (e: any) {\n // No separate AbortError check: stop()/dispose() bump `_gen` *before*\n // calling `ac.abort()` (see stop() below), so a stop()-triggered\n // AbortError always has a stale `gen` here and is already caught by\n // the check above. The signal is private and never exposed, so an\n // AbortError from any other source cannot occur.\n if (gen !== this._gen) return;\n // Tear down the failed session's listener/controller (mirrors stop()):\n // without this, the failed `_detector` stays wired to `_onChange` and a\n // later `change` on that same (never-truly-started) instance would\n // still write state, contradicting the error just recorded.\n this._detector?.removeEventListener(\"change\", this._onChange);\n this._detector = null;\n this._abortController = null;\n this._setError({ error: e });\n }\n }\n\n /** Stop the current session (if any) and detach its listener. Safe to call when not started. */\n stop(): void {\n this._gen++;\n this._abortController?.abort();\n this._abortController = null;\n if (this._detector) {\n this._detector.removeEventListener(\"change\", this._onChange);\n this._detector = null;\n }\n }\n\n private _onChange = (event: Event): void => {\n const detector = event.target as IdleDetectorLike;\n this._setState(detector.userState, detector.screenState);\n };\n}\n","import { IdleScreenState, IdleUserState, IWcBindable } from \"../types.js\";\nimport { IdleCore } from \"../core/IdleCore.js\";\n\n/**\n * `<wcs-idle>` — declarative Idle Detection API primitive.\n *\n * Does NOT auto-start on connect (docs/idle-detection-tag-design.md §6): the\n * permission gate sits in front of `start()`, so an unconditional\n * connectedCallback start would be guaranteed to fail before permission is\n * granted. Callers drive `requestPermission()` → `start()` explicitly, e.g.\n * from a click handler.\n *\n * Compose with `<wcs-permission name=\"idle-detection\">` for prompt/granted/\n * denied status — this Shell only exposes the actual idle state.\n */\nexport class WcsIdle extends HTMLElement {\n static hasConnectedCallbackPromise = true;\n\n static wcBindable: IWcBindable = {\n ...IdleCore.wcBindable,\n inputs: [\n { name: \"threshold\", attribute: \"threshold\" },\n ],\n // Core の commands をそのまま継承(単一情報源)。\n commands: IdleCore.wcBindable.commands,\n };\n\n private _core: IdleCore;\n private _connectedCallbackPromise: Promise<void> = Promise.resolve();\n\n constructor() {\n super();\n this._core = new IdleCore(this);\n }\n\n // --- Attribute accessors ---\n\n /**\n * Minimum idle time (ms) before `userState` becomes `\"idle\"`. This value is\n * read only at `start()` time — there is no `attributeChangedCallback`\n * (deliberately not declared in `observedAttributes`, mirroring\n * `<wcs-gyroscope>`'s `frequency`), so mutating the attribute/property on an\n * already-running session has no effect until the caller `stop()`s and\n * `start()`s again.\n */\n get threshold(): number {\n const attr = this.getAttribute(\"threshold\");\n // An absent, empty, or whitespace-only attribute all mean \"no value\n // supplied\" and must fall back to the default — without this check,\n // `Number(\"\")`/`Number(\" \")` coerce to `0` (finite), which would slip\n // past the `Number.isFinite` fallback below and silently return `0`\n // instead of the documented 60000ms default.\n if (attr === null || attr.trim() === \"\") return 60000;\n const n = Number(attr);\n return Number.isFinite(n) ? n : 60000;\n }\n\n set threshold(value: number) {\n this.setAttribute(\"threshold\", String(value));\n }\n\n // --- Core delegated getters ---\n\n get userState(): IdleUserState | null {\n return this._core.userState;\n }\n\n get screenState(): IdleScreenState | null {\n return this._core.screenState;\n }\n\n get active(): boolean {\n return this._core.active;\n }\n\n get error(): any {\n return this._core.error;\n }\n\n get connectedCallbackPromise(): Promise<void> {\n return this._connectedCallbackPromise;\n }\n\n // --- Commands (delegated to Core) ---\n\n requestPermission(): Promise<\"granted\" | \"denied\"> {\n return this._core.requestPermission();\n }\n\n start(threshold?: number): Promise<void> {\n return this._core.start(threshold ?? this.threshold);\n }\n\n stop(): void {\n this._core.stop();\n }\n\n // --- Lifecycle ---\n\n connectedCallback(): void {\n this.style.display = \"none\";\n // No auto-start (§6) — observe() is a synchronous no-op, kept only for\n // API uniformity with other IO nodes' lifecycle.\n this._connectedCallbackPromise = this._core.observe();\n }\n\n disconnectedCallback(): void {\n this._core.dispose();\n }\n}\n","import { setConfig } from \"./config.js\";\nimport { registerComponents } from \"./registerComponents.js\";\nimport { IWritableConfig } from \"./types.js\";\n\nexport function bootstrapIdle(userConfig?: IWritableConfig): void {\n if (userConfig) {\n setConfig(userConfig);\n }\n registerComponents();\n}\n","import { WcsIdle } from \"./components/Idle.js\";\nimport { config } from \"./config.js\";\n\nexport function registerComponents(): void {\n if (!customElements.get(config.tagNames.idle)) {\n customElements.define(config.tagNames.idle, WcsIdle);\n }\n}\n"],"names":["_config","tagNames","idle","deepFreeze","obj","Object","freeze","key","keys","deepClone","clone","frozenConfig","config","getConfig","IdleCore","EventTarget","static","protocol","version","properties","name","event","getter","e","detail","userState","screenState","commands","async","_target","_userState","_screenState","_error","_detector","_abortController","_gen","_ready","Promise","resolve","constructor","target","super","this","ready","active","error","observe","dispose","stop","_api","g","globalThis","IdleDetector","undefined","_setState","dispatchEvent","CustomEvent","bubbles","_setError","requestPermission","Ctor","message","result","start","threshold","ac","AbortController","gen","detector","addEventListener","_onChange","signal","removeEventListener","abort","WcsIdle","HTMLElement","wcBindable","inputs","attribute","_core","_connectedCallbackPromise","attr","getAttribute","trim","n","Number","isFinite","value","setAttribute","String","connectedCallbackPromise","connectedCallback","style","display","disconnectedCallback","bootstrapIdle","userConfig","partialConfig","assign","customElements","get","define"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@wcstack/idle",
3
+ "version": "1.16.0",
4
+ "description": "Declarative Idle Detection component for Web Components. Framework-agnostic IdleDetector wrapper via wc-bindable-protocol.",
5
+ "type": "module",
6
+ "main": "./dist/index.esm.js",
7
+ "module": "./dist/index.esm.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.esm.js"
13
+ },
14
+ "./auto": "./dist/auto.min.js"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "clean": "rimraf dist .tsc-out",
21
+ "build": "rimraf dist .tsc-out && tsc && rollup -c",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "test:coverage": "vitest run --coverage",
25
+ "lint": "eslint src",
26
+ "version:patch": "npm version patch",
27
+ "version:minor": "npm version minor",
28
+ "version:major": "npm version major",
29
+ "prepublishOnly": "npm run build && npm run test:coverage"
30
+ },
31
+ "keywords": [
32
+ "web-components",
33
+ "idle-detection",
34
+ "presence",
35
+ "custom-elements",
36
+ "wc-bindable",
37
+ "declarative",
38
+ "zero-dependencies",
39
+ "framework-agnostic"
40
+ ],
41
+ "author": "mogera551",
42
+ "homepage": "https://wcstack.github.io",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/wcstack/wcstack.git",
46
+ "directory": "packages/idle"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/wcstack/wcstack/issues"
50
+ },
51
+ "license": "MIT",
52
+ "devDependencies": {
53
+ "@eslint/js": "^9.39.1",
54
+ "@rollup/plugin-terser": "^0.4.4",
55
+ "@rollup/plugin-typescript": "^11.1.6",
56
+ "@vitest/coverage-v8": "^4.0.15",
57
+ "@vitest/ui": "^4.0.15",
58
+ "eslint": "^9.39.1",
59
+ "globals": "^16.5.0",
60
+ "happy-dom": "^20.0.11",
61
+ "rimraf": "^6.0.1",
62
+ "rollup": "^4.22.4",
63
+ "rollup-plugin-dts": "^6.1.1",
64
+ "rollup-plugin-copy": "^3.5.0",
65
+ "tslib": "^2.8.1",
66
+ "typescript": "^5.9.3",
67
+ "typescript-eslint": "^8.49.0",
68
+ "vitest": "^4.0.15"
69
+ }
70
+ }