@wcstack/media-query 2.1.1

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,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
package/README.md ADDED
@@ -0,0 +1,220 @@
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` is a headless `matchMedia` component for the wcstack ecosystem.
6
+
7
+ It is not a visual UI widget.
8
+ It is an **async primitive node** that turns a CSS media query into reactive state — the same way `@wcstack/network` turns the connection-quality signal into reactive state.
9
+
10
+ With `@wcstack/state`, `<wcs-media-query>` can be bound directly through path contracts:
11
+
12
+ - **input surface**: `query` — the media query string, mirrored from the `query` attribute
13
+ - **output state surface**: `matched`, `media`, `supported`
14
+
15
+ This means "is the user in dark mode", "does the user prefer reduced motion", "is the viewport narrower than 600px" become plain booleans in state — usable by `data-wcs` conditionals, computed getters, and other I/O nodes — without writing `matchMedia` / `change`-listener glue in your UI layer.
16
+
17
+ `@wcstack/media-query` follows the [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md) (Core / Shell / Binding Contract) architecture:
18
+
19
+ - **Core** (`MediaQueryCore`) calls `matchMedia(query)` and tracks the list's live `change` event
20
+ - **Shell** (`<wcs-media-query>`) connects that state to DOM lifecycle and re-subscribes when `query` changes
21
+ - **Binding Contract** (`static wcBindable`) declares observable `properties`, one `input` (`query`), and **no commands**
22
+
23
+ ## Why this exists — CSS already has `@media`; state does not
24
+
25
+ A media query that only changes *styling* belongs in a stylesheet. This node is for the cases where the answer has to reach **logic**: choosing a default theme value, pausing a `<wcs-raf>` loop under `prefers-reduced-motion`, swapping a table for a card list below a breakpoint, or detecting `(display-mode: standalone)` for a PWA. Each of those is four lines of imperative wiring by hand (`matchMedia` → `addEventListener("change")` → initial sync → cleanup); here it is one tag with the same skeleton as every other wcstack I/O node.
26
+
27
+ > **`matched`, not `matches`.** The platform property is `MediaQueryList.matches`, but `Element.prototype.matches(selector)` already exists on every element and a wc-bindable property is read straight off the Shell — so the output is named `matched` to leave the DOM method intact. See `docs/media-query-tag-design.md` §2.1.
28
+
29
+ > **No secure-context requirement, no permission.** `matchMedia` is available on every page.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install @wcstack/media-query
35
+ ```
36
+
37
+ CDN (pinned): `https://esm.run/@wcstack/media-query@2.1.1/auto`
38
+
39
+ ## Quick Start
40
+
41
+ ### 1. Dark-mode default for a theme
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
+ One timing rule applies to every example on this page: `<wcs-media-query>` publishes its snapshot through `wcs-media-query:change` events, and the *first* snapshot fires synchronously at connect — before `@wcstack/state` has attached its binding listeners. The initial value still arrives, because every observable property on `<wcs-media-query>` is output-only (declared in `properties`, absent from `inputs`): that makes the default binding authority `element`, so the binding **reads the property directly when it attaches** instead of waiting for an event it already missed (directional initial sync, on by default since v1.21.0). No manual pull is needed (see Notes & limitations).
64
+
65
+ ### 2. Respect `prefers-reduced-motion` in a `<wcs-raf>` loop
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>` also has its own `reduced-motion="pause"` attribute for exactly this case; the example shows the general shape — any I/O node's commands can be driven from a media query.)
82
+
83
+ ### 3. Layout switch at a breakpoint
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
+ Every bound state path must be declared up front — binding an undeclared path throws at initialization. `matched` is a strict boolean (never `null`), so `|not` is safe here.
106
+
107
+ ## Attributes / Inputs
108
+
109
+ | Attribute | Property | Description |
110
+ | --------- | -------- | ----------- |
111
+ | `query` | `query` | The media query string passed to `matchMedia()`. Changing it while connected tears down the old `MediaQueryList` subscription and subscribes to the new one. Removing the attribute means "watch nothing" — `matched` drops to `false`. An invalid query does not throw (browsers report `media: "not all"`, `matched: false`). |
112
+
113
+ `query` is the only input, declared in `wcBindable.inputs` with `attribute: "query"`. Property assignment before the element is upgraded is picked up on connect (property upgrade).
114
+
115
+ ## Observable Properties (outputs)
116
+
117
+ | Property | Event | Semantics | Description |
118
+ | ----------- | ----------------------- | --------- | ----------- |
119
+ | `matched` | `wcs-media-query:change` | `state` | `MediaQueryList.matches`. `false` whenever there is no live list (unsupported, empty `query`, or a `matchMedia` call that threw). |
120
+ | `media` | `wcs-media-query:change` | `state` | The browser-normalized `MediaQueryList.media` string (`"not all"` for an invalid query); `""` when there is no list. |
121
+ | `supported` | `wcs-media-query:change` | `state` | `true` when `matchMedia` is a function in this environment, resolved on every subscription (never cached at construction). |
122
+
123
+ All three derive from the single `wcs-media-query:change` event (a full snapshot `{ matched, media, supported }`), so a query change that flips `media` and `matched` together arrives as one consistent update. Values are primitives; there are no live handles or owned objects to release.
124
+
125
+ ## Commands
126
+
127
+ **None.** A `MediaQueryList` has no action to invoke. `<wcs-media-query>` is a pure monitor.
128
+
129
+ ## Notes & limitations
130
+
131
+ - **One tag, one query.** Compose several `<wcs-media-query>` elements for several queries; a `queries` array would break the "one event plus derived getters" shape every node shares.
132
+ - **The initial snapshot *event* misses bindings, but the value still arrives.** The first `wcs-media-query:change` fires synchronously during `connectedCallback` — before `@wcstack/state` attaches its binding listeners — and events are not replayed to late subscribers. The value is not lost, because every observable here is output-only, which makes the default binding authority `element`: the binding reads the property directly when it attaches (directional initial sync). Only with `enableDirectionalInitialSync: false` do you need a manual `$connectedCallback` + `whenDefined` pull.
133
+ - **Generation guard.** Subscribing is synchronous, but a query change *replaces* a subscription. Each subscription's `change` listener captures a generation and ignores events once a newer subscription exists, so a `MediaQueryList` whose `removeEventListener` misbehaves can never write the old query's value over the new one's. See `docs/media-query-tag-design.md` §6.
134
+ - **Old Safari.** If the list lacks `addEventListener`, the deprecated `addListener` / `removeListener` pair is used; if it has neither, only the snapshot taken at subscription time is reported.
135
+ - **Reconnect re-subscribes.** Removing and re-inserting the element tears down the listener on disconnect and re-establishes it (for the current `query`) on reconnect.
136
+ - **SSR (`@wcstack/server`).** Declares `static hasConnectedCallbackPromise = true` and exposes `connectedCallbackPromise`; since `observe()` is synchronous this promise settles immediately. Without a `matchMedia` (Node), `supported` and `matched` are `false` and `media` is `""`.
137
+ - **Same-value guard.** A field-by-field comparison suppresses a redundant dispatch — a legacy `addListener` double-fire, or re-subscribing to an equivalent query the browser normalizes to the same `media`.
138
+
139
+ ## CSS styling with `:state()`
140
+
141
+ `<wcs-media-query>` reflects two boolean output states onto its
142
+ [`ElementInternals` `CustomStateSet`](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet),
143
+ so you can style from CSS with the `:state()` pseudo-class — no `data-wcs`
144
+ binding or class toggling required.
145
+
146
+ | State | On when |
147
+ |-------|---------|
148
+ | `matched` | `wcs-media-query:change` fires with `matched === true` |
149
+ | `supported` | `wcs-media-query:change` fires with `supported === true` |
150
+
151
+ `media` is a string and is not reflected.
152
+
153
+ ```css
154
+ /* Sibling-driven theming without JS glue */
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
+ Unlike attributes or classes, `:state()` cannot be written from outside the
160
+ element, so there is no risk of confusing this output state with an input.
161
+
162
+ **Browser support** (`:state(x)` syntax): Chrome/Edge 125+, Safari 17.4+,
163
+ Firefox 126+. In older browsers the states are simply never set — `:state()`
164
+ selectors never match, but `<wcs-media-query>` itself keeps working normally
165
+ (graceful degradation, never-throw).
166
+
167
+ **SSR**: `:state()` cannot be serialized into HTML, so server-rendered markup
168
+ never carries these states on first paint (`@wcstack/server` is unaffected).
169
+ If you need to style the pre-hydration gap, pair your rule with
170
+ `wcs-media-query:not(:defined)` instead.
171
+
172
+ ### Debugging
173
+
174
+ Custom states are invisible in DevTools' Elements panel and `attachInternals()`
175
+ cannot be called twice, so there is no console way to inspect them directly.
176
+ Two debug-only aids are provided for that:
177
+
178
+ - `el.debugStates` — a **snapshot** array of the currently-on state names
179
+ (e.g. `["matched", "supported"]`). It is not part of `wc-bindable` (not a bind
180
+ target) and its shape is not a guaranteed contract — use it for debugging only.
181
+ - The `debug-states` attribute (opt-in, default off) mirrors state changes
182
+ onto `data-wcs-state-matched` / `data-wcs-state-supported` attributes on
183
+ the element, so the Elements panel highlights them as they toggle:
184
+
185
+ ```html
186
+ <wcs-media-query query="(max-width: 600px)" debug-states></wcs-media-query>
187
+ ```
188
+
189
+ **Write your CSS against `:state()`, not `data-wcs-state-*`.** The mirrored
190
+ attributes exist purely to make state changes visible while debugging with
191
+ DevTools open; they are not a supported styling hook.
192
+
193
+ ## Headless usage (`MediaQueryCore`)
194
+
195
+ The Core has no DOM dependency and can be used directly with `bind()` from `@wc-bindable/core`:
196
+
197
+ ```typescript
198
+ import { MediaQueryCore } from "@wcstack/media-query";
199
+
200
+ const mq = new MediaQueryCore();
201
+ mq.addEventListener("wcs-media-query:change", (e) => {
202
+ console.log((e as CustomEvent).detail); // { matched, media, supported }
203
+ });
204
+
205
+ mq.observe("(prefers-color-scheme: dark)"); // synchronous — no promise to await for data
206
+ console.log(mq.matched);
207
+
208
+ mq.observe("(max-width: 600px)"); // switch query: old list released, new one subscribed
209
+
210
+ // later, when done:
211
+ mq.dispose(); // detach the live `change` listener
212
+ ```
213
+
214
+ Constructor: `new MediaQueryCore(target?, { matchMedia? })`. `target` is the `EventTarget` events are dispatched to (the Core itself when omitted); `matchMedia` injects the function to call instead of resolving `globalThis.matchMedia` at call time — useful in tests and non-window hosts. The lifecycle is manual: `observe(query)` / `dispose()`.
215
+
216
+ The structural Core surface is normative across wcstack IO nodes ([async-io-node-guidelines §3.9](../../docs/async-io-node-guidelines.md)); to bind it into signals with no element at all, see [@wcstack/signals — Binding a Core directly](../signals/README.md#binding-a-core-directly-no-element).
217
+
218
+ ## License
219
+
220
+ MIT
@@ -0,0 +1,2 @@
1
+ const e={tagNames:{mediaQuery:"wcs-media-query"}},t=Object.freeze({matched:!1,media:"",supported:!1}),s=Object.freeze({matched:!1,media:"",supported:!0});class n extends EventTarget{static wcBindable={protocol:"wc-bindable",version:1,properties:[{name:"matched",event:"wcs-media-query:change",semantics:"state",getter:e=>e.detail.matched},{name:"media",event:"wcs-media-query:change",semantics:"state",getter:e=>e.detail.media},{name:"supported",event:"wcs-media-query:change",semantics:"state",getter:e=>e.detail.supported}],commands:[]};_target;_snapshot=t;_query="";_unsubscribe=null;_subscribed=!1;_gen=0;_injectedMatchMedia;_ready=Promise.resolve();constructor(e,t){super(),this._target=e??this,this._injectedMatchMedia=t?.matchMedia??null}get ready(){return this._ready}get query(){return this._query}get matched(){return this._snapshot.matched}get media(){return this._snapshot.media}get supported(){return this._snapshot.supported}observe(e=this._query){return this._subscribed&&e===this._query||(this._teardown(),this._query=e,this._subscribed=!0,this._subscribe(e)),this._ready}dispose(){this._subscribed=!1,this._teardown()}_resolveMatchMedia(){if(null!==this._injectedMatchMedia)return this._injectedMatchMedia;const e=globalThis;return"function"==typeof e.matchMedia?t=>e.matchMedia(t):null}_subscribe(e){const n=++this._gen,i=this._resolveMatchMedia();if(null===i)return void this._apply(t);if(""===e)return void this._apply(s);let r=null;try{r=i(e);const t=()=>{n===this._gen&&this._apply(this._read(r))};this._unsubscribe=function(e,t){if("function"==typeof e.addEventListener&&"function"==typeof e.removeEventListener)return e.addEventListener("change",t),()=>e.removeEventListener("change",t);if("function"==typeof e.addListener&&"function"==typeof e.removeListener)return e.addListener(t),()=>e.removeListener(t);return()=>{}}(r,t)}catch{r=null,this._unsubscribe=null}this._apply(null===r?s:this._read(r))}_teardown(){if(this._gen++,null!==this._unsubscribe){const e=this._unsubscribe;this._unsubscribe=null;try{e()}catch{}}}_read(e){return{matched:!0===e.matches,media:"string"==typeof e.media?e.media:"",supported:!0}}_apply(e){const t=this._snapshot;t.matched===e.matched&&t.media===e.media&&t.supported===e.supported||(this._snapshot=e,this._target.dispatchEvent(new CustomEvent("wcs-media-query:change",{detail:e,bubbles:!0})))}}function i(e,t){let s=Object.getPrototypeOf(e);for(;null!==s;){const e=Object.getOwnPropertyDescriptor(s,t);if(void 0!==e)return"function"==typeof e.get||"function"==typeof e.set;s=Object.getPrototypeOf(s)}return!1}class r extends HTMLElement{static hasConnectedCallbackPromise=!0;static wcBindable={...n.wcBindable,inputs:[{name:"query",attribute:"query"}],commands:n.wcBindable.commands};static get observedAttributes(){return["query"]}_core;_connectedCallbackPromise=Promise.resolve();_internals=null;constructor(){super(),this._core=new n(this),this._internals=this._initInternals(),this._wireStates({"wcs-media-query:change":e=>({matched:!0===e.matched,supported:!0===e.supported})})}get debugStates(){return this._internals?[...this._internals.states]:[]}_initInternals(){try{if("function"!=typeof this.attachInternals)return null;const e=this.attachInternals();return e.states.add("wcs-probe"),e.states.delete("wcs-probe"),e}catch{return null}}_wireStates(e){if(null===this._internals)return;const t=this._internals.states;for(const[s,n]of Object.entries(e))this.addEventListener(s,e=>{const s=this.hasAttribute("debug-states");for(const[i,r]of Object.entries(n(e.detail))){try{r?t.add(i):t.delete(i)}catch{}s&&this.toggleAttribute(`data-wcs-state-${i}`,r)}})}get query(){return this.getAttribute("query")??""}set query(e){this.setAttribute("query",e)}get matched(){return this._core.matched}get media(){return this._core.media}get supported(){return this._core.supported}get connectedCallbackPromise(){return this._connectedCallbackPromise}attributeChangedCallback(e,t,s){"query"===e&&this.isConnected&&this._core.observe(s??"")}connectedCallback(){!function(e){const t=e.constructor?.wcBindable,s=t?.inputs;if(void 0!==s)for(const t of s){const s=t.name;if(!Object.prototype.hasOwnProperty.call(e,s))continue;if(!i(e,s))continue;const n=e,r=n[s];delete n[s],n[s]=r}}(this),this.style.display="none",this._connectedCallbackPromise=this._core.observe(this.query)}disconnectedCallback(){this._core.dispose()}}var a;!function(t=customElements){t.get(e.tagNames.mediaQuery)||t.define(e.tagNames.mediaQuery,r)}(a);
2
+ //# sourceMappingURL=auto.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto.min.js","sources":["../src/config.ts","../src/core/MediaQueryCore.ts","../src/protocol/upgradeProperties.ts","../src/components/MediaQuery.ts","../src/bootstrapMediaQuery.ts","../src/registerComponents.ts"],"sourcesContent":["import { IConfig, IWritableConfig } from \"./types.js\";\r\n\r\ninterface IInternalConfig extends IConfig {\r\n tagNames: {\r\n mediaQuery: string;\r\n };\r\n}\r\n\r\nconst _config: IInternalConfig = {\r\n tagNames: {\r\n mediaQuery: \"wcs-media-query\",\r\n },\r\n};\r\n\r\nfunction deepFreeze<T>(obj: T): T {\r\n if (obj === null || typeof obj !== \"object\") return obj;\r\n Object.freeze(obj);\r\n for (const key of Object.keys(obj)) {\r\n deepFreeze((obj as Record<string, unknown>)[key]);\r\n }\r\n return obj;\r\n}\r\n\r\nfunction deepClone<T>(obj: T): T {\r\n if (obj === null || typeof obj !== \"object\") return obj;\r\n const clone: Record<string, unknown> = {};\r\n for (const key of Object.keys(obj)) {\r\n clone[key] = deepClone((obj as Record<string, unknown>)[key]);\r\n }\r\n return clone as T;\r\n}\r\n\r\nlet frozenConfig: IConfig | null = null;\r\n\r\nexport const config: IConfig = _config as IConfig;\r\n\r\nexport function getConfig(): IConfig {\r\n if (!frozenConfig) {\r\n frozenConfig = deepFreeze(deepClone(_config));\r\n }\r\n return frozenConfig;\r\n}\r\n\r\nexport function setConfig(partialConfig: IWritableConfig): void {\r\n if (partialConfig.tagNames) {\r\n Object.assign(_config.tagNames, partialConfig.tagNames);\r\n }\r\n frozenConfig = null;\r\n}\r\n","import {\r\n IWcBindable, WcsMatchMedia, WcsMediaQueryCoreOptions, WcsMediaQueryList, WcsMediaQuerySnapshot,\r\n} from \"../types.js\";\r\n\r\nconst UNSUPPORTED_SNAPSHOT: WcsMediaQuerySnapshot = Object.freeze({\r\n matched: false,\r\n media: \"\",\r\n supported: false,\r\n});\r\n\r\n// \"matchMedia exists but there is nothing to watch\": an empty query, or a\r\n// matchMedia call that threw. Same shape as UNSUPPORTED_SNAPSHOT except that\r\n// `supported` stays honest about the API being present.\r\nconst IDLE_SNAPSHOT: WcsMediaQuerySnapshot = Object.freeze({\r\n matched: false,\r\n media: \"\",\r\n supported: true,\r\n});\r\n\r\n/**\r\n * Headless media-query primitive. A thin, framework-agnostic wrapper around\r\n * `window.matchMedia` exposed through the wc-bindable protocol.\r\n *\r\n * `observe(query)` subscribes to one `MediaQueryList` and republishes its\r\n * `matches` / `media` as `matched` / `media` state; a different query while subscribed tears the\r\n * old list down and subscribes to the new one. Subscribing is synchronous, but\r\n * — unlike `@wcstack/network` — this Core does keep a `_gen` generation guard\r\n * (§3.4): each subscription's `change` listener captures the generation it was\r\n * created under and bails when it is stale, so a `MediaQueryList` whose\r\n * `removeEventListener` / `removeListener` misbehaves can never write the old\r\n * query's `matched` over the new query's (docs/media-query-tag-design.md §6).\r\n *\r\n * `matchMedia` is universally available in browsers; `supported === false` is\r\n * the non-browser case (SSR, workers) rather than a browser quirk.\r\n */\r\nexport class MediaQueryCore extends EventTarget {\r\n static wcBindable: IWcBindable = {\r\n protocol: \"wc-bindable\",\r\n version: 1,\r\n properties: [\r\n { name: \"matched\", event: \"wcs-media-query:change\", semantics: \"state\", getter: (e: Event) => (e as CustomEvent).detail.matched },\r\n { name: \"media\", event: \"wcs-media-query:change\", semantics: \"state\", getter: (e: Event) => (e as CustomEvent).detail.media },\r\n { name: \"supported\", event: \"wcs-media-query:change\", semantics: \"state\", getter: (e: Event) => (e as CustomEvent).detail.supported },\r\n ],\r\n // Pure monitor: a MediaQueryList has no action to invoke.\r\n //\r\n // `matched`, not `matches`: `Element.prototype.matches(selector)` exists on\r\n // every element, and a wc-bindable property name is read straight off the\r\n // Shell — a boolean `matches` would shadow the platform method\r\n // (docs/media-query-tag-design.md §2.1).\r\n commands: [],\r\n };\r\n\r\n private _target: EventTarget;\r\n private _snapshot: WcsMediaQuerySnapshot = UNSUPPORTED_SNAPSHOT;\r\n\r\n // The query currently subscribed (or last requested). \"\" means \"nothing to watch\".\r\n private _query = \"\";\r\n\r\n // Detaches the live `change` listener of the current subscription, if any.\r\n private _unsubscribe: (() => void) | null = null;\r\n\r\n // True between observe() and dispose(). Guards observe() so a redundant call\r\n // with the same query does not re-subscribe; dispose() resets it.\r\n private _subscribed = false;\r\n\r\n // Generation guard (§3.4). Bumped by every (re)subscription and by dispose().\r\n // A `change` listener captures its generation and ignores the event once a\r\n // newer subscription exists — see the class docs for why this is kept even\r\n // though subscribing itself is synchronous.\r\n private _gen = 0;\r\n\r\n // Injected matchMedia (tests / non-window hosts). `null` = resolve\r\n // `globalThis.matchMedia` at call time (§3.7).\r\n private _injectedMatchMedia: WcsMatchMedia | null;\r\n\r\n // SSR (§3.8): no asynchronous probe to await — observe() completes\r\n // synchronously, so readiness is immediate.\r\n private _ready: Promise<void> = Promise.resolve();\r\n\r\n constructor(target?: EventTarget, options?: WcsMediaQueryCoreOptions) {\r\n super();\r\n this._target = target ?? this;\r\n this._injectedMatchMedia = options?.matchMedia ?? null;\r\n }\r\n\r\n get ready(): Promise<void> {\r\n return this._ready;\r\n }\r\n\r\n get query(): string {\r\n return this._query;\r\n }\r\n\r\n get matched(): boolean {\r\n return this._snapshot.matched;\r\n }\r\n\r\n get media(): string {\r\n return this._snapshot.media;\r\n }\r\n\r\n get supported(): boolean {\r\n return this._snapshot.supported;\r\n }\r\n\r\n // Lifecycle (§3.5). Idempotent: observe() with the query already subscribed\r\n // is a no-op (no double listener, no redundant dispatch). A different query\r\n // re-subscribes (dispose-then-observe semantics in one call). Omitting the\r\n // argument keeps the current query — the reconnect case for the Shell.\r\n // Synchronous overall (no probe to await), so the returned promise is only\r\n // for API uniformity with other IO nodes.\r\n observe(query: string = this._query): Promise<void> {\r\n if (this._subscribed && query === this._query) {\r\n return this._ready;\r\n }\r\n this._teardown();\r\n this._query = query;\r\n this._subscribed = true;\r\n this._subscribe(query);\r\n return this._ready;\r\n }\r\n\r\n dispose(): void {\r\n this._subscribed = false;\r\n this._teardown();\r\n }\r\n\r\n // API resolution is call-time, never cached (§3.7): lets tests install/remove\r\n // globalThis.matchMedia freely and lets a non-browser host be detected\r\n // correctly on every observe(). Called as a method of globalThis so the\r\n // native implementation keeps its `this` (calling it unbound throws).\r\n private _resolveMatchMedia(): WcsMatchMedia | null {\r\n if (this._injectedMatchMedia !== null) {\r\n return this._injectedMatchMedia;\r\n }\r\n const g = globalThis as { matchMedia?: WcsMatchMedia };\r\n return typeof g.matchMedia === \"function\" ? (q: string) => g.matchMedia!(q) : null;\r\n }\r\n\r\n private _subscribe(query: string): void {\r\n const gen = ++this._gen;\r\n const matchMedia = this._resolveMatchMedia();\r\n if (matchMedia === null) {\r\n this._apply(UNSUPPORTED_SNAPSHOT);\r\n return;\r\n }\r\n if (query === \"\") {\r\n this._apply(IDLE_SNAPSHOT);\r\n return;\r\n }\r\n // never-throw (§3.6): browsers do not throw on an invalid query string\r\n // (they return `media: \"not all\"`), but a hostile host or a broken\r\n // MediaQueryList polyfill might — that must not kill observe().\r\n let list: WcsMediaQueryList | null = null;\r\n try {\r\n list = matchMedia(query);\r\n const onChange = (): void => {\r\n if (gen !== this._gen) return; // stale subscription — never write\r\n this._apply(this._read(list!));\r\n };\r\n this._unsubscribe = attachChange(list, onChange);\r\n } catch {\r\n list = null;\r\n this._unsubscribe = null;\r\n }\r\n this._apply(list === null ? IDLE_SNAPSHOT : this._read(list));\r\n }\r\n\r\n private _teardown(): void {\r\n this._gen++;\r\n if (this._unsubscribe !== null) {\r\n const unsubscribe = this._unsubscribe;\r\n this._unsubscribe = null;\r\n try {\r\n unsubscribe();\r\n } catch {\r\n // never-throw: a list that refuses to detach is already neutralized\r\n // by the generation bump above.\r\n }\r\n }\r\n }\r\n\r\n private _read(list: WcsMediaQueryList): WcsMediaQuerySnapshot {\r\n return {\r\n matched: list.matches === true,\r\n media: typeof list.media === \"string\" ? list.media : \"\",\r\n supported: true,\r\n };\r\n }\r\n\r\n // Same-value guard (§3.3 MUST): the native `change` event already fires only\r\n // when `matches` flips, but this Core still verifies field-by-field before\r\n // dispatching — a re-subscription to an equivalent query, or a legacy\r\n // `addListener` double-fire, must not produce a redundant event.\r\n private _apply(next: WcsMediaQuerySnapshot): void {\r\n const prev = this._snapshot;\r\n if (\r\n prev.matched === next.matched &&\r\n prev.media === next.media &&\r\n prev.supported === next.supported\r\n ) {\r\n return;\r\n }\r\n this._snapshot = next;\r\n this._target.dispatchEvent(new CustomEvent(\"wcs-media-query:change\", {\r\n detail: next,\r\n // Family-wide MUST (async-io-node-guidelines.md §3.3): the event bubbles\r\n // from the Shell element so document-level consumers can delegate.\r\n bubbles: true,\r\n }));\r\n }\r\n}\r\n\r\n// Subscribe to a MediaQueryList's `change` with whichever listener API it has.\r\n// Modern engines: EventTarget-style. Old Safari (< 14): the deprecated\r\n// addListener / removeListener pair. Neither: no live updates — the snapshot\r\n// taken at observe() is all there is (a static polyfill, for instance).\r\n// Returns the matching detach function.\r\nfunction attachChange(list: WcsMediaQueryList, listener: () => void): () => void {\r\n if (typeof list.addEventListener === \"function\" && typeof list.removeEventListener === \"function\") {\r\n list.addEventListener(\"change\", listener);\r\n return () => list.removeEventListener!(\"change\", listener);\r\n }\r\n if (typeof list.addListener === \"function\" && typeof list.removeListener === \"function\") {\r\n list.addListener(listener);\r\n return () => list.removeListener!(listener);\r\n }\r\n return () => {};\r\n}\r\n","// ===========================================================================\r\n// AUTO-GENERATED FILE - DO NOT EDIT.\r\n// Generated from /protocol/upgrade-properties.ts by scripts/sync-protocol-types.mjs.\r\n// Run `node scripts/sync-protocol-types.mjs` after editing the source.\r\n// ===========================================================================\r\n\r\n// custom element の property upgrade — `static wcBindable.inputs` に宣言した入力のうち、\r\n// 要素が upgrade される前に代入された own データプロパティを取り込み直す。\r\n//\r\n// なぜ必要か:\r\n// 未定義タグの要素は素の HTMLElement なので、`el.url = \"...\"` は own データプロパティを作る。\r\n// upgrade 後にクラスの accessor が prototype へ入っても own プロパティが優先されるため、\r\n// setter は二度と呼ばれず、値は要素へ届かないまま消える(エラーも警告も出ない)。\r\n// 常にプロパティ代入を行う framework(Angular の `[prop]`、Lit の `.prop=`、\r\n// Solid の `prop:`、Vue の `.prop` 修飾子)× 遅延定義(autoloader / CDN / code-split)で\r\n// 常態的に起きる。docs/architecture-hardening/13-framework-adapter-binding-constraints.md §1.2。\r\n//\r\n// 安全側の判定:\r\n// own プロパティがあっても、prototype チェーンに accessor が無ければ「シャドウ」ではなく\r\n// その own プロパティ自体が正規の格納先なので触らない(public class field を壊さない)。\r\n//\r\n// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/upgrade-properties.ts), then run\r\n// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies\r\n// (packages/<pkg>/src/protocol/upgradeProperties.ts). Those copies are generated — do not edit them.\r\nimport { IWcBindable } from \"./wcBindable.js\";\r\n\r\nfunction hasAccessorOnPrototype(target: object, name: string): boolean {\r\n let proto = Object.getPrototypeOf(target);\r\n while (proto !== null) {\r\n const descriptor = Object.getOwnPropertyDescriptor(proto, name);\r\n if (descriptor !== undefined) {\r\n return typeof descriptor.get === \"function\" || typeof descriptor.set === \"function\";\r\n }\r\n proto = Object.getPrototypeOf(proto);\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * `connectedCallback` の先頭で呼ぶ。宣言済み input のうち upgrade 前の代入で\r\n * accessor をシャドウしている own プロパティを、delete → 再代入で setter に通し直す。\r\n *\r\n * - 冪等: 再代入は accessor を通るので own プロパティは残らず、2 回目以降は no-op。\r\n * - 宣言に `inputs` が無い要素、`wcBindable` を持たない要素では何もしない。\r\n * - 値の意味は変えない。今まで捨てられていた代入が届くようになる一方向の変化。\r\n */\r\nexport function upgradeProperties(element: object): void {\r\n const declaration = (element as { constructor?: { wcBindable?: IWcBindable } }).constructor?.wcBindable;\r\n const inputs = declaration?.inputs;\r\n if (inputs === undefined) return;\r\n for (const input of inputs) {\r\n const name = input.name;\r\n if (!Object.prototype.hasOwnProperty.call(element, name)) continue;\r\n if (!hasAccessorOnPrototype(element, name)) continue;\r\n const record = element as Record<string, unknown>;\r\n const value = record[name];\r\n delete record[name];\r\n record[name] = value;\r\n }\r\n}\r\n","import { IWcBindable } from \"../types.js\";\r\nimport { MediaQueryCore } from \"../core/MediaQueryCore.js\";\r\nimport { upgradeProperties } from \"../protocol/upgradeProperties.js\";\r\n\r\n/**\r\n * `<wcs-media-query query=\"(prefers-color-scheme: dark)\">` — declarative\r\n * `matchMedia` monitor.\r\n *\r\n * One attribute (`query`), three outputs (`matched` / `media` / `supported`),\r\n * no commands. Changing `query` while connected re-subscribes the Core to the\r\n * new MediaQueryList (docs/media-query-tag-design.md §7).\r\n */\r\nexport class WcsMediaQuery extends HTMLElement {\r\n // SSR (§4.4): observe() completes synchronously, but the Shell still exposes\r\n // connectedCallbackPromise so SSR (@wcstack/server render.ts) can await it\r\n // uniformly across all IO nodes before snapshotting the HTML.\r\n static hasConnectedCallbackPromise = true;\r\n\r\n static wcBindable: IWcBindable = {\r\n ...MediaQueryCore.wcBindable,\r\n // Shell-level settable surface: the media query string, mirrored to the\r\n // `query` attribute (idempotent reflect, so a binder writing through\r\n // inputs[].attribute is safe).\r\n inputs: [\r\n { name: \"query\", attribute: \"query\" },\r\n ],\r\n // Core の commands をそのまま継承(単一情報源)。network と同型。\r\n commands: MediaQueryCore.wcBindable.commands,\r\n };\r\n\r\n // `query` is the only attribute worth re-subscribing for; it is the whole\r\n // configuration of this node.\r\n static get observedAttributes(): string[] { return [\"query\"]; }\r\n\r\n private _core: MediaQueryCore;\r\n private _connectedCallbackPromise: Promise<void> = Promise.resolve();\r\n private _internals: ElementInternals | null = null;\r\n\r\n constructor() {\r\n super();\r\n this._core = new MediaQueryCore(this);\r\n this._internals = this._initInternals();\r\n this._wireStates({\r\n \"wcs-media-query:change\": (d) => ({\r\n matched: d.matched === true,\r\n supported: d.supported === true,\r\n }),\r\n });\r\n }\r\n\r\n // CSS state reflection (:state()) — debug-only snapshot getter. NOT part of\r\n // wc-bindable (not a bind target); see README \"CSS styling with :state()\".\r\n // MUST NOT return the live CustomStateSet (that would let callers write\r\n // states from outside, defeating the point of :state() being read-only).\r\n get debugStates(): string[] {\r\n return this._internals ? [...this._internals.states] : [];\r\n }\r\n\r\n private _initInternals(): ElementInternals | null {\r\n // never-throw (async-io-node-guidelines.md §3.6): attachInternals is absent\r\n // in happy-dom / older environments, and pre-125 Chromium rejects\r\n // non-dashed state names from states.add() (probed and discarded here).\r\n // Either case silently disables reflection — the component still works,\r\n // it just doesn't expose :state() selectors.\r\n try {\r\n if (typeof this.attachInternals !== \"function\") return null;\r\n const internals = this.attachInternals();\r\n internals.states.add(\"wcs-probe\");\r\n internals.states.delete(\"wcs-probe\");\r\n return internals;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n private _wireStates(map: Record<string, (detail: any) => Record<string, boolean>>): void {\r\n if (this._internals === null) return;\r\n const states = this._internals.states;\r\n for (const [event, toStates] of Object.entries(map)) {\r\n this.addEventListener(event, (e) => {\r\n const debug = this.hasAttribute(\"debug-states\");\r\n for (const [name, on] of Object.entries(toStates((e as CustomEvent).detail))) {\r\n try {\r\n if (on) { states.add(name); } else { states.delete(name); }\r\n } catch { /* never-throw */ }\r\n if (debug) this.toggleAttribute(`data-wcs-state-${name}`, on);\r\n }\r\n });\r\n }\r\n }\r\n\r\n // --- Attribute accessors ---\r\n\r\n get query(): string {\r\n return this.getAttribute(\"query\") ?? \"\";\r\n }\r\n\r\n set query(value: string) {\r\n this.setAttribute(\"query\", value);\r\n }\r\n\r\n // --- Core delegated getters ---\r\n\r\n get matched(): boolean {\r\n return this._core.matched;\r\n }\r\n\r\n get media(): string {\r\n return this._core.media;\r\n }\r\n\r\n get supported(): boolean {\r\n return this._core.supported;\r\n }\r\n\r\n get connectedCallbackPromise(): Promise<void> {\r\n return this._connectedCallbackPromise;\r\n }\r\n\r\n // --- Lifecycle ---\r\n\r\n attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void {\r\n // Re-subscribe on a live query change. Removing the attribute (newValue\r\n // null) is a real change too — it means \"watch nothing\", so `matched`\r\n // drops to false instead of lingering on the old query's value. Before\r\n // connect the attribute is simply read by connectedCallback.\r\n if (name === \"query\" && this.isConnected) {\r\n this._core.observe(newValue ?? \"\");\r\n }\r\n }\r\n\r\n connectedCallback(): void {\r\n // upgrade 前に代入された input を取り込み直す(doc 13 §1.2 / Phase A1)\r\n upgradeProperties(this);\r\n this.style.display = \"none\";\r\n this._connectedCallbackPromise = this._core.observe(this.query);\r\n }\r\n\r\n disconnectedCallback(): void {\r\n this._core.dispose();\r\n }\r\n}\r\n","import { setConfig } from \"./config.js\";\r\nimport { registerComponents } from \"./registerComponents.js\";\r\nimport { IWritableConfig } from \"./types.js\";\r\n\r\nexport function bootstrapMediaQuery(userConfig?: IWritableConfig, registry?: CustomElementRegistry): void {\r\n if (userConfig) {\r\n setConfig(userConfig);\r\n }\r\n registerComponents(registry);\r\n}\r\n","import { WcsMediaQuery } from \"./components/MediaQuery.js\";\r\nimport { config } from \"./config.js\";\r\n\r\n/**\r\n * Register this package's tags. Pass a scoped `CustomElementRegistry` to define\r\n * them for a single shadow tree -- scoped registries do not inherit the global\r\n * one, so a tree using one needs its own definitions.\r\n */\r\nexport function registerComponents(registry: CustomElementRegistry = customElements): void {\r\n if (!registry.get(config.tagNames.mediaQuery)) {\r\n registry.define(config.tagNames.mediaQuery, WcsMediaQuery);\r\n }\r\n}\r\n"],"names":["config","tagNames","mediaQuery","UNSUPPORTED_SNAPSHOT","Object","freeze","matched","media","supported","IDLE_SNAPSHOT","MediaQueryCore","EventTarget","static","protocol","version","properties","name","event","semantics","getter","e","detail","commands","_target","_snapshot","_query","_unsubscribe","_subscribed","_gen","_injectedMatchMedia","_ready","Promise","resolve","constructor","target","options","super","this","matchMedia","ready","query","observe","_teardown","_subscribe","dispose","_resolveMatchMedia","g","globalThis","q","gen","_apply","list","onChange","_read","listener","addEventListener","removeEventListener","addListener","removeListener","attachChange","unsubscribe","matches","next","prev","dispatchEvent","CustomEvent","bubbles","hasAccessorOnPrototype","proto","getPrototypeOf","descriptor","getOwnPropertyDescriptor","undefined","get","set","WcsMediaQuery","HTMLElement","wcBindable","inputs","attribute","observedAttributes","_core","_connectedCallbackPromise","_internals","_initInternals","_wireStates","d","debugStates","states","attachInternals","internals","add","delete","map","toStates","entries","debug","hasAttribute","on","toggleAttribute","getAttribute","value","setAttribute","connectedCallbackPromise","attributeChangedCallback","_oldValue","newValue","isConnected","connectedCallback","element","declaration","input","prototype","hasOwnProperty","call","record","upgradeProperties","style","display","disconnectedCallback","registry","customElements","define","registerComponents"],"mappings":"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"}
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Observation semantics of a `properties` entry.
3
+ *
4
+ * "state" — current value. A snapshot may cache it, and equality-based dedupe is safe.
5
+ * "event" — occurrence. Repeated identical payloads are distinct occurrences; never dedupe.
6
+ * "handle" — live / opaque resource with its own lifecycle (e.g. MediaStream). Not
7
+ * snapshot-safe and not necessarily serializable; consumers need an explicit
8
+ * ref / callback surface rather than a value slot.
9
+ */
10
+ type WcBindableSemantics = "state" | "event" | "handle";
11
+ interface IWcBindableProperty {
12
+ readonly name: string;
13
+ readonly event: string;
14
+ readonly getter?: (event: Event) => any;
15
+ /**
16
+ * Optional, additive, forward-compatible. An absent value means **unspecified**, NOT
17
+ * "state": a reader that finds no `semantics` MUST keep the behavior it had before this
18
+ * field existed (deliver the update as-is; do not start deduping, caching or serializing
19
+ * on assumption). Only an explicit value licenses a reader to change its handling.
20
+ */
21
+ readonly semantics?: WcBindableSemantics;
22
+ }
23
+ interface IWcBindableInput {
24
+ readonly name: string;
25
+ readonly attribute?: string;
26
+ }
27
+ interface IWcBindableCommand {
28
+ readonly name: string;
29
+ readonly async?: boolean;
30
+ }
31
+ interface IWcBindable {
32
+ readonly protocol: "wc-bindable";
33
+ /** Integer protocol version. All versions >= 1 are core-compatible. */
34
+ readonly version: number;
35
+ readonly properties: readonly IWcBindableProperty[];
36
+ readonly inputs?: readonly IWcBindableInput[];
37
+ readonly commands?: readonly IWcBindableCommand[];
38
+ }
39
+
40
+ interface ITagNames {
41
+ readonly mediaQuery: string;
42
+ }
43
+ interface IWritableTagNames {
44
+ mediaQuery?: string;
45
+ }
46
+ interface IConfig {
47
+ readonly tagNames: ITagNames;
48
+ }
49
+ interface IWritableConfig {
50
+ tagNames?: IWritableTagNames;
51
+ }
52
+
53
+ /**
54
+ * The subset of `MediaQueryList` this node reads and subscribes to. Modern
55
+ * engines expose `addEventListener("change", …)`; old Safari (< 14) only has
56
+ * the deprecated `addListener` / `removeListener` pair, so both are optional
57
+ * here and the Core picks whichever exists (docs/media-query-tag-design.md §5).
58
+ */
59
+ interface WcsMediaQueryList {
60
+ readonly matches: boolean;
61
+ readonly media: string;
62
+ addEventListener?(type: "change", listener: () => void): void;
63
+ removeEventListener?(type: "change", listener: () => void): void;
64
+ addListener?(listener: () => void): void;
65
+ removeListener?(listener: () => void): void;
66
+ }
67
+ /**
68
+ * Injectable `matchMedia` for MediaQueryCore. The default resolves
69
+ * `globalThis.matchMedia` at call time (§3.7); tests inject a fake whose
70
+ * `matches` and `change` dispatch they drive directly — happy-dom's
71
+ * MediaQueryList change delivery is not reliable enough to test against
72
+ * (the same precedent as `@wcstack/raf`'s reduced-motion gate).
73
+ */
74
+ type WcsMatchMedia = (query: string) => WcsMediaQueryList;
75
+ interface WcsMediaQueryCoreOptions {
76
+ matchMedia?: WcsMatchMedia;
77
+ }
78
+ /**
79
+ * A single snapshot of the subscribed `MediaQueryList`, or the "nothing to
80
+ * watch" default. `matched` (the list's `matches`) is `false` and `media` is `""` whenever there is
81
+ * no live list — `matchMedia` absent (`supported === false`), an empty query,
82
+ * or a `matchMedia` call that threw. `supported` answers only "is
83
+ * `matchMedia` a function here", resolved on every observe().
84
+ */
85
+ interface WcsMediaQuerySnapshot {
86
+ matched: boolean;
87
+ media: string;
88
+ supported: boolean;
89
+ }
90
+ /**
91
+ * Value types for MediaQueryCore (headless) — the observable state properties.
92
+ * Use with `bind()` from a wc-bindable binding core for compile-time type checking.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const core = new MediaQueryCore();
97
+ * bind(core, (name: keyof WcsMediaQueryCoreValues, value) => { ... });
98
+ * ```
99
+ */
100
+ type WcsMediaQueryCoreValues = WcsMediaQuerySnapshot;
101
+ /**
102
+ * Value types for the Shell (`<wcs-media-query>`) — the Core's observable
103
+ * surface plus the one attribute-linked input, `query`.
104
+ */
105
+ type WcsMediaQueryValues = WcsMediaQueryCoreValues & {
106
+ query: string;
107
+ };
108
+
109
+ declare function bootstrapMediaQuery(userConfig?: IWritableConfig, registry?: CustomElementRegistry): void;
110
+
111
+ declare function getConfig(): IConfig;
112
+
113
+ /**
114
+ * Headless media-query primitive. A thin, framework-agnostic wrapper around
115
+ * `window.matchMedia` exposed through the wc-bindable protocol.
116
+ *
117
+ * `observe(query)` subscribes to one `MediaQueryList` and republishes its
118
+ * `matches` / `media` as `matched` / `media` state; a different query while subscribed tears the
119
+ * old list down and subscribes to the new one. Subscribing is synchronous, but
120
+ * — unlike `@wcstack/network` — this Core does keep a `_gen` generation guard
121
+ * (§3.4): each subscription's `change` listener captures the generation it was
122
+ * created under and bails when it is stale, so a `MediaQueryList` whose
123
+ * `removeEventListener` / `removeListener` misbehaves can never write the old
124
+ * query's `matched` over the new query's (docs/media-query-tag-design.md §6).
125
+ *
126
+ * `matchMedia` is universally available in browsers; `supported === false` is
127
+ * the non-browser case (SSR, workers) rather than a browser quirk.
128
+ */
129
+ declare class MediaQueryCore extends EventTarget {
130
+ static wcBindable: IWcBindable;
131
+ private _target;
132
+ private _snapshot;
133
+ private _query;
134
+ private _unsubscribe;
135
+ private _subscribed;
136
+ private _gen;
137
+ private _injectedMatchMedia;
138
+ private _ready;
139
+ constructor(target?: EventTarget, options?: WcsMediaQueryCoreOptions);
140
+ get ready(): Promise<void>;
141
+ get query(): string;
142
+ get matched(): boolean;
143
+ get media(): string;
144
+ get supported(): boolean;
145
+ observe(query?: string): Promise<void>;
146
+ dispose(): void;
147
+ private _resolveMatchMedia;
148
+ private _subscribe;
149
+ private _teardown;
150
+ private _read;
151
+ private _apply;
152
+ }
153
+
154
+ /**
155
+ * `<wcs-media-query query="(prefers-color-scheme: dark)">` — declarative
156
+ * `matchMedia` monitor.
157
+ *
158
+ * One attribute (`query`), three outputs (`matched` / `media` / `supported`),
159
+ * no commands. Changing `query` while connected re-subscribes the Core to the
160
+ * new MediaQueryList (docs/media-query-tag-design.md §7).
161
+ */
162
+ declare class WcsMediaQuery extends HTMLElement {
163
+ static hasConnectedCallbackPromise: boolean;
164
+ static wcBindable: IWcBindable;
165
+ static get observedAttributes(): string[];
166
+ private _core;
167
+ private _connectedCallbackPromise;
168
+ private _internals;
169
+ constructor();
170
+ get debugStates(): string[];
171
+ private _initInternals;
172
+ private _wireStates;
173
+ get query(): string;
174
+ set query(value: string);
175
+ get matched(): boolean;
176
+ get media(): string;
177
+ get supported(): boolean;
178
+ get connectedCallbackPromise(): Promise<void>;
179
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
180
+ connectedCallback(): void;
181
+ disconnectedCallback(): void;
182
+ }
183
+
184
+ declare global {
185
+ interface HTMLElementTagNameMap {
186
+ "wcs-media-query": WcsMediaQuery;
187
+ }
188
+ }
189
+
190
+ export { MediaQueryCore, WcsMediaQuery, bootstrapMediaQuery, getConfig };
191
+ export type { IWritableConfig, IWritableTagNames, WcsMatchMedia, WcsMediaQueryCoreOptions, WcsMediaQueryCoreValues, WcsMediaQueryList, WcsMediaQuerySnapshot, WcsMediaQueryValues };
@@ -0,0 +1,435 @@
1
+ const _config = {
2
+ tagNames: {
3
+ mediaQuery: "wcs-media-query",
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 UNSUPPORTED_SNAPSHOT = Object.freeze({
40
+ matched: false,
41
+ media: "",
42
+ supported: false,
43
+ });
44
+ // "matchMedia exists but there is nothing to watch": an empty query, or a
45
+ // matchMedia call that threw. Same shape as UNSUPPORTED_SNAPSHOT except that
46
+ // `supported` stays honest about the API being present.
47
+ const IDLE_SNAPSHOT = Object.freeze({
48
+ matched: false,
49
+ media: "",
50
+ supported: true,
51
+ });
52
+ /**
53
+ * Headless media-query primitive. A thin, framework-agnostic wrapper around
54
+ * `window.matchMedia` exposed through the wc-bindable protocol.
55
+ *
56
+ * `observe(query)` subscribes to one `MediaQueryList` and republishes its
57
+ * `matches` / `media` as `matched` / `media` state; a different query while subscribed tears the
58
+ * old list down and subscribes to the new one. Subscribing is synchronous, but
59
+ * — unlike `@wcstack/network` — this Core does keep a `_gen` generation guard
60
+ * (§3.4): each subscription's `change` listener captures the generation it was
61
+ * created under and bails when it is stale, so a `MediaQueryList` whose
62
+ * `removeEventListener` / `removeListener` misbehaves can never write the old
63
+ * query's `matched` over the new query's (docs/media-query-tag-design.md §6).
64
+ *
65
+ * `matchMedia` is universally available in browsers; `supported === false` is
66
+ * the non-browser case (SSR, workers) rather than a browser quirk.
67
+ */
68
+ class MediaQueryCore extends EventTarget {
69
+ static wcBindable = {
70
+ protocol: "wc-bindable",
71
+ version: 1,
72
+ properties: [
73
+ { name: "matched", event: "wcs-media-query:change", semantics: "state", getter: (e) => e.detail.matched },
74
+ { name: "media", event: "wcs-media-query:change", semantics: "state", getter: (e) => e.detail.media },
75
+ { name: "supported", event: "wcs-media-query:change", semantics: "state", getter: (e) => e.detail.supported },
76
+ ],
77
+ // Pure monitor: a MediaQueryList has no action to invoke.
78
+ //
79
+ // `matched`, not `matches`: `Element.prototype.matches(selector)` exists on
80
+ // every element, and a wc-bindable property name is read straight off the
81
+ // Shell — a boolean `matches` would shadow the platform method
82
+ // (docs/media-query-tag-design.md §2.1).
83
+ commands: [],
84
+ };
85
+ _target;
86
+ _snapshot = UNSUPPORTED_SNAPSHOT;
87
+ // The query currently subscribed (or last requested). "" means "nothing to watch".
88
+ _query = "";
89
+ // Detaches the live `change` listener of the current subscription, if any.
90
+ _unsubscribe = null;
91
+ // True between observe() and dispose(). Guards observe() so a redundant call
92
+ // with the same query does not re-subscribe; dispose() resets it.
93
+ _subscribed = false;
94
+ // Generation guard (§3.4). Bumped by every (re)subscription and by dispose().
95
+ // A `change` listener captures its generation and ignores the event once a
96
+ // newer subscription exists — see the class docs for why this is kept even
97
+ // though subscribing itself is synchronous.
98
+ _gen = 0;
99
+ // Injected matchMedia (tests / non-window hosts). `null` = resolve
100
+ // `globalThis.matchMedia` at call time (§3.7).
101
+ _injectedMatchMedia;
102
+ // SSR (§3.8): no asynchronous probe to await — observe() completes
103
+ // synchronously, so readiness is immediate.
104
+ _ready = Promise.resolve();
105
+ constructor(target, options) {
106
+ super();
107
+ this._target = target ?? this;
108
+ this._injectedMatchMedia = options?.matchMedia ?? null;
109
+ }
110
+ get ready() {
111
+ return this._ready;
112
+ }
113
+ get query() {
114
+ return this._query;
115
+ }
116
+ get matched() {
117
+ return this._snapshot.matched;
118
+ }
119
+ get media() {
120
+ return this._snapshot.media;
121
+ }
122
+ get supported() {
123
+ return this._snapshot.supported;
124
+ }
125
+ // Lifecycle (§3.5). Idempotent: observe() with the query already subscribed
126
+ // is a no-op (no double listener, no redundant dispatch). A different query
127
+ // re-subscribes (dispose-then-observe semantics in one call). Omitting the
128
+ // argument keeps the current query — the reconnect case for the Shell.
129
+ // Synchronous overall (no probe to await), so the returned promise is only
130
+ // for API uniformity with other IO nodes.
131
+ observe(query = this._query) {
132
+ if (this._subscribed && query === this._query) {
133
+ return this._ready;
134
+ }
135
+ this._teardown();
136
+ this._query = query;
137
+ this._subscribed = true;
138
+ this._subscribe(query);
139
+ return this._ready;
140
+ }
141
+ dispose() {
142
+ this._subscribed = false;
143
+ this._teardown();
144
+ }
145
+ // API resolution is call-time, never cached (§3.7): lets tests install/remove
146
+ // globalThis.matchMedia freely and lets a non-browser host be detected
147
+ // correctly on every observe(). Called as a method of globalThis so the
148
+ // native implementation keeps its `this` (calling it unbound throws).
149
+ _resolveMatchMedia() {
150
+ if (this._injectedMatchMedia !== null) {
151
+ return this._injectedMatchMedia;
152
+ }
153
+ const g = globalThis;
154
+ return typeof g.matchMedia === "function" ? (q) => g.matchMedia(q) : null;
155
+ }
156
+ _subscribe(query) {
157
+ const gen = ++this._gen;
158
+ const matchMedia = this._resolveMatchMedia();
159
+ if (matchMedia === null) {
160
+ this._apply(UNSUPPORTED_SNAPSHOT);
161
+ return;
162
+ }
163
+ if (query === "") {
164
+ this._apply(IDLE_SNAPSHOT);
165
+ return;
166
+ }
167
+ // never-throw (§3.6): browsers do not throw on an invalid query string
168
+ // (they return `media: "not all"`), but a hostile host or a broken
169
+ // MediaQueryList polyfill might — that must not kill observe().
170
+ let list = null;
171
+ try {
172
+ list = matchMedia(query);
173
+ const onChange = () => {
174
+ if (gen !== this._gen)
175
+ return; // stale subscription — never write
176
+ this._apply(this._read(list));
177
+ };
178
+ this._unsubscribe = attachChange(list, onChange);
179
+ }
180
+ catch {
181
+ list = null;
182
+ this._unsubscribe = null;
183
+ }
184
+ this._apply(list === null ? IDLE_SNAPSHOT : this._read(list));
185
+ }
186
+ _teardown() {
187
+ this._gen++;
188
+ if (this._unsubscribe !== null) {
189
+ const unsubscribe = this._unsubscribe;
190
+ this._unsubscribe = null;
191
+ try {
192
+ unsubscribe();
193
+ }
194
+ catch {
195
+ // never-throw: a list that refuses to detach is already neutralized
196
+ // by the generation bump above.
197
+ }
198
+ }
199
+ }
200
+ _read(list) {
201
+ return {
202
+ matched: list.matches === true,
203
+ media: typeof list.media === "string" ? list.media : "",
204
+ supported: true,
205
+ };
206
+ }
207
+ // Same-value guard (§3.3 MUST): the native `change` event already fires only
208
+ // when `matches` flips, but this Core still verifies field-by-field before
209
+ // dispatching — a re-subscription to an equivalent query, or a legacy
210
+ // `addListener` double-fire, must not produce a redundant event.
211
+ _apply(next) {
212
+ const prev = this._snapshot;
213
+ if (prev.matched === next.matched &&
214
+ prev.media === next.media &&
215
+ prev.supported === next.supported) {
216
+ return;
217
+ }
218
+ this._snapshot = next;
219
+ this._target.dispatchEvent(new CustomEvent("wcs-media-query:change", {
220
+ detail: next,
221
+ // Family-wide MUST (async-io-node-guidelines.md §3.3): the event bubbles
222
+ // from the Shell element so document-level consumers can delegate.
223
+ bubbles: true,
224
+ }));
225
+ }
226
+ }
227
+ // Subscribe to a MediaQueryList's `change` with whichever listener API it has.
228
+ // Modern engines: EventTarget-style. Old Safari (< 14): the deprecated
229
+ // addListener / removeListener pair. Neither: no live updates — the snapshot
230
+ // taken at observe() is all there is (a static polyfill, for instance).
231
+ // Returns the matching detach function.
232
+ function attachChange(list, listener) {
233
+ if (typeof list.addEventListener === "function" && typeof list.removeEventListener === "function") {
234
+ list.addEventListener("change", listener);
235
+ return () => list.removeEventListener("change", listener);
236
+ }
237
+ if (typeof list.addListener === "function" && typeof list.removeListener === "function") {
238
+ list.addListener(listener);
239
+ return () => list.removeListener(listener);
240
+ }
241
+ return () => { };
242
+ }
243
+
244
+ // ===========================================================================
245
+ // AUTO-GENERATED FILE - DO NOT EDIT.
246
+ // Generated from /protocol/upgrade-properties.ts by scripts/sync-protocol-types.mjs.
247
+ // Run `node scripts/sync-protocol-types.mjs` after editing the source.
248
+ // ===========================================================================
249
+ function hasAccessorOnPrototype(target, name) {
250
+ let proto = Object.getPrototypeOf(target);
251
+ while (proto !== null) {
252
+ const descriptor = Object.getOwnPropertyDescriptor(proto, name);
253
+ if (descriptor !== undefined) {
254
+ return typeof descriptor.get === "function" || typeof descriptor.set === "function";
255
+ }
256
+ proto = Object.getPrototypeOf(proto);
257
+ }
258
+ return false;
259
+ }
260
+ /**
261
+ * `connectedCallback` の先頭で呼ぶ。宣言済み input のうち upgrade 前の代入で
262
+ * accessor をシャドウしている own プロパティを、delete → 再代入で setter に通し直す。
263
+ *
264
+ * - 冪等: 再代入は accessor を通るので own プロパティは残らず、2 回目以降は no-op。
265
+ * - 宣言に `inputs` が無い要素、`wcBindable` を持たない要素では何もしない。
266
+ * - 値の意味は変えない。今まで捨てられていた代入が届くようになる一方向の変化。
267
+ */
268
+ function upgradeProperties(element) {
269
+ const declaration = element.constructor?.wcBindable;
270
+ const inputs = declaration?.inputs;
271
+ if (inputs === undefined)
272
+ return;
273
+ for (const input of inputs) {
274
+ const name = input.name;
275
+ if (!Object.prototype.hasOwnProperty.call(element, name))
276
+ continue;
277
+ if (!hasAccessorOnPrototype(element, name))
278
+ continue;
279
+ const record = element;
280
+ const value = record[name];
281
+ delete record[name];
282
+ record[name] = value;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * `<wcs-media-query query="(prefers-color-scheme: dark)">` — declarative
288
+ * `matchMedia` monitor.
289
+ *
290
+ * One attribute (`query`), three outputs (`matched` / `media` / `supported`),
291
+ * no commands. Changing `query` while connected re-subscribes the Core to the
292
+ * new MediaQueryList (docs/media-query-tag-design.md §7).
293
+ */
294
+ class WcsMediaQuery extends HTMLElement {
295
+ // SSR (§4.4): observe() completes synchronously, but the Shell still exposes
296
+ // connectedCallbackPromise so SSR (@wcstack/server render.ts) can await it
297
+ // uniformly across all IO nodes before snapshotting the HTML.
298
+ static hasConnectedCallbackPromise = true;
299
+ static wcBindable = {
300
+ ...MediaQueryCore.wcBindable,
301
+ // Shell-level settable surface: the media query string, mirrored to the
302
+ // `query` attribute (idempotent reflect, so a binder writing through
303
+ // inputs[].attribute is safe).
304
+ inputs: [
305
+ { name: "query", attribute: "query" },
306
+ ],
307
+ // Core の commands をそのまま継承(単一情報源)。network と同型。
308
+ commands: MediaQueryCore.wcBindable.commands,
309
+ };
310
+ // `query` is the only attribute worth re-subscribing for; it is the whole
311
+ // configuration of this node.
312
+ static get observedAttributes() { return ["query"]; }
313
+ _core;
314
+ _connectedCallbackPromise = Promise.resolve();
315
+ _internals = null;
316
+ constructor() {
317
+ super();
318
+ this._core = new MediaQueryCore(this);
319
+ this._internals = this._initInternals();
320
+ this._wireStates({
321
+ "wcs-media-query:change": (d) => ({
322
+ matched: d.matched === true,
323
+ supported: d.supported === true,
324
+ }),
325
+ });
326
+ }
327
+ // CSS state reflection (:state()) — debug-only snapshot getter. NOT part of
328
+ // wc-bindable (not a bind target); see README "CSS styling with :state()".
329
+ // MUST NOT return the live CustomStateSet (that would let callers write
330
+ // states from outside, defeating the point of :state() being read-only).
331
+ get debugStates() {
332
+ return this._internals ? [...this._internals.states] : [];
333
+ }
334
+ _initInternals() {
335
+ // never-throw (async-io-node-guidelines.md §3.6): attachInternals is absent
336
+ // in happy-dom / older environments, and pre-125 Chromium rejects
337
+ // non-dashed state names from states.add() (probed and discarded here).
338
+ // Either case silently disables reflection — the component still works,
339
+ // it just doesn't expose :state() selectors.
340
+ try {
341
+ if (typeof this.attachInternals !== "function")
342
+ return null;
343
+ const internals = this.attachInternals();
344
+ internals.states.add("wcs-probe");
345
+ internals.states.delete("wcs-probe");
346
+ return internals;
347
+ }
348
+ catch {
349
+ return null;
350
+ }
351
+ }
352
+ _wireStates(map) {
353
+ if (this._internals === null)
354
+ return;
355
+ const states = this._internals.states;
356
+ for (const [event, toStates] of Object.entries(map)) {
357
+ this.addEventListener(event, (e) => {
358
+ const debug = this.hasAttribute("debug-states");
359
+ for (const [name, on] of Object.entries(toStates(e.detail))) {
360
+ try {
361
+ if (on) {
362
+ states.add(name);
363
+ }
364
+ else {
365
+ states.delete(name);
366
+ }
367
+ }
368
+ catch { /* never-throw */ }
369
+ if (debug)
370
+ this.toggleAttribute(`data-wcs-state-${name}`, on);
371
+ }
372
+ });
373
+ }
374
+ }
375
+ // --- Attribute accessors ---
376
+ get query() {
377
+ return this.getAttribute("query") ?? "";
378
+ }
379
+ set query(value) {
380
+ this.setAttribute("query", value);
381
+ }
382
+ // --- Core delegated getters ---
383
+ get matched() {
384
+ return this._core.matched;
385
+ }
386
+ get media() {
387
+ return this._core.media;
388
+ }
389
+ get supported() {
390
+ return this._core.supported;
391
+ }
392
+ get connectedCallbackPromise() {
393
+ return this._connectedCallbackPromise;
394
+ }
395
+ // --- Lifecycle ---
396
+ attributeChangedCallback(name, _oldValue, newValue) {
397
+ // Re-subscribe on a live query change. Removing the attribute (newValue
398
+ // null) is a real change too — it means "watch nothing", so `matched`
399
+ // drops to false instead of lingering on the old query's value. Before
400
+ // connect the attribute is simply read by connectedCallback.
401
+ if (name === "query" && this.isConnected) {
402
+ this._core.observe(newValue ?? "");
403
+ }
404
+ }
405
+ connectedCallback() {
406
+ // upgrade 前に代入された input を取り込み直す(doc 13 §1.2 / Phase A1)
407
+ upgradeProperties(this);
408
+ this.style.display = "none";
409
+ this._connectedCallbackPromise = this._core.observe(this.query);
410
+ }
411
+ disconnectedCallback() {
412
+ this._core.dispose();
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Register this package's tags. Pass a scoped `CustomElementRegistry` to define
418
+ * them for a single shadow tree -- scoped registries do not inherit the global
419
+ * one, so a tree using one needs its own definitions.
420
+ */
421
+ function registerComponents(registry = customElements) {
422
+ if (!registry.get(config.tagNames.mediaQuery)) {
423
+ registry.define(config.tagNames.mediaQuery, WcsMediaQuery);
424
+ }
425
+ }
426
+
427
+ function bootstrapMediaQuery(userConfig, registry) {
428
+ if (userConfig) {
429
+ setConfig(userConfig);
430
+ }
431
+ registerComponents(registry);
432
+ }
433
+
434
+ export { MediaQueryCore, WcsMediaQuery, bootstrapMediaQuery, getConfig };
435
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":["../src/config.ts","../src/core/MediaQueryCore.ts","../src/protocol/upgradeProperties.ts","../src/components/MediaQuery.ts","../src/registerComponents.ts","../src/bootstrapMediaQuery.ts"],"sourcesContent":["import { IConfig, IWritableConfig } from \"./types.js\";\r\n\r\ninterface IInternalConfig extends IConfig {\r\n tagNames: {\r\n mediaQuery: string;\r\n };\r\n}\r\n\r\nconst _config: IInternalConfig = {\r\n tagNames: {\r\n mediaQuery: \"wcs-media-query\",\r\n },\r\n};\r\n\r\nfunction deepFreeze<T>(obj: T): T {\r\n if (obj === null || typeof obj !== \"object\") return obj;\r\n Object.freeze(obj);\r\n for (const key of Object.keys(obj)) {\r\n deepFreeze((obj as Record<string, unknown>)[key]);\r\n }\r\n return obj;\r\n}\r\n\r\nfunction deepClone<T>(obj: T): T {\r\n if (obj === null || typeof obj !== \"object\") return obj;\r\n const clone: Record<string, unknown> = {};\r\n for (const key of Object.keys(obj)) {\r\n clone[key] = deepClone((obj as Record<string, unknown>)[key]);\r\n }\r\n return clone as T;\r\n}\r\n\r\nlet frozenConfig: IConfig | null = null;\r\n\r\nexport const config: IConfig = _config as IConfig;\r\n\r\nexport function getConfig(): IConfig {\r\n if (!frozenConfig) {\r\n frozenConfig = deepFreeze(deepClone(_config));\r\n }\r\n return frozenConfig;\r\n}\r\n\r\nexport function setConfig(partialConfig: IWritableConfig): void {\r\n if (partialConfig.tagNames) {\r\n Object.assign(_config.tagNames, partialConfig.tagNames);\r\n }\r\n frozenConfig = null;\r\n}\r\n","import {\r\n IWcBindable, WcsMatchMedia, WcsMediaQueryCoreOptions, WcsMediaQueryList, WcsMediaQuerySnapshot,\r\n} from \"../types.js\";\r\n\r\nconst UNSUPPORTED_SNAPSHOT: WcsMediaQuerySnapshot = Object.freeze({\r\n matched: false,\r\n media: \"\",\r\n supported: false,\r\n});\r\n\r\n// \"matchMedia exists but there is nothing to watch\": an empty query, or a\r\n// matchMedia call that threw. Same shape as UNSUPPORTED_SNAPSHOT except that\r\n// `supported` stays honest about the API being present.\r\nconst IDLE_SNAPSHOT: WcsMediaQuerySnapshot = Object.freeze({\r\n matched: false,\r\n media: \"\",\r\n supported: true,\r\n});\r\n\r\n/**\r\n * Headless media-query primitive. A thin, framework-agnostic wrapper around\r\n * `window.matchMedia` exposed through the wc-bindable protocol.\r\n *\r\n * `observe(query)` subscribes to one `MediaQueryList` and republishes its\r\n * `matches` / `media` as `matched` / `media` state; a different query while subscribed tears the\r\n * old list down and subscribes to the new one. Subscribing is synchronous, but\r\n * — unlike `@wcstack/network` — this Core does keep a `_gen` generation guard\r\n * (§3.4): each subscription's `change` listener captures the generation it was\r\n * created under and bails when it is stale, so a `MediaQueryList` whose\r\n * `removeEventListener` / `removeListener` misbehaves can never write the old\r\n * query's `matched` over the new query's (docs/media-query-tag-design.md §6).\r\n *\r\n * `matchMedia` is universally available in browsers; `supported === false` is\r\n * the non-browser case (SSR, workers) rather than a browser quirk.\r\n */\r\nexport class MediaQueryCore extends EventTarget {\r\n static wcBindable: IWcBindable = {\r\n protocol: \"wc-bindable\",\r\n version: 1,\r\n properties: [\r\n { name: \"matched\", event: \"wcs-media-query:change\", semantics: \"state\", getter: (e: Event) => (e as CustomEvent).detail.matched },\r\n { name: \"media\", event: \"wcs-media-query:change\", semantics: \"state\", getter: (e: Event) => (e as CustomEvent).detail.media },\r\n { name: \"supported\", event: \"wcs-media-query:change\", semantics: \"state\", getter: (e: Event) => (e as CustomEvent).detail.supported },\r\n ],\r\n // Pure monitor: a MediaQueryList has no action to invoke.\r\n //\r\n // `matched`, not `matches`: `Element.prototype.matches(selector)` exists on\r\n // every element, and a wc-bindable property name is read straight off the\r\n // Shell — a boolean `matches` would shadow the platform method\r\n // (docs/media-query-tag-design.md §2.1).\r\n commands: [],\r\n };\r\n\r\n private _target: EventTarget;\r\n private _snapshot: WcsMediaQuerySnapshot = UNSUPPORTED_SNAPSHOT;\r\n\r\n // The query currently subscribed (or last requested). \"\" means \"nothing to watch\".\r\n private _query = \"\";\r\n\r\n // Detaches the live `change` listener of the current subscription, if any.\r\n private _unsubscribe: (() => void) | null = null;\r\n\r\n // True between observe() and dispose(). Guards observe() so a redundant call\r\n // with the same query does not re-subscribe; dispose() resets it.\r\n private _subscribed = false;\r\n\r\n // Generation guard (§3.4). Bumped by every (re)subscription and by dispose().\r\n // A `change` listener captures its generation and ignores the event once a\r\n // newer subscription exists — see the class docs for why this is kept even\r\n // though subscribing itself is synchronous.\r\n private _gen = 0;\r\n\r\n // Injected matchMedia (tests / non-window hosts). `null` = resolve\r\n // `globalThis.matchMedia` at call time (§3.7).\r\n private _injectedMatchMedia: WcsMatchMedia | null;\r\n\r\n // SSR (§3.8): no asynchronous probe to await — observe() completes\r\n // synchronously, so readiness is immediate.\r\n private _ready: Promise<void> = Promise.resolve();\r\n\r\n constructor(target?: EventTarget, options?: WcsMediaQueryCoreOptions) {\r\n super();\r\n this._target = target ?? this;\r\n this._injectedMatchMedia = options?.matchMedia ?? null;\r\n }\r\n\r\n get ready(): Promise<void> {\r\n return this._ready;\r\n }\r\n\r\n get query(): string {\r\n return this._query;\r\n }\r\n\r\n get matched(): boolean {\r\n return this._snapshot.matched;\r\n }\r\n\r\n get media(): string {\r\n return this._snapshot.media;\r\n }\r\n\r\n get supported(): boolean {\r\n return this._snapshot.supported;\r\n }\r\n\r\n // Lifecycle (§3.5). Idempotent: observe() with the query already subscribed\r\n // is a no-op (no double listener, no redundant dispatch). A different query\r\n // re-subscribes (dispose-then-observe semantics in one call). Omitting the\r\n // argument keeps the current query — the reconnect case for the Shell.\r\n // Synchronous overall (no probe to await), so the returned promise is only\r\n // for API uniformity with other IO nodes.\r\n observe(query: string = this._query): Promise<void> {\r\n if (this._subscribed && query === this._query) {\r\n return this._ready;\r\n }\r\n this._teardown();\r\n this._query = query;\r\n this._subscribed = true;\r\n this._subscribe(query);\r\n return this._ready;\r\n }\r\n\r\n dispose(): void {\r\n this._subscribed = false;\r\n this._teardown();\r\n }\r\n\r\n // API resolution is call-time, never cached (§3.7): lets tests install/remove\r\n // globalThis.matchMedia freely and lets a non-browser host be detected\r\n // correctly on every observe(). Called as a method of globalThis so the\r\n // native implementation keeps its `this` (calling it unbound throws).\r\n private _resolveMatchMedia(): WcsMatchMedia | null {\r\n if (this._injectedMatchMedia !== null) {\r\n return this._injectedMatchMedia;\r\n }\r\n const g = globalThis as { matchMedia?: WcsMatchMedia };\r\n return typeof g.matchMedia === \"function\" ? (q: string) => g.matchMedia!(q) : null;\r\n }\r\n\r\n private _subscribe(query: string): void {\r\n const gen = ++this._gen;\r\n const matchMedia = this._resolveMatchMedia();\r\n if (matchMedia === null) {\r\n this._apply(UNSUPPORTED_SNAPSHOT);\r\n return;\r\n }\r\n if (query === \"\") {\r\n this._apply(IDLE_SNAPSHOT);\r\n return;\r\n }\r\n // never-throw (§3.6): browsers do not throw on an invalid query string\r\n // (they return `media: \"not all\"`), but a hostile host or a broken\r\n // MediaQueryList polyfill might — that must not kill observe().\r\n let list: WcsMediaQueryList | null = null;\r\n try {\r\n list = matchMedia(query);\r\n const onChange = (): void => {\r\n if (gen !== this._gen) return; // stale subscription — never write\r\n this._apply(this._read(list!));\r\n };\r\n this._unsubscribe = attachChange(list, onChange);\r\n } catch {\r\n list = null;\r\n this._unsubscribe = null;\r\n }\r\n this._apply(list === null ? IDLE_SNAPSHOT : this._read(list));\r\n }\r\n\r\n private _teardown(): void {\r\n this._gen++;\r\n if (this._unsubscribe !== null) {\r\n const unsubscribe = this._unsubscribe;\r\n this._unsubscribe = null;\r\n try {\r\n unsubscribe();\r\n } catch {\r\n // never-throw: a list that refuses to detach is already neutralized\r\n // by the generation bump above.\r\n }\r\n }\r\n }\r\n\r\n private _read(list: WcsMediaQueryList): WcsMediaQuerySnapshot {\r\n return {\r\n matched: list.matches === true,\r\n media: typeof list.media === \"string\" ? list.media : \"\",\r\n supported: true,\r\n };\r\n }\r\n\r\n // Same-value guard (§3.3 MUST): the native `change` event already fires only\r\n // when `matches` flips, but this Core still verifies field-by-field before\r\n // dispatching — a re-subscription to an equivalent query, or a legacy\r\n // `addListener` double-fire, must not produce a redundant event.\r\n private _apply(next: WcsMediaQuerySnapshot): void {\r\n const prev = this._snapshot;\r\n if (\r\n prev.matched === next.matched &&\r\n prev.media === next.media &&\r\n prev.supported === next.supported\r\n ) {\r\n return;\r\n }\r\n this._snapshot = next;\r\n this._target.dispatchEvent(new CustomEvent(\"wcs-media-query:change\", {\r\n detail: next,\r\n // Family-wide MUST (async-io-node-guidelines.md §3.3): the event bubbles\r\n // from the Shell element so document-level consumers can delegate.\r\n bubbles: true,\r\n }));\r\n }\r\n}\r\n\r\n// Subscribe to a MediaQueryList's `change` with whichever listener API it has.\r\n// Modern engines: EventTarget-style. Old Safari (< 14): the deprecated\r\n// addListener / removeListener pair. Neither: no live updates — the snapshot\r\n// taken at observe() is all there is (a static polyfill, for instance).\r\n// Returns the matching detach function.\r\nfunction attachChange(list: WcsMediaQueryList, listener: () => void): () => void {\r\n if (typeof list.addEventListener === \"function\" && typeof list.removeEventListener === \"function\") {\r\n list.addEventListener(\"change\", listener);\r\n return () => list.removeEventListener!(\"change\", listener);\r\n }\r\n if (typeof list.addListener === \"function\" && typeof list.removeListener === \"function\") {\r\n list.addListener(listener);\r\n return () => list.removeListener!(listener);\r\n }\r\n return () => {};\r\n}\r\n","// ===========================================================================\r\n// AUTO-GENERATED FILE - DO NOT EDIT.\r\n// Generated from /protocol/upgrade-properties.ts by scripts/sync-protocol-types.mjs.\r\n// Run `node scripts/sync-protocol-types.mjs` after editing the source.\r\n// ===========================================================================\r\n\r\n// custom element の property upgrade — `static wcBindable.inputs` に宣言した入力のうち、\r\n// 要素が upgrade される前に代入された own データプロパティを取り込み直す。\r\n//\r\n// なぜ必要か:\r\n// 未定義タグの要素は素の HTMLElement なので、`el.url = \"...\"` は own データプロパティを作る。\r\n// upgrade 後にクラスの accessor が prototype へ入っても own プロパティが優先されるため、\r\n// setter は二度と呼ばれず、値は要素へ届かないまま消える(エラーも警告も出ない)。\r\n// 常にプロパティ代入を行う framework(Angular の `[prop]`、Lit の `.prop=`、\r\n// Solid の `prop:`、Vue の `.prop` 修飾子)× 遅延定義(autoloader / CDN / code-split)で\r\n// 常態的に起きる。docs/architecture-hardening/13-framework-adapter-binding-constraints.md §1.2。\r\n//\r\n// 安全側の判定:\r\n// own プロパティがあっても、prototype チェーンに accessor が無ければ「シャドウ」ではなく\r\n// その own プロパティ自体が正規の格納先なので触らない(public class field を壊さない)。\r\n//\r\n// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/upgrade-properties.ts), then run\r\n// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies\r\n// (packages/<pkg>/src/protocol/upgradeProperties.ts). Those copies are generated — do not edit them.\r\nimport { IWcBindable } from \"./wcBindable.js\";\r\n\r\nfunction hasAccessorOnPrototype(target: object, name: string): boolean {\r\n let proto = Object.getPrototypeOf(target);\r\n while (proto !== null) {\r\n const descriptor = Object.getOwnPropertyDescriptor(proto, name);\r\n if (descriptor !== undefined) {\r\n return typeof descriptor.get === \"function\" || typeof descriptor.set === \"function\";\r\n }\r\n proto = Object.getPrototypeOf(proto);\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * `connectedCallback` の先頭で呼ぶ。宣言済み input のうち upgrade 前の代入で\r\n * accessor をシャドウしている own プロパティを、delete → 再代入で setter に通し直す。\r\n *\r\n * - 冪等: 再代入は accessor を通るので own プロパティは残らず、2 回目以降は no-op。\r\n * - 宣言に `inputs` が無い要素、`wcBindable` を持たない要素では何もしない。\r\n * - 値の意味は変えない。今まで捨てられていた代入が届くようになる一方向の変化。\r\n */\r\nexport function upgradeProperties(element: object): void {\r\n const declaration = (element as { constructor?: { wcBindable?: IWcBindable } }).constructor?.wcBindable;\r\n const inputs = declaration?.inputs;\r\n if (inputs === undefined) return;\r\n for (const input of inputs) {\r\n const name = input.name;\r\n if (!Object.prototype.hasOwnProperty.call(element, name)) continue;\r\n if (!hasAccessorOnPrototype(element, name)) continue;\r\n const record = element as Record<string, unknown>;\r\n const value = record[name];\r\n delete record[name];\r\n record[name] = value;\r\n }\r\n}\r\n","import { IWcBindable } from \"../types.js\";\r\nimport { MediaQueryCore } from \"../core/MediaQueryCore.js\";\r\nimport { upgradeProperties } from \"../protocol/upgradeProperties.js\";\r\n\r\n/**\r\n * `<wcs-media-query query=\"(prefers-color-scheme: dark)\">` — declarative\r\n * `matchMedia` monitor.\r\n *\r\n * One attribute (`query`), three outputs (`matched` / `media` / `supported`),\r\n * no commands. Changing `query` while connected re-subscribes the Core to the\r\n * new MediaQueryList (docs/media-query-tag-design.md §7).\r\n */\r\nexport class WcsMediaQuery extends HTMLElement {\r\n // SSR (§4.4): observe() completes synchronously, but the Shell still exposes\r\n // connectedCallbackPromise so SSR (@wcstack/server render.ts) can await it\r\n // uniformly across all IO nodes before snapshotting the HTML.\r\n static hasConnectedCallbackPromise = true;\r\n\r\n static wcBindable: IWcBindable = {\r\n ...MediaQueryCore.wcBindable,\r\n // Shell-level settable surface: the media query string, mirrored to the\r\n // `query` attribute (idempotent reflect, so a binder writing through\r\n // inputs[].attribute is safe).\r\n inputs: [\r\n { name: \"query\", attribute: \"query\" },\r\n ],\r\n // Core の commands をそのまま継承(単一情報源)。network と同型。\r\n commands: MediaQueryCore.wcBindable.commands,\r\n };\r\n\r\n // `query` is the only attribute worth re-subscribing for; it is the whole\r\n // configuration of this node.\r\n static get observedAttributes(): string[] { return [\"query\"]; }\r\n\r\n private _core: MediaQueryCore;\r\n private _connectedCallbackPromise: Promise<void> = Promise.resolve();\r\n private _internals: ElementInternals | null = null;\r\n\r\n constructor() {\r\n super();\r\n this._core = new MediaQueryCore(this);\r\n this._internals = this._initInternals();\r\n this._wireStates({\r\n \"wcs-media-query:change\": (d) => ({\r\n matched: d.matched === true,\r\n supported: d.supported === true,\r\n }),\r\n });\r\n }\r\n\r\n // CSS state reflection (:state()) — debug-only snapshot getter. NOT part of\r\n // wc-bindable (not a bind target); see README \"CSS styling with :state()\".\r\n // MUST NOT return the live CustomStateSet (that would let callers write\r\n // states from outside, defeating the point of :state() being read-only).\r\n get debugStates(): string[] {\r\n return this._internals ? [...this._internals.states] : [];\r\n }\r\n\r\n private _initInternals(): ElementInternals | null {\r\n // never-throw (async-io-node-guidelines.md §3.6): attachInternals is absent\r\n // in happy-dom / older environments, and pre-125 Chromium rejects\r\n // non-dashed state names from states.add() (probed and discarded here).\r\n // Either case silently disables reflection — the component still works,\r\n // it just doesn't expose :state() selectors.\r\n try {\r\n if (typeof this.attachInternals !== \"function\") return null;\r\n const internals = this.attachInternals();\r\n internals.states.add(\"wcs-probe\");\r\n internals.states.delete(\"wcs-probe\");\r\n return internals;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n private _wireStates(map: Record<string, (detail: any) => Record<string, boolean>>): void {\r\n if (this._internals === null) return;\r\n const states = this._internals.states;\r\n for (const [event, toStates] of Object.entries(map)) {\r\n this.addEventListener(event, (e) => {\r\n const debug = this.hasAttribute(\"debug-states\");\r\n for (const [name, on] of Object.entries(toStates((e as CustomEvent).detail))) {\r\n try {\r\n if (on) { states.add(name); } else { states.delete(name); }\r\n } catch { /* never-throw */ }\r\n if (debug) this.toggleAttribute(`data-wcs-state-${name}`, on);\r\n }\r\n });\r\n }\r\n }\r\n\r\n // --- Attribute accessors ---\r\n\r\n get query(): string {\r\n return this.getAttribute(\"query\") ?? \"\";\r\n }\r\n\r\n set query(value: string) {\r\n this.setAttribute(\"query\", value);\r\n }\r\n\r\n // --- Core delegated getters ---\r\n\r\n get matched(): boolean {\r\n return this._core.matched;\r\n }\r\n\r\n get media(): string {\r\n return this._core.media;\r\n }\r\n\r\n get supported(): boolean {\r\n return this._core.supported;\r\n }\r\n\r\n get connectedCallbackPromise(): Promise<void> {\r\n return this._connectedCallbackPromise;\r\n }\r\n\r\n // --- Lifecycle ---\r\n\r\n attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void {\r\n // Re-subscribe on a live query change. Removing the attribute (newValue\r\n // null) is a real change too — it means \"watch nothing\", so `matched`\r\n // drops to false instead of lingering on the old query's value. Before\r\n // connect the attribute is simply read by connectedCallback.\r\n if (name === \"query\" && this.isConnected) {\r\n this._core.observe(newValue ?? \"\");\r\n }\r\n }\r\n\r\n connectedCallback(): void {\r\n // upgrade 前に代入された input を取り込み直す(doc 13 §1.2 / Phase A1)\r\n upgradeProperties(this);\r\n this.style.display = \"none\";\r\n this._connectedCallbackPromise = this._core.observe(this.query);\r\n }\r\n\r\n disconnectedCallback(): void {\r\n this._core.dispose();\r\n }\r\n}\r\n","import { WcsMediaQuery } from \"./components/MediaQuery.js\";\r\nimport { config } from \"./config.js\";\r\n\r\n/**\r\n * Register this package's tags. Pass a scoped `CustomElementRegistry` to define\r\n * them for a single shadow tree -- scoped registries do not inherit the global\r\n * one, so a tree using one needs its own definitions.\r\n */\r\nexport function registerComponents(registry: CustomElementRegistry = customElements): void {\r\n if (!registry.get(config.tagNames.mediaQuery)) {\r\n registry.define(config.tagNames.mediaQuery, WcsMediaQuery);\r\n }\r\n}\r\n","import { setConfig } from \"./config.js\";\r\nimport { registerComponents } from \"./registerComponents.js\";\r\nimport { IWritableConfig } from \"./types.js\";\r\n\r\nexport function bootstrapMediaQuery(userConfig?: IWritableConfig, registry?: CustomElementRegistry): void {\r\n if (userConfig) {\r\n setConfig(userConfig);\r\n }\r\n registerComponents(registry);\r\n}\r\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@wcstack/media-query",
3
+ "version": "2.1.1",
4
+ "description": "Declarative matchMedia component for Web Components. Framework-agnostic media-query monitor (prefers-color-scheme, prefers-reduced-motion, viewport width) 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
+ "matchmedia",
34
+ "media-query",
35
+ "prefers-color-scheme",
36
+ "prefers-reduced-motion",
37
+ "responsive",
38
+ "custom-elements",
39
+ "wc-bindable",
40
+ "declarative",
41
+ "zero-dependencies",
42
+ "framework-agnostic"
43
+ ],
44
+ "author": "mogera551",
45
+ "homepage": "https://wcstack.github.io",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/wcstack/wcstack.git",
49
+ "directory": "packages/media-query"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/wcstack/wcstack/issues"
53
+ },
54
+ "license": "MIT",
55
+ "devDependencies": {
56
+ "@eslint/js": "^9.39.1",
57
+ "@rollup/plugin-terser": "^0.4.4",
58
+ "@rollup/plugin-typescript": "^11.1.6",
59
+ "@vitest/coverage-v8": "^4.0.15",
60
+ "@vitest/ui": "^4.0.15",
61
+ "eslint": "^9.39.1",
62
+ "globals": "^16.5.0",
63
+ "happy-dom": "^20.0.11",
64
+ "rimraf": "^6.0.1",
65
+ "rollup": "^4.22.4",
66
+ "rollup-plugin-dts": "^6.1.1",
67
+ "rollup-plugin-copy": "^3.5.0",
68
+ "tslib": "^2.8.1",
69
+ "typescript": "^5.9.3",
70
+ "typescript-eslint": "^8.49.0",
71
+ "vitest": "^4.0.15"
72
+ }
73
+ }