@wcstack/media-query 2.1.1 → 2.2.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 CHANGED
@@ -1,218 +1,218 @@
1
- # @wcstack/media-query
2
-
3
- > 🤖 **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
4
-
5
- `@wcstack/media-query` は wcstack エコシステム向けのヘッドレスな `matchMedia` コンポーネントです。
6
-
7
- 視覚的な UI ウィジェットではありません。
8
- `@wcstack/network` が回線品質シグナルをリアクティブな state に変えるのと同じように、CSS メディアクエリの真偽をリアクティブな state に変える **非同期プリミティブノード** です。
9
-
10
- `@wcstack/state` と組み合わせると、`<wcs-media-query>` はパス契約で直接バインドできます:
11
-
12
- - **入力サーフェス**: `query` — `query` 属性にミラーされるメディアクエリ文字列
13
- - **出力 state サーフェス**: `matched`、`media`、`supported`
14
-
15
- これにより「ダークモードか」「reduced-motion を望んでいるか」「ビューポートが 600px 未満か」が state 上の素の boolean になり、`data-wcs` の条件分岐・computed getter・他の I/O ノードから、UI 層で `matchMedia` や `change` リスナーの配線を書かずに使えます。
16
-
17
- `@wcstack/media-query` は [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md)(Core / Shell / Binding Contract)アーキテクチャに従います:
18
-
19
- - **Core**(`MediaQueryCore`)が `matchMedia(query)` を呼び、リストの live な `change` イベントを追従
20
- - **Shell**(`<wcs-media-query>`)がその state を DOM ライフサイクルに接続し、`query` 変更時に購読を張り替える
21
- - **Binding Contract**(`static wcBindable`)が観測可能な `properties` と 1 つの `input`(`query`)を宣言(**コマンドは持たない**)
22
-
23
- ## なぜ存在するか — CSS には `@media` があるが、state には無い
24
-
25
- *スタイル*だけを切り替えるメディアクエリはスタイルシートに書くべきです。このノードは、答えが**ロジック**に届く必要がある場面のためにあります: テーマの既定値を決める、`prefers-reduced-motion` で `<wcs-raf>` のループを止める、ブレークポイント未満でテーブルをカードリストに差し替える、`(display-mode: standalone)` で PWA としてのインストールを検知する。いずれも手書きなら 4 行の命令的配線(`matchMedia` → `addEventListener("change")` → 初期同期 → 後始末)が要りますが、ここでは他の wcstack I/O ノードと同じ骨格を持つ 1 タグです。
26
-
27
- > **`matches` ではなく `matched`。** プラットフォームのプロパティは `MediaQueryList.matches` ですが、`Element.prototype.matches(selector)` が全要素に既に存在し、wc-bindable のプロパティは Shell から直接読まれるため、DOM メソッドを潰さないよう出力名を `matched` にしています。`docs/media-query-tag-design.md` §2.1 参照。
28
-
29
- > **secure context 不要・権限不要。** `matchMedia` はあらゆるページで使えます。
30
-
31
- ## インストール
32
-
33
- ```bash
34
- npm install @wcstack/media-query
35
- ```
36
-
37
- CDN(バージョン固定): `https://esm.run/@wcstack/media-query@2.1.1/auto`
38
-
39
- ## クイックスタート
40
-
41
- ### 1. テーマのダークモード既定値
42
-
43
- ```html
44
- <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
45
- <script type="module" src="https://esm.run/@wcstack/media-query/auto"></script>
46
-
47
- <wcs-state>
48
- <script type="module">
49
- export default {
50
- isDark: false,
51
- get theme() {
52
- return this.isDark ? "dark" : "light";
53
- },
54
- };
55
- </script>
56
- </wcs-state>
57
-
58
- <wcs-media-query query="(prefers-color-scheme: dark)" data-wcs="matched: isDark"></wcs-media-query>
59
-
60
- <main data-wcs="attr.data-theme: theme">…</main>
61
- ```
62
-
63
- このページの全例に共通するタイミング規則が 1 つあります: `<wcs-media-query>` はスナップショットを `wcs-media-query:change` イベントで公開しますが、*初回*のスナップショットは接続時に同期発火するため、`@wcstack/state` がバインドリスナーを張るより先に流れてしまいます。それでも初期値が届くのは、`<wcs-media-query>` の観測可能プロパティがすべて output-only(`properties` にのみ宣言され `inputs` に無い)だからです — 既定の binding authority が `element` になり、バインド確立時に**イベントを待たずプロパティを直接読みます**(directional initial sync、v1.21.0 以降は既定 ON)。手動 pull は不要です(「注意・制限」参照)。
64
-
65
- ### 2. `<wcs-raf>` のループで `prefers-reduced-motion` を尊重する
66
-
67
- ```html
68
- <wcs-state>
69
- <script type="module">
70
- export default {
71
- reduceMotion: false,
72
- frame: 0,
73
- };
74
- </script>
75
- </wcs-state>
76
-
77
- <wcs-media-query query="(prefers-reduced-motion: reduce)" data-wcs="matched: reduceMotion"></wcs-media-query>
78
- <wcs-raf data-wcs="tick: frame; command.pause: reduceMotion|truthy; command.resume: reduceMotion|not" manual></wcs-raf>
79
- ```
80
-
81
- (`<wcs-raf>` にはまさにこの用途の `reduced-motion="pause"` 属性もあります。この例は一般形 — 任意の I/O ノードのコマンドをメディアクエリから駆動できる — を示すものです。)
82
-
83
- ### 3. ブレークポイントでのレイアウト切り替え
84
-
85
- ```html
86
- <wcs-state>
87
- <script type="module">
88
- export default {
89
- narrow: false,
90
- rows: [],
91
- };
92
- </script>
93
- </wcs-state>
94
-
95
- <wcs-media-query query="(max-width: 600px)" data-wcs="matched: narrow"></wcs-media-query>
96
-
97
- <template data-wcs="if: narrow">
98
- <ul data-wcs="for: rows"><li data-wcs="textContent: rows.*.name"></li></ul>
99
- </template>
100
- <template data-wcs="if: narrow|not">
101
- <table>…</table>
102
- </template>
103
- ```
104
-
105
- バインドする state パスは必ず事前に宣言してください — 未宣言パスへのバインドは初期化時に例外になります。`matched` は厳密な boolean(`null` になり得ない)なので `|not` が安全です。
106
-
107
- ## 属性 / 入力
108
-
109
- | 属性 | プロパティ | 説明 |
110
- | -------- | ---------- | ---- |
111
- | `query` | `query` | `matchMedia()` に渡すメディアクエリ文字列。接続中に変更すると旧 `MediaQueryList` の購読を解除して新しいリストを購読します。属性を除去すると「何も監視しない」となり `matched` は `false` に落ちます。不正なクエリでも throw しません(ブラウザは `media: "not all"`、`matched: false` を報告)。 |
112
-
113
- `query` は唯一の入力で、`wcBindable.inputs` に `attribute: "query"` で宣言されています。upgrade 前のプロパティ代入は接続時に取り込まれます(property upgrade)。
114
-
115
- ## 観測可能プロパティ(出力)
116
-
117
- | プロパティ | イベント | semantics | 説明 |
118
- | ------------ | ------------------------- | --------- | ---- |
119
- | `matched` | `wcs-media-query:change` | `state` | `MediaQueryList.matches`。live なリストが無いとき(非対応・空 `query`・`matchMedia` が throw)は `false`。 |
120
- | `media` | `wcs-media-query:change` | `state` | ブラウザが正規化した `MediaQueryList.media` 文字列(不正クエリは `"not all"`)。リストが無ければ `""`。 |
121
- | `supported` | `wcs-media-query:change` | `state` | この環境で `matchMedia` が関数なら `true`。購読のたびに解決(コンストラクタでキャッシュしない)。 |
122
-
123
- 3 つすべては単一の `wcs-media-query:change` イベント(スナップショット全体 `{ matched, media, supported }`)から派生します。query 変更で `media` と `matched` が同時に変わる場合も、1 つの整合した更新として届きます。値はプリミティブのみで、解放すべきライブハンドルや所有オブジェクトはありません。
124
-
125
- ## コマンド
126
-
127
- **無し。** `MediaQueryList` には呼ぶべきアクションがありません。`<wcs-media-query>` は純粋なモニタです。
128
-
129
- ## 注意・制限
130
-
131
- - **1 タグ 1 クエリ。** 複数のクエリは `<wcs-media-query>` を並べてください。`queries` 配列は全ノード共通の「1 イベント+派生 getter」の形を崩します。
132
- - **初回スナップショットの*イベント*はバインドに届きませんが、値は届きます。** 最初の `wcs-media-query:change` は `connectedCallback` 中に同期発火し、`@wcstack/state` のバインドリスナー確立はそれより後です。イベントは後から購読した相手に再送されません。それでも初期値が失われないのは、本ノードの観測可能プロパティがすべて output-only で既定の binding authority が `element` になるためです: バインドは確立時にプロパティを直接読みます(directional initial sync)。`enableDirectionalInitialSync: false` に倒した構成でのみ `$connectedCallback` + `whenDefined` の手動 pull が必要です。
133
- - **世代ガード。** 購読自体は同期ですが、query の変更は購読を*置き換え*ます。各購読の `change` リスナーは世代を捕捉し、新しい購読ができた後のイベントを無視するため、`removeEventListener` が効かない `MediaQueryList` でも古い query の値が新しい query の値を上書きすることはありません。`docs/media-query-tag-design.md` §6。
134
- - **旧 Safari。** リストに `addEventListener` が無ければ非推奨の `addListener` / `removeListener` ペアを使い、どちらも無ければ購読時点のスナップショットだけを報告します。
135
- - **再接続で再購読。** 要素を取り外すとリスナーを解除し、再挿入時に(その時点の `query` で)再確立します。
136
- - **SSR(`@wcstack/server`)。** `static hasConnectedCallbackPromise = true` を宣言し `connectedCallbackPromise` を公開しますが、`observe()` が同期的なため常に即座に settle します。`matchMedia` の無い環境(Node)では `supported` と `matched` は `false`、`media` は `""` です。
137
- - **同値ガード。** フィールド単位の比較で冗長な dispatch を抑止します — 旧 `addListener` の二重発火や、ブラウザが同じ `media` に正規化する等価クエリへの再購読など。
138
-
139
- ## `:state()` による CSS スタイリング
140
-
141
- `<wcs-media-query>` は 2 つの boolean 出力ステートを
142
- [`ElementInternals` の `CustomStateSet`](https://developer.mozilla.org/ja/docs/Web/API/CustomStateSet)
143
- に反映します。そのため `data-wcs` バインディングやクラスの手動トグルなしに、CSS の
144
- `:state()` 疑似クラスで直接スタイリングできます。
145
-
146
- | ステート | on になる条件 |
147
- |----------|----------------|
148
- | `matched` | `wcs-media-query:change` が `matched === true` で発火 |
149
- | `supported` | `wcs-media-query:change` が `supported === true` で発火 |
150
-
151
- `media` は文字列なので反映されません。
152
-
153
- ```css
154
- /* JS 配線なしの兄弟要素駆動テーマ */
155
- wcs-media-query:state(matched) ~ main { color-scheme: dark; }
156
- body:has(wcs-media-query:not(:state(supported))) .needs-js-media { display: none; }
157
- ```
158
-
159
- 属性やクラスと異なり `:state()` は要素の外部から書き込めないため、この出力ステートが
160
- 入力と混同される心配がありません。
161
-
162
- **対応ブラウザ**(新構文 `:state(x)`): Chrome/Edge 125+、Safari 17.4+、Firefox 126+。
163
- 非対応の環境ではステートが一切 set されないだけです — `:state()` セレクタがマッチしなく
164
- なりますが、`<wcs-media-query>` 自体は通常どおり動作し続けます(graceful degradation・never-throw)。
165
-
166
- **SSR:** `:state()` は HTML にシリアライズできないため、サーバーレンダリングされた
167
- マークアップの初期ペイントにはこれらのステートは乗りません(`@wcstack/server` は無改変)。
168
- ハイドレーション前の見た目を制御したい場合は、代わりに `wcs-media-query:not(:defined)` と組み合わせてください。
169
-
170
- ### デバッグ
171
-
172
- カスタムステートは DevTools の Elements パネルには表示されず、`attachInternals()`
173
- は同一要素に 2 回呼べないため、コンソールから直接覗く手段がありません。そのための
174
- デバッグ専用の補助を 2 つ用意しています:
175
-
176
- - `el.debugStates` — 現在 on になっているステート名の**スナップショット**配列
177
- (例: `["matched", "supported"]`)。`wc-bindable` の一部ではなく(バインド対象ではない)、
178
- 形状も契約として保証されません — デバッグ用途にのみ使ってください。
179
- - `debug-states` 属性(opt-in・既定 OFF)は、ステート変化を要素の
180
- `data-wcs-state-matched` / `data-wcs-state-supported` 属性にミラーします。
181
- Elements パネルを開いておけば、トグルのたびにハイライトされます:
182
-
183
- ```html
184
- <wcs-media-query query="(max-width: 600px)" debug-states></wcs-media-query>
185
- ```
186
-
187
- **CSS は `data-wcs-state-*` ではなく `:state()` に書いてください。** ミラーされた
188
- 属性は、DevTools を開いた状態でステート変化を可視化するためだけのものであり、
189
- スタイリング用の正式なフックではありません。
190
-
191
- ## ヘッドレス利用(`MediaQueryCore`)
192
-
193
- Core は DOM 非依存で、`@wc-bindable/core` の `bind()` と直接使えます:
194
-
195
- ```typescript
196
- import { MediaQueryCore } from "@wcstack/media-query";
197
-
198
- const mq = new MediaQueryCore();
199
- mq.addEventListener("wcs-media-query:change", (e) => {
200
- console.log((e as CustomEvent).detail); // { matched, media, supported }
201
- });
202
-
203
- mq.observe("(prefers-color-scheme: dark)"); // 同期的 — データ取得に promise を待つ必要は無い
204
- console.log(mq.matched);
205
-
206
- mq.observe("(max-width: 600px)"); // query 切替: 旧リストを解放し新リストを購読
207
-
208
- // 後始末:
209
- mq.dispose(); // live な `change` リスナーを外す
210
- ```
211
-
212
- コンストラクタ: `new MediaQueryCore(target?, { matchMedia? })`。`target` はイベントの dispatch 先 `EventTarget`(省略時は Core 自身)、`matchMedia` は `globalThis.matchMedia` を呼び出し時に解決する代わりに使う関数の注入(テストや window の無いホスト向け)。ライフサイクルは手動です: `observe(query)` / `dispose()`。
213
-
214
- Core の構造サーフェスは wcstack I/O ノード横断の規範です([async-io-node-guidelines §3.9](../../docs/async-io-node-guidelines.ja.md))。要素なしで signals に束縛するには [@wcstack/signals — Core を直接束縛する](../signals/README.ja.md#core-を直接束縛する要素なし) を参照。
215
-
216
- ## ライセンス
217
-
218
- MIT
1
+ # @wcstack/media-query
2
+
3
+ > 🤖 **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
4
+
5
+ `@wcstack/media-query` は wcstack エコシステム向けのヘッドレスな `matchMedia` コンポーネントです。
6
+
7
+ 視覚的な UI ウィジェットではありません。
8
+ `@wcstack/network` が回線品質シグナルをリアクティブな state に変えるのと同じように、CSS メディアクエリの真偽をリアクティブな state に変える **非同期プリミティブノード** です。
9
+
10
+ `@wcstack/state` と組み合わせると、`<wcs-media-query>` はパス契約で直接バインドできます:
11
+
12
+ - **入力サーフェス**: `query` — `query` 属性にミラーされるメディアクエリ文字列
13
+ - **出力 state サーフェス**: `matched`、`media`、`supported`
14
+
15
+ これにより「ダークモードか」「reduced-motion を望んでいるか」「ビューポートが 600px 未満か」が state 上の素の boolean になり、`data-wcs` の条件分岐・computed getter・他の I/O ノードから、UI 層で `matchMedia` や `change` リスナーの配線を書かずに使えます。
16
+
17
+ `@wcstack/media-query` は [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md)(Core / Shell / Binding Contract)アーキテクチャに従います:
18
+
19
+ - **Core**(`MediaQueryCore`)が `matchMedia(query)` を呼び、リストの live な `change` イベントを追従
20
+ - **Shell**(`<wcs-media-query>`)がその state を DOM ライフサイクルに接続し、`query` 変更時に購読を張り替える
21
+ - **Binding Contract**(`static wcBindable`)が観測可能な `properties` と 1 つの `input`(`query`)を宣言(**コマンドは持たない**)
22
+
23
+ ## なぜ存在するか — CSS には `@media` があるが、state には無い
24
+
25
+ *スタイル*だけを切り替えるメディアクエリはスタイルシートに書くべきです。このノードは、答えが**ロジック**に届く必要がある場面のためにあります: テーマの既定値を決める、`prefers-reduced-motion` で `<wcs-raf>` のループを止める、ブレークポイント未満でテーブルをカードリストに差し替える、`(display-mode: standalone)` で PWA としてのインストールを検知する。いずれも手書きなら 4 行の命令的配線(`matchMedia` → `addEventListener("change")` → 初期同期 → 後始末)が要りますが、ここでは他の wcstack I/O ノードと同じ骨格を持つ 1 タグです。
26
+
27
+ > **`matches` ではなく `matched`。** プラットフォームのプロパティは `MediaQueryList.matches` ですが、`Element.prototype.matches(selector)` が全要素に既に存在し、wc-bindable のプロパティは Shell から直接読まれるため、DOM メソッドを潰さないよう出力名を `matched` にしています。`docs/media-query-tag-design.md` §2.1 参照。
28
+
29
+ > **secure context 不要・権限不要。** `matchMedia` はあらゆるページで使えます。
30
+
31
+ ## インストール
32
+
33
+ ```bash
34
+ npm install @wcstack/media-query
35
+ ```
36
+
37
+ CDN(バージョン固定): `https://esm.run/@wcstack/media-query@2.2.0/auto`
38
+
39
+ ## クイックスタート
40
+
41
+ ### 1. テーマのダークモード既定値
42
+
43
+ ```html
44
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
45
+ <script type="module" src="https://esm.run/@wcstack/media-query/auto"></script>
46
+
47
+ <wcs-state>
48
+ <script type="module">
49
+ export default {
50
+ isDark: false,
51
+ get theme() {
52
+ return this.isDark ? "dark" : "light";
53
+ },
54
+ };
55
+ </script>
56
+ </wcs-state>
57
+
58
+ <wcs-media-query query="(prefers-color-scheme: dark)" data-wcs="matched: isDark"></wcs-media-query>
59
+
60
+ <main data-wcs="attr.data-theme: theme">…</main>
61
+ ```
62
+
63
+ このページの全例に共通するタイミング規則が 1 つあります: `<wcs-media-query>` はスナップショットを `wcs-media-query:change` イベントで公開しますが、*初回*のスナップショットは接続時に同期発火するため、`@wcstack/state` がバインドリスナーを張るより先に流れてしまいます。それでも初期値が届くのは、`<wcs-media-query>` の観測可能プロパティがすべて output-only(`properties` にのみ宣言され `inputs` に無い)だからです — 既定の binding authority が `element` になり、バインド確立時に**イベントを待たずプロパティを直接読みます**(directional initial sync、v1.21.0 以降は既定 ON)。手動 pull は不要です(「注意・制限」参照)。
64
+
65
+ ### 2. `<wcs-raf>` のループで `prefers-reduced-motion` を尊重する
66
+
67
+ ```html
68
+ <wcs-state>
69
+ <script type="module">
70
+ export default {
71
+ reduceMotion: false,
72
+ frame: 0,
73
+ };
74
+ </script>
75
+ </wcs-state>
76
+
77
+ <wcs-media-query query="(prefers-reduced-motion: reduce)" data-wcs="matched: reduceMotion"></wcs-media-query>
78
+ <wcs-raf data-wcs="tick: frame; command.pause: reduceMotion|truthy; command.resume: reduceMotion|not" manual></wcs-raf>
79
+ ```
80
+
81
+ (`<wcs-raf>` にはまさにこの用途の `reduced-motion="pause"` 属性もあります。この例は一般形 — 任意の I/O ノードのコマンドをメディアクエリから駆動できる — を示すものです。)
82
+
83
+ ### 3. ブレークポイントでのレイアウト切り替え
84
+
85
+ ```html
86
+ <wcs-state>
87
+ <script type="module">
88
+ export default {
89
+ narrow: false,
90
+ rows: [],
91
+ };
92
+ </script>
93
+ </wcs-state>
94
+
95
+ <wcs-media-query query="(max-width: 600px)" data-wcs="matched: narrow"></wcs-media-query>
96
+
97
+ <template data-wcs="if: narrow">
98
+ <ul data-wcs="for: rows"><li data-wcs="textContent: rows.*.name"></li></ul>
99
+ </template>
100
+ <template data-wcs="if: narrow|not">
101
+ <table>…</table>
102
+ </template>
103
+ ```
104
+
105
+ バインドする state パスは必ず事前に宣言してください — 未宣言パスへのバインドは初期化時に例外になります。`matched` は厳密な boolean(`null` になり得ない)なので `|not` が安全です。
106
+
107
+ ## 属性 / 入力
108
+
109
+ | 属性 | プロパティ | 説明 |
110
+ | -------- | ---------- | ---- |
111
+ | `query` | `query` | `matchMedia()` に渡すメディアクエリ文字列。接続中に変更すると旧 `MediaQueryList` の購読を解除して新しいリストを購読します。属性を除去すると「何も監視しない」となり `matched` は `false` に落ちます。不正なクエリでも throw しません(ブラウザは `media: "not all"`、`matched: false` を報告)。 |
112
+
113
+ `query` は唯一の入力で、`wcBindable.inputs` に `attribute: "query"` で宣言されています。upgrade 前のプロパティ代入は接続時に取り込まれます(property upgrade)。
114
+
115
+ ## 観測可能プロパティ(出力)
116
+
117
+ | プロパティ | イベント | semantics | 説明 |
118
+ | ------------ | ------------------------- | --------- | ---- |
119
+ | `matched` | `wcs-media-query:change` | `state` | `MediaQueryList.matches`。live なリストが無いとき(非対応・空 `query`・`matchMedia` が throw)は `false`。 |
120
+ | `media` | `wcs-media-query:change` | `state` | ブラウザが正規化した `MediaQueryList.media` 文字列(不正クエリは `"not all"`)。リストが無ければ `""`。 |
121
+ | `supported` | `wcs-media-query:change` | `state` | この環境で `matchMedia` が関数なら `true`。購読のたびに解決(コンストラクタでキャッシュしない)。 |
122
+
123
+ 3 つすべては単一の `wcs-media-query:change` イベント(スナップショット全体 `{ matched, media, supported }`)から派生します。query 変更で `media` と `matched` が同時に変わる場合も、1 つの整合した更新として届きます。値はプリミティブのみで、解放すべきライブハンドルや所有オブジェクトはありません。
124
+
125
+ ## コマンド
126
+
127
+ **無し。** `MediaQueryList` には呼ぶべきアクションがありません。`<wcs-media-query>` は純粋なモニタです。
128
+
129
+ ## 注意・制限
130
+
131
+ - **1 タグ 1 クエリ。** 複数のクエリは `<wcs-media-query>` を並べてください。`queries` 配列は全ノード共通の「1 イベント+派生 getter」の形を崩します。
132
+ - **初回スナップショットの*イベント*はバインドに届きませんが、値は届きます。** 最初の `wcs-media-query:change` は `connectedCallback` 中に同期発火し、`@wcstack/state` のバインドリスナー確立はそれより後です。イベントは後から購読した相手に再送されません。それでも初期値が失われないのは、本ノードの観測可能プロパティがすべて output-only で既定の binding authority が `element` になるためです: バインドは確立時にプロパティを直接読みます(directional initial sync)。`enableDirectionalInitialSync: false` に倒した構成でのみ `$connectedCallback` + `whenDefined` の手動 pull が必要です。
133
+ - **世代ガード。** 購読自体は同期ですが、query の変更は購読を*置き換え*ます。各購読の `change` リスナーは世代を捕捉し、新しい購読ができた後のイベントを無視するため、`removeEventListener` が効かない `MediaQueryList` でも古い query の値が新しい query の値を上書きすることはありません。`docs/media-query-tag-design.md` §6。
134
+ - **旧 Safari。** リストに `addEventListener` が無ければ非推奨の `addListener` / `removeListener` ペアを使い、どちらも無ければ購読時点のスナップショットだけを報告します。
135
+ - **再接続で再購読。** 要素を取り外すとリスナーを解除し、再挿入時に(その時点の `query` で)再確立します。
136
+ - **SSR(`@wcstack/server`)。** `static hasConnectedCallbackPromise = true` を宣言し `connectedCallbackPromise` を公開しますが、`observe()` が同期的なため常に即座に settle します。`matchMedia` の無い環境(Node)では `supported` と `matched` は `false`、`media` は `""` です。
137
+ - **同値ガード。** フィールド単位の比較で冗長な dispatch を抑止します — 旧 `addListener` の二重発火や、ブラウザが同じ `media` に正規化する等価クエリへの再購読など。
138
+
139
+ ## `:state()` による CSS スタイリング
140
+
141
+ `<wcs-media-query>` は 2 つの boolean 出力ステートを
142
+ [`ElementInternals` の `CustomStateSet`](https://developer.mozilla.org/ja/docs/Web/API/CustomStateSet)
143
+ に反映します。そのため `data-wcs` バインディングやクラスの手動トグルなしに、CSS の
144
+ `:state()` 疑似クラスで直接スタイリングできます。
145
+
146
+ | ステート | on になる条件 |
147
+ |----------|----------------|
148
+ | `matched` | `wcs-media-query:change` が `matched === true` で発火 |
149
+ | `supported` | `wcs-media-query:change` が `supported === true` で発火 |
150
+
151
+ `media` は文字列なので反映されません。
152
+
153
+ ```css
154
+ /* JS 配線なしの兄弟要素駆動テーマ */
155
+ wcs-media-query:state(matched) ~ main { color-scheme: dark; }
156
+ body:has(wcs-media-query:not(:state(supported))) .needs-js-media { display: none; }
157
+ ```
158
+
159
+ 属性やクラスと異なり `:state()` は要素の外部から書き込めないため、この出力ステートが
160
+ 入力と混同される心配がありません。
161
+
162
+ **対応ブラウザ**(新構文 `:state(x)`): Chrome/Edge 125+、Safari 17.4+、Firefox 126+。
163
+ 非対応の環境ではステートが一切 set されないだけです — `:state()` セレクタがマッチしなく
164
+ なりますが、`<wcs-media-query>` 自体は通常どおり動作し続けます(graceful degradation・never-throw)。
165
+
166
+ **SSR:** `:state()` は HTML にシリアライズできないため、サーバーレンダリングされた
167
+ マークアップの初期ペイントにはこれらのステートは乗りません(`@wcstack/server` は無改変)。
168
+ ハイドレーション前の見た目を制御したい場合は、代わりに `wcs-media-query:not(:defined)` と組み合わせてください。
169
+
170
+ ### デバッグ
171
+
172
+ カスタムステートは DevTools の Elements パネルには表示されず、`attachInternals()`
173
+ は同一要素に 2 回呼べないため、コンソールから直接覗く手段がありません。そのための
174
+ デバッグ専用の補助を 2 つ用意しています:
175
+
176
+ - `el.debugStates` — 現在 on になっているステート名の**スナップショット**配列
177
+ (例: `["matched", "supported"]`)。`wc-bindable` の一部ではなく(バインド対象ではない)、
178
+ 形状も契約として保証されません — デバッグ用途にのみ使ってください。
179
+ - `debug-states` 属性(opt-in・既定 OFF)は、ステート変化を要素の
180
+ `data-wcs-state-matched` / `data-wcs-state-supported` 属性にミラーします。
181
+ Elements パネルを開いておけば、トグルのたびにハイライトされます:
182
+
183
+ ```html
184
+ <wcs-media-query query="(max-width: 600px)" debug-states></wcs-media-query>
185
+ ```
186
+
187
+ **CSS は `data-wcs-state-*` ではなく `:state()` に書いてください。** ミラーされた
188
+ 属性は、DevTools を開いた状態でステート変化を可視化するためだけのものであり、
189
+ スタイリング用の正式なフックではありません。
190
+
191
+ ## ヘッドレス利用(`MediaQueryCore`)
192
+
193
+ Core は DOM 非依存で、`@wc-bindable/core` の `bind()` と直接使えます:
194
+
195
+ ```typescript
196
+ import { MediaQueryCore } from "@wcstack/media-query";
197
+
198
+ const mq = new MediaQueryCore();
199
+ mq.addEventListener("wcs-media-query:change", (e) => {
200
+ console.log((e as CustomEvent).detail); // { matched, media, supported }
201
+ });
202
+
203
+ mq.observe("(prefers-color-scheme: dark)"); // 同期的 — データ取得に promise を待つ必要は無い
204
+ console.log(mq.matched);
205
+
206
+ mq.observe("(max-width: 600px)"); // query 切替: 旧リストを解放し新リストを購読
207
+
208
+ // 後始末:
209
+ mq.dispose(); // live な `change` リスナーを外す
210
+ ```
211
+
212
+ コンストラクタ: `new MediaQueryCore(target?, { matchMedia? })`。`target` はイベントの dispatch 先 `EventTarget`(省略時は Core 自身)、`matchMedia` は `globalThis.matchMedia` を呼び出し時に解決する代わりに使う関数の注入(テストや window の無いホスト向け)。ライフサイクルは手動です: `observe(query)` / `dispose()`。
213
+
214
+ Core の構造サーフェスは wcstack I/O ノード横断の規範です([async-io-node-guidelines §3.9](../../docs/async-io-node-guidelines.ja.md))。要素なしで signals に束縛するには [@wcstack/signals — Core を直接束縛する](../signals/README.ja.md#core-を直接束縛する要素なし) を参照。
215
+
216
+ ## ライセンス
217
+
218
+ MIT