@wcstack/media-query 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +218 -218
- package/README.md +220 -220
- package/dist/auto.min.js.map +1 -1
- package/dist/index.esm.js.map +1 -1
- package/package.json +73 -73
package/README.md
CHANGED
|
@@ -1,220 +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.
|
|
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
|
|
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.2.0/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
|
package/dist/auto.min.js.map
CHANGED
|
@@ -1 +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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"}
|
|
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\";\n\ninterface IInternalConfig extends IConfig {\n tagNames: {\n mediaQuery: string;\n };\n}\n\nconst _config: IInternalConfig = {\n tagNames: {\n mediaQuery: \"wcs-media-query\",\n },\n};\n\nfunction deepFreeze<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n Object.freeze(obj);\n for (const key of Object.keys(obj)) {\n deepFreeze((obj as Record<string, unknown>)[key]);\n }\n return obj;\n}\n\nfunction deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n const clone: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n clone[key] = deepClone((obj as Record<string, unknown>)[key]);\n }\n return clone as T;\n}\n\nlet frozenConfig: IConfig | null = null;\n\nexport const config: IConfig = _config as IConfig;\n\nexport function getConfig(): IConfig {\n if (!frozenConfig) {\n frozenConfig = deepFreeze(deepClone(_config));\n }\n return frozenConfig;\n}\n\nexport function setConfig(partialConfig: IWritableConfig): void {\n if (partialConfig.tagNames) {\n Object.assign(_config.tagNames, partialConfig.tagNames);\n }\n frozenConfig = null;\n}\n","import {\n IWcBindable, WcsMatchMedia, WcsMediaQueryCoreOptions, WcsMediaQueryList, WcsMediaQuerySnapshot,\n} from \"../types.js\";\n\nconst UNSUPPORTED_SNAPSHOT: WcsMediaQuerySnapshot = Object.freeze({\n matched: false,\n media: \"\",\n supported: false,\n});\n\n// \"matchMedia exists but there is nothing to watch\": an empty query, or a\n// matchMedia call that threw. Same shape as UNSUPPORTED_SNAPSHOT except that\n// `supported` stays honest about the API being present.\nconst IDLE_SNAPSHOT: WcsMediaQuerySnapshot = Object.freeze({\n matched: false,\n media: \"\",\n supported: true,\n});\n\n/**\n * Headless media-query primitive. A thin, framework-agnostic wrapper around\n * `window.matchMedia` exposed through the wc-bindable protocol.\n *\n * `observe(query)` subscribes to one `MediaQueryList` and republishes its\n * `matches` / `media` as `matched` / `media` state; a different query while subscribed tears the\n * old list down and subscribes to the new one. Subscribing is synchronous, but\n * — unlike `@wcstack/network` — this Core does keep a `_gen` generation guard\n * (§3.4): each subscription's `change` listener captures the generation it was\n * created under and bails when it is stale, so a `MediaQueryList` whose\n * `removeEventListener` / `removeListener` misbehaves can never write the old\n * query's `matched` over the new query's (docs/media-query-tag-design.md §6).\n *\n * `matchMedia` is universally available in browsers; `supported === false` is\n * the non-browser case (SSR, workers) rather than a browser quirk.\n */\nexport class MediaQueryCore extends EventTarget {\n static wcBindable: IWcBindable = {\n protocol: \"wc-bindable\",\n version: 1,\n properties: [\n { name: \"matched\", event: \"wcs-media-query:change\", semantics: \"state\", getter: (e: Event) => (e as CustomEvent).detail.matched },\n { name: \"media\", event: \"wcs-media-query:change\", semantics: \"state\", getter: (e: Event) => (e as CustomEvent).detail.media },\n { name: \"supported\", event: \"wcs-media-query:change\", semantics: \"state\", getter: (e: Event) => (e as CustomEvent).detail.supported },\n ],\n // Pure monitor: a MediaQueryList has no action to invoke.\n //\n // `matched`, not `matches`: `Element.prototype.matches(selector)` exists on\n // every element, and a wc-bindable property name is read straight off the\n // Shell — a boolean `matches` would shadow the platform method\n // (docs/media-query-tag-design.md §2.1).\n commands: [],\n };\n\n private _target: EventTarget;\n private _snapshot: WcsMediaQuerySnapshot = UNSUPPORTED_SNAPSHOT;\n\n // The query currently subscribed (or last requested). \"\" means \"nothing to watch\".\n private _query = \"\";\n\n // Detaches the live `change` listener of the current subscription, if any.\n private _unsubscribe: (() => void) | null = null;\n\n // True between observe() and dispose(). Guards observe() so a redundant call\n // with the same query does not re-subscribe; dispose() resets it.\n private _subscribed = false;\n\n // Generation guard (§3.4). Bumped by every (re)subscription and by dispose().\n // A `change` listener captures its generation and ignores the event once a\n // newer subscription exists — see the class docs for why this is kept even\n // though subscribing itself is synchronous.\n private _gen = 0;\n\n // Injected matchMedia (tests / non-window hosts). `null` = resolve\n // `globalThis.matchMedia` at call time (§3.7).\n private _injectedMatchMedia: WcsMatchMedia | null;\n\n // SSR (§3.8): no asynchronous probe to await — observe() completes\n // synchronously, so readiness is immediate.\n private _ready: Promise<void> = Promise.resolve();\n\n constructor(target?: EventTarget, options?: WcsMediaQueryCoreOptions) {\n super();\n this._target = target ?? this;\n this._injectedMatchMedia = options?.matchMedia ?? null;\n }\n\n get ready(): Promise<void> {\n return this._ready;\n }\n\n get query(): string {\n return this._query;\n }\n\n get matched(): boolean {\n return this._snapshot.matched;\n }\n\n get media(): string {\n return this._snapshot.media;\n }\n\n get supported(): boolean {\n return this._snapshot.supported;\n }\n\n // Lifecycle (§3.5). Idempotent: observe() with the query already subscribed\n // is a no-op (no double listener, no redundant dispatch). A different query\n // re-subscribes (dispose-then-observe semantics in one call). Omitting the\n // argument keeps the current query — the reconnect case for the Shell.\n // Synchronous overall (no probe to await), so the returned promise is only\n // for API uniformity with other IO nodes.\n observe(query: string = this._query): Promise<void> {\n if (this._subscribed && query === this._query) {\n return this._ready;\n }\n this._teardown();\n this._query = query;\n this._subscribed = true;\n this._subscribe(query);\n return this._ready;\n }\n\n dispose(): void {\n this._subscribed = false;\n this._teardown();\n }\n\n // API resolution is call-time, never cached (§3.7): lets tests install/remove\n // globalThis.matchMedia freely and lets a non-browser host be detected\n // correctly on every observe(). Called as a method of globalThis so the\n // native implementation keeps its `this` (calling it unbound throws).\n private _resolveMatchMedia(): WcsMatchMedia | null {\n if (this._injectedMatchMedia !== null) {\n return this._injectedMatchMedia;\n }\n const g = globalThis as { matchMedia?: WcsMatchMedia };\n return typeof g.matchMedia === \"function\" ? (q: string) => g.matchMedia!(q) : null;\n }\n\n private _subscribe(query: string): void {\n const gen = ++this._gen;\n const matchMedia = this._resolveMatchMedia();\n if (matchMedia === null) {\n this._apply(UNSUPPORTED_SNAPSHOT);\n return;\n }\n if (query === \"\") {\n this._apply(IDLE_SNAPSHOT);\n return;\n }\n // never-throw (§3.6): browsers do not throw on an invalid query string\n // (they return `media: \"not all\"`), but a hostile host or a broken\n // MediaQueryList polyfill might — that must not kill observe().\n let list: WcsMediaQueryList | null = null;\n try {\n list = matchMedia(query);\n const onChange = (): void => {\n if (gen !== this._gen) return; // stale subscription — never write\n this._apply(this._read(list!));\n };\n this._unsubscribe = attachChange(list, onChange);\n } catch {\n list = null;\n this._unsubscribe = null;\n }\n this._apply(list === null ? IDLE_SNAPSHOT : this._read(list));\n }\n\n private _teardown(): void {\n this._gen++;\n if (this._unsubscribe !== null) {\n const unsubscribe = this._unsubscribe;\n this._unsubscribe = null;\n try {\n unsubscribe();\n } catch {\n // never-throw: a list that refuses to detach is already neutralized\n // by the generation bump above.\n }\n }\n }\n\n private _read(list: WcsMediaQueryList): WcsMediaQuerySnapshot {\n return {\n matched: list.matches === true,\n media: typeof list.media === \"string\" ? list.media : \"\",\n supported: true,\n };\n }\n\n // Same-value guard (§3.3 MUST): the native `change` event already fires only\n // when `matches` flips, but this Core still verifies field-by-field before\n // dispatching — a re-subscription to an equivalent query, or a legacy\n // `addListener` double-fire, must not produce a redundant event.\n private _apply(next: WcsMediaQuerySnapshot): void {\n const prev = this._snapshot;\n if (\n prev.matched === next.matched &&\n prev.media === next.media &&\n prev.supported === next.supported\n ) {\n return;\n }\n this._snapshot = next;\n this._target.dispatchEvent(new CustomEvent(\"wcs-media-query:change\", {\n detail: next,\n // Family-wide MUST (async-io-node-guidelines.md §3.3): the event bubbles\n // from the Shell element so document-level consumers can delegate.\n bubbles: true,\n }));\n }\n}\n\n// Subscribe to a MediaQueryList's `change` with whichever listener API it has.\n// Modern engines: EventTarget-style. Old Safari (< 14): the deprecated\n// addListener / removeListener pair. Neither: no live updates — the snapshot\n// taken at observe() is all there is (a static polyfill, for instance).\n// Returns the matching detach function.\nfunction attachChange(list: WcsMediaQueryList, listener: () => void): () => void {\n if (typeof list.addEventListener === \"function\" && typeof list.removeEventListener === \"function\") {\n list.addEventListener(\"change\", listener);\n return () => list.removeEventListener!(\"change\", listener);\n }\n if (typeof list.addListener === \"function\" && typeof list.removeListener === \"function\") {\n list.addListener(listener);\n return () => list.removeListener!(listener);\n }\n return () => {};\n}\n","// ===========================================================================\n// AUTO-GENERATED FILE - DO NOT EDIT.\n// Generated from /protocol/upgrade-properties.ts by scripts/sync-protocol-types.mjs.\n// Run `node scripts/sync-protocol-types.mjs` after editing the source.\n// ===========================================================================\n\n// custom element の property upgrade — `static wcBindable.inputs` に宣言した入力のうち、\n// 要素が upgrade される前に代入された own データプロパティを取り込み直す。\n//\n// なぜ必要か:\n// 未定義タグの要素は素の HTMLElement なので、`el.url = \"...\"` は own データプロパティを作る。\n// upgrade 後にクラスの accessor が prototype へ入っても own プロパティが優先されるため、\n// setter は二度と呼ばれず、値は要素へ届かないまま消える(エラーも警告も出ない)。\n// 常にプロパティ代入を行う framework(Angular の `[prop]`、Lit の `.prop=`、\n// Solid の `prop:`、Vue の `.prop` 修飾子)× 遅延定義(autoloader / CDN / code-split)で\n// 常態的に起きる。docs/architecture-hardening/13-framework-adapter-binding-constraints.md §1.2。\n//\n// 安全側の判定:\n// own プロパティがあっても、prototype チェーンに accessor が無ければ「シャドウ」ではなく\n// その own プロパティ自体が正規の格納先なので触らない(public class field を壊さない)。\n//\n// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/upgrade-properties.ts), then run\n// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies\n// (packages/<pkg>/src/protocol/upgradeProperties.ts). Those copies are generated — do not edit them.\nimport { IWcBindable } from \"./wcBindable.js\";\n\nfunction hasAccessorOnPrototype(target: object, name: string): boolean {\n let proto = Object.getPrototypeOf(target);\n while (proto !== null) {\n const descriptor = Object.getOwnPropertyDescriptor(proto, name);\n if (descriptor !== undefined) {\n return typeof descriptor.get === \"function\" || typeof descriptor.set === \"function\";\n }\n proto = Object.getPrototypeOf(proto);\n }\n return false;\n}\n\n/**\n * `connectedCallback` の先頭で呼ぶ。宣言済み input のうち upgrade 前の代入で\n * accessor をシャドウしている own プロパティを、delete → 再代入で setter に通し直す。\n *\n * - 冪等: 再代入は accessor を通るので own プロパティは残らず、2 回目以降は no-op。\n * - 宣言に `inputs` が無い要素、`wcBindable` を持たない要素では何もしない。\n * - 値の意味は変えない。今まで捨てられていた代入が届くようになる一方向の変化。\n */\nexport function upgradeProperties(element: object): void {\n const declaration = (element as { constructor?: { wcBindable?: IWcBindable } }).constructor?.wcBindable;\n const inputs = declaration?.inputs;\n if (inputs === undefined) return;\n for (const input of inputs) {\n const name = input.name;\n if (!Object.prototype.hasOwnProperty.call(element, name)) continue;\n if (!hasAccessorOnPrototype(element, name)) continue;\n const record = element as Record<string, unknown>;\n const value = record[name];\n delete record[name];\n record[name] = value;\n }\n}\n","import { IWcBindable } from \"../types.js\";\nimport { MediaQueryCore } from \"../core/MediaQueryCore.js\";\nimport { upgradeProperties } from \"../protocol/upgradeProperties.js\";\n\n/**\n * `<wcs-media-query query=\"(prefers-color-scheme: dark)\">` — declarative\n * `matchMedia` monitor.\n *\n * One attribute (`query`), three outputs (`matched` / `media` / `supported`),\n * no commands. Changing `query` while connected re-subscribes the Core to the\n * new MediaQueryList (docs/media-query-tag-design.md §7).\n */\nexport class WcsMediaQuery extends HTMLElement {\n // SSR (§4.4): observe() completes synchronously, but the Shell still exposes\n // connectedCallbackPromise so SSR (@wcstack/server render.ts) can await it\n // uniformly across all IO nodes before snapshotting the HTML.\n static hasConnectedCallbackPromise = true;\n\n static wcBindable: IWcBindable = {\n ...MediaQueryCore.wcBindable,\n // Shell-level settable surface: the media query string, mirrored to the\n // `query` attribute (idempotent reflect, so a binder writing through\n // inputs[].attribute is safe).\n inputs: [\n { name: \"query\", attribute: \"query\" },\n ],\n // Core の commands をそのまま継承(単一情報源)。network と同型。\n commands: MediaQueryCore.wcBindable.commands,\n };\n\n // `query` is the only attribute worth re-subscribing for; it is the whole\n // configuration of this node.\n static get observedAttributes(): string[] { return [\"query\"]; }\n\n private _core: MediaQueryCore;\n private _connectedCallbackPromise: Promise<void> = Promise.resolve();\n private _internals: ElementInternals | null = null;\n\n constructor() {\n super();\n this._core = new MediaQueryCore(this);\n this._internals = this._initInternals();\n this._wireStates({\n \"wcs-media-query:change\": (d) => ({\n matched: d.matched === true,\n supported: d.supported === true,\n }),\n });\n }\n\n // CSS state reflection (:state()) — debug-only snapshot getter. NOT part of\n // wc-bindable (not a bind target); see README \"CSS styling with :state()\".\n // MUST NOT return the live CustomStateSet (that would let callers write\n // states from outside, defeating the point of :state() being read-only).\n get debugStates(): string[] {\n return this._internals ? [...this._internals.states] : [];\n }\n\n private _initInternals(): ElementInternals | null {\n // never-throw (async-io-node-guidelines.md §3.6): attachInternals is absent\n // in happy-dom / older environments, and pre-125 Chromium rejects\n // non-dashed state names from states.add() (probed and discarded here).\n // Either case silently disables reflection — the component still works,\n // it just doesn't expose :state() selectors.\n try {\n if (typeof this.attachInternals !== \"function\") return null;\n const internals = this.attachInternals();\n internals.states.add(\"wcs-probe\");\n internals.states.delete(\"wcs-probe\");\n return internals;\n } catch {\n return null;\n }\n }\n\n private _wireStates(map: Record<string, (detail: any) => Record<string, boolean>>): void {\n if (this._internals === null) return;\n const states = this._internals.states;\n for (const [event, toStates] of Object.entries(map)) {\n this.addEventListener(event, (e) => {\n const debug = this.hasAttribute(\"debug-states\");\n for (const [name, on] of Object.entries(toStates((e as CustomEvent).detail))) {\n try {\n if (on) { states.add(name); } else { states.delete(name); }\n } catch { /* never-throw */ }\n if (debug) this.toggleAttribute(`data-wcs-state-${name}`, on);\n }\n });\n }\n }\n\n // --- Attribute accessors ---\n\n get query(): string {\n return this.getAttribute(\"query\") ?? \"\";\n }\n\n set query(value: string) {\n this.setAttribute(\"query\", value);\n }\n\n // --- Core delegated getters ---\n\n get matched(): boolean {\n return this._core.matched;\n }\n\n get media(): string {\n return this._core.media;\n }\n\n get supported(): boolean {\n return this._core.supported;\n }\n\n get connectedCallbackPromise(): Promise<void> {\n return this._connectedCallbackPromise;\n }\n\n // --- Lifecycle ---\n\n attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void {\n // Re-subscribe on a live query change. Removing the attribute (newValue\n // null) is a real change too — it means \"watch nothing\", so `matched`\n // drops to false instead of lingering on the old query's value. Before\n // connect the attribute is simply read by connectedCallback.\n if (name === \"query\" && this.isConnected) {\n this._core.observe(newValue ?? \"\");\n }\n }\n\n connectedCallback(): void {\n // upgrade 前に代入された input を取り込み直す(doc 13 §1.2 / Phase A1)\n upgradeProperties(this);\n this.style.display = \"none\";\n this._connectedCallbackPromise = this._core.observe(this.query);\n }\n\n disconnectedCallback(): void {\n this._core.dispose();\n }\n}\n","import { setConfig } from \"./config.js\";\nimport { registerComponents } from \"./registerComponents.js\";\nimport { IWritableConfig } from \"./types.js\";\n\nexport function bootstrapMediaQuery(userConfig?: IWritableConfig, registry?: CustomElementRegistry): void {\n if (userConfig) {\n setConfig(userConfig);\n }\n registerComponents(registry);\n}\n","import { WcsMediaQuery } from \"./components/MediaQuery.js\";\nimport { config } from \"./config.js\";\n\n/**\n * Register this package's tags. Pass a scoped `CustomElementRegistry` to define\n * them for a single shadow tree -- scoped registries do not inherit the global\n * one, so a tree using one needs its own definitions.\n */\nexport function registerComponents(registry: CustomElementRegistry = customElements): void {\n if (!registry.get(config.tagNames.mediaQuery)) {\n registry.define(config.tagNames.mediaQuery, WcsMediaQuery);\n }\n}\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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"}
|