@wcstack/camera 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -77,6 +77,66 @@ $on: {
77
77
 
78
78
  > **`objectURL` の寿命は recorder に束縛されます。** dispose が最後の object URL を revoke し、**さらに次の録画は前のクリップの URL を新規発行前に revoke する**ため、古い URL を指したままの `<video src>` / `<wcs-upload>` は `<wcs-recorder>` が外れる・次のクリップが完成した時点で壊れます。常に最新の `objectURL` / `recorded` 値を追従し、古い URL を保持しないでください。URL を寿命の長い消費側へ渡す場合は、URL を使い終わるまで recorder を接続したままにするか、`Blob` から自前で URL を作り(`URL.createObjectURL(blob)`)revoke も自分で管理してください。structured-clone 可能な **`blob`** にはこの結合がありません——`Blob` を state に流し、利用箇所で URL を生成するのが安全です。
79
79
 
80
+ ## `:state()` による CSS スタイリング
81
+
82
+ `<wcs-camera>` と `<wcs-recorder>` は boolean 出力ステートを
83
+ [`ElementInternals` の `CustomStateSet`](https://developer.mozilla.org/ja/docs/Web/API/CustomStateSet)
84
+ に反映します。そのため `data-wcs` バインディングやクラスの手動トグルなしに、CSS の
85
+ `:state()` 疑似クラスで直接スタイリングできます。
86
+
87
+ | 要素 | ステート | on になる条件 |
88
+ |------|----------|----------------|
89
+ | `wcs-camera` | `active` | `wcs-camera:active-changed` が `true` で発火(`false` でクリア) |
90
+ | `wcs-camera` | `error` | `wcs-camera:error` が非 `null` の detail で発火(`null` でクリア) |
91
+ | `wcs-recorder` | `recording` | `wcs-recorder:recording-changed` が `true` で発火(`false` でクリア) |
92
+ | `wcs-recorder` | `paused` | `wcs-recorder:paused-changed` が `true` で発火(`false` でクリア) |
93
+ | `wcs-recorder` | `error` | `wcs-recorder:error` が非 `null` の detail で発火(`null` でクリア) |
94
+
95
+ > `permission` / `audioPermission` には現時点で boolean 派生 getter が無いため
96
+ > 反映対象外です(v1 スコープ。docs/custom-state-reflection-design.md §7 参照)。
97
+ > `duration` は連続値のため意図的に除外しています。
98
+
99
+ ```css
100
+ wcs-camera:state(active) ~ .live-badge { display: block; }
101
+ form:has(wcs-camera:state(error)) .banner { display: block; }
102
+
103
+ wcs-recorder:state(recording) ~ .rec-dot { animation: blink 1s infinite; }
104
+ wcs-recorder:state(paused) ~ .rec-dot { animation: none; opacity: .4; }
105
+ ```
106
+
107
+ 属性やクラスと異なり `:state()` は要素の外部から書き込めないため、この出力ステートが
108
+ 入力と混同される心配がありません。
109
+
110
+ **対応ブラウザ**(新構文 `:state(x)`): Chrome/Edge 125+、Safari 17.4+、Firefox 126+。
111
+ 非対応の環境ではステートが一切 set されないだけです — `:state()` セレクタがマッチしなく
112
+ なりますが、各要素自体は通常どおり動作し続けます(graceful degradation・never-throw)。
113
+
114
+ **SSR:** `:state()` は HTML にシリアライズできないため、サーバーレンダリングされた
115
+ マークアップの初期ペイントにはこれらのステートは乗りません(`@wcstack/server` は無改変)。
116
+ ハイドレーション前の見た目を制御したい場合は、代わりに `wcs-camera:not(:defined)` /
117
+ `wcs-recorder:not(:defined)` と組み合わせてください。
118
+
119
+ ### デバッグ
120
+
121
+ カスタムステートは DevTools の Elements パネルには表示されず、`attachInternals()`
122
+ は同一要素に 2 回呼べないため、コンソールから直接覗く手段がありません。そのための
123
+ デバッグ専用の補助を 2 つ用意しています:
124
+
125
+ - `el.debugStates` — 現在 on になっているステート名の**スナップショット**配列
126
+ (例: `["active"]`)。`wc-bindable` の一部ではなく(バインド対象ではない)、
127
+ 形状も契約として保証されません — デバッグ用途にのみ使ってください。
128
+ - `debug-states` 属性(opt-in・既定 OFF)は、ステート変化を要素の
129
+ `data-wcs-state-*` 属性にミラーします。Elements パネルを開いておけば、
130
+ トグルのたびにハイライトされます:
131
+
132
+ ```html
133
+ <wcs-camera autostart debug-states></wcs-camera>
134
+ ```
135
+
136
+ **CSS は `data-wcs-state-*` ではなく `:state()` に書いてください。** ミラーされた
137
+ 属性は、DevTools を開いた状態でステート変化を可視化するためだけのものであり、
138
+ スタイリング用の正式なフックではありません。
139
+
80
140
  ## ヘッドレス Core
81
141
 
82
142
  `CameraCore` / `RecorderCore` を非 DOM 用途向けにエクスポートしています(`@wc-bindable/core` の `bind()`)。Shell は薄いラッパです。
package/README.md CHANGED
@@ -77,6 +77,67 @@ The assembled `Blob` is structured-clone friendly, so it *is* a value and may fl
77
77
 
78
78
  > **`objectURL` lifetime is bound to the recorder.** Because dispose revokes the last object URL — **and a new recording revokes the previous clip's URL before minting the next** — any `<video src>` / `<wcs-upload>` still pointing at an old URL breaks once the `<wcs-recorder>` is removed or the next clip completes. Always follow the latest `objectURL` / `recorded` value; never pin a stale one. If you hand the URL to a longer-lived consumer, either keep the recorder mounted for as long as the URL is in use, or build your own URL from the `Blob` (`URL.createObjectURL(blob)`) and own its revoke. The structured-clone-friendly **`blob`** has no such coupling — prefer flowing the `Blob` through state and minting URLs at the point of use.
79
79
 
80
+ ## CSS styling with `:state()`
81
+
82
+ `<wcs-camera>` and `<wcs-recorder>` reflect their boolean output states onto
83
+ [`ElementInternals` `CustomStateSet`](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet),
84
+ so you can style them directly from CSS with the `:state()` pseudo-class — no
85
+ `data-wcs` binding or extra class toggling required.
86
+
87
+ | Element | State | On when |
88
+ |---------|-------|---------|
89
+ | `wcs-camera` | `active` | `wcs-camera:active-changed` fires with `true` (cleared on `false`) |
90
+ | `wcs-camera` | `error` | `wcs-camera:error` fires with a non-`null` detail (cleared on `null`) |
91
+ | `wcs-recorder` | `recording` | `wcs-recorder:recording-changed` fires with `true` (cleared on `false`) |
92
+ | `wcs-recorder` | `paused` | `wcs-recorder:paused-changed` fires with `true` (cleared on `false`) |
93
+ | `wcs-recorder` | `error` | `wcs-recorder:error` fires with a non-`null` detail (cleared on `null`) |
94
+
95
+ > `permission` / `audioPermission` have no boolean derived getter today, so they
96
+ > are not reflected (v1 scope; see docs/custom-state-reflection-design.md §7).
97
+ > `duration` is a continuous value and is intentionally excluded.
98
+
99
+ ```css
100
+ wcs-camera:state(active) ~ .live-badge { display: block; }
101
+ form:has(wcs-camera:state(error)) .banner { display: block; }
102
+
103
+ wcs-recorder:state(recording) ~ .rec-dot { animation: blink 1s infinite; }
104
+ wcs-recorder:state(paused) ~ .rec-dot { animation: none; opacity: .4; }
105
+ ```
106
+
107
+ Unlike attributes or classes, `:state()` cannot be written from outside the
108
+ element, so there is no risk of confusing this output state with an input.
109
+
110
+ **Browser support** (`:state(x)` syntax): Chrome/Edge 125+, Safari 17.4+,
111
+ Firefox 126+. In older browsers the states are simply never set — `:state()`
112
+ selectors never match, but the elements themselves keep working normally
113
+ (graceful degradation, never-throw).
114
+
115
+ **SSR**: `:state()` cannot be serialized into HTML, so server-rendered markup
116
+ never carries these states on first paint (`@wcstack/server` is unaffected).
117
+ If you need to style the pre-hydration gap, pair your rule with
118
+ `wcs-camera:not(:defined)` / `wcs-recorder:not(:defined)` instead.
119
+
120
+ ### Debugging
121
+
122
+ Custom states are invisible in DevTools' Elements panel and `attachInternals()`
123
+ cannot be called twice, so there is no console way to inspect them directly.
124
+ Two debug-only aids are provided for that:
125
+
126
+ - `el.debugStates` — a **snapshot** array of the currently-on state names
127
+ (e.g. `["active"]`). It is not part of `wc-bindable` (not a bind target)
128
+ and its shape is not a guaranteed contract — use it for debugging only.
129
+ - The `debug-states` attribute (opt-in, default off) mirrors state changes
130
+ onto `data-wcs-state-*` attributes on the element, so the Elements panel
131
+ highlights them as they toggle:
132
+
133
+ ```html
134
+ <wcs-camera autostart debug-states></wcs-camera>
135
+ ```
136
+
137
+ **Write your CSS against `:state()`, not `data-wcs-state-*`.** The mirrored
138
+ attributes exist purely to make state changes visible while debugging with
139
+ DevTools open; they are not a supported styling hook.
140
+
80
141
  ## Headless cores
81
142
 
82
143
  `CameraCore` and `RecorderCore` are exported for non-DOM use (`bind()` from `@wc-bindable/core`). The Shells are thin wrappers.
package/dist/index.d.ts CHANGED
@@ -299,7 +299,11 @@ declare class WcsCamera extends HTMLElement {
299
299
  private _connectedCallbackPromise;
300
300
  private _connected;
301
301
  private _batchingAttrs;
302
+ private _internals;
302
303
  constructor();
304
+ get debugStates(): string[];
305
+ private _initInternals;
306
+ private _wireStates;
303
307
  get audio(): boolean;
304
308
  set audio(value: boolean);
305
309
  get facingMode(): FacingMode;
@@ -451,7 +455,11 @@ declare class WcsRecorder extends HTMLElement {
451
455
  static wcBindable: IWcBindable;
452
456
  private _core;
453
457
  private _connectedCallbackPromise;
458
+ private _internals;
454
459
  constructor();
460
+ get debugStates(): string[];
461
+ private _initInternals;
462
+ private _wireStates;
455
463
  get connectedCallbackPromise(): Promise<void>;
456
464
  get mimeType(): string;
457
465
  set mimeType(value: string);
package/dist/index.esm.js CHANGED
@@ -583,6 +583,7 @@ class WcsCamera extends HTMLElement {
583
583
  // tear active down so the later change's re-acquire is skipped). We suppress the
584
584
  // per-attribute re-acquire and drive a single one with the final constraints.
585
585
  _batchingAttrs = false;
586
+ _internals = null;
586
587
  constructor() {
587
588
  super();
588
589
  this._core = new CameraCore(this);
@@ -603,6 +604,60 @@ class WcsCamera extends HTMLElement {
603
604
  // `document` (outlives the element) and MUST be detached.
604
605
  this.addEventListener("wcs-camera:stream-ready", this._onStreamReady);
605
606
  this.addEventListener("wcs-camera:active-changed", this._onActiveChanged);
607
+ this._internals = this._initInternals();
608
+ this._wireStates({
609
+ "wcs-camera:active-changed": (d) => ({ active: d === true }),
610
+ "wcs-camera:error": (d) => ({ error: d != null }),
611
+ });
612
+ }
613
+ // CSS state reflection (:state()) — debug-only snapshot getter. NOT part of
614
+ // wc-bindable (not a bind target); see README "CSS styling with :state()".
615
+ // MUST NOT return the live CustomStateSet (that would let callers write
616
+ // states from outside, defeating the point of :state() being read-only).
617
+ get debugStates() {
618
+ return this._internals ? [...this._internals.states] : [];
619
+ }
620
+ _initInternals() {
621
+ // never-throw (docs/custom-state-reflection-design.md §3.4): attachInternals is
622
+ // absent in happy-dom / older environments, and pre-125 Chromium rejects
623
+ // non-dashed state names from states.add() (probed and discarded here). Either
624
+ // case silently disables reflection — the component still works, it just doesn't
625
+ // expose :state() selectors.
626
+ try {
627
+ if (typeof this.attachInternals !== "function")
628
+ return null;
629
+ const internals = this.attachInternals();
630
+ internals.states.add("wcs-probe");
631
+ internals.states.delete("wcs-probe");
632
+ return internals;
633
+ }
634
+ catch {
635
+ return null;
636
+ }
637
+ }
638
+ _wireStates(map) {
639
+ if (this._internals === null)
640
+ return;
641
+ const states = this._internals.states;
642
+ for (const [event, toStates] of Object.entries(map)) {
643
+ this.addEventListener(event, (e) => {
644
+ const debug = this.hasAttribute("debug-states");
645
+ for (const [name, on] of Object.entries(toStates(e.detail))) {
646
+ try {
647
+ // 式文の三項演算子は ESLint no-unused-expressions に抵触するため if/else。
648
+ if (on) {
649
+ states.add(name);
650
+ }
651
+ else {
652
+ states.delete(name);
653
+ }
654
+ }
655
+ catch { /* never-throw */ }
656
+ if (debug)
657
+ this.toggleAttribute(`data-wcs-state-${name}`, on);
658
+ }
659
+ });
660
+ }
606
661
  }
607
662
  // --- Attribute accessors ---
608
663
  get audio() { return this.hasAttribute("audio"); }
@@ -1080,9 +1135,65 @@ class WcsRecorder extends HTMLElement {
1080
1135
  };
1081
1136
  _core;
1082
1137
  _connectedCallbackPromise = Promise.resolve();
1138
+ _internals = null;
1083
1139
  constructor() {
1084
1140
  super();
1085
1141
  this._core = new RecorderCore(this);
1142
+ this._internals = this._initInternals();
1143
+ this._wireStates({
1144
+ "wcs-recorder:recording-changed": (d) => ({ recording: d === true }),
1145
+ "wcs-recorder:paused-changed": (d) => ({ paused: d === true }),
1146
+ "wcs-recorder:error": (d) => ({ error: d != null }),
1147
+ });
1148
+ }
1149
+ // CSS state reflection (:state()) — debug-only snapshot getter. NOT part of
1150
+ // wc-bindable (not a bind target); see README "CSS styling with :state()".
1151
+ // MUST NOT return the live CustomStateSet (that would let callers write
1152
+ // states from outside, defeating the point of :state() being read-only).
1153
+ get debugStates() {
1154
+ return this._internals ? [...this._internals.states] : [];
1155
+ }
1156
+ _initInternals() {
1157
+ // never-throw (docs/custom-state-reflection-design.md §3.4): attachInternals is
1158
+ // absent in happy-dom / older environments, and pre-125 Chromium rejects
1159
+ // non-dashed state names from states.add() (probed and discarded here). Either
1160
+ // case silently disables reflection — the component still works, it just doesn't
1161
+ // expose :state() selectors.
1162
+ try {
1163
+ if (typeof this.attachInternals !== "function")
1164
+ return null;
1165
+ const internals = this.attachInternals();
1166
+ internals.states.add("wcs-probe");
1167
+ internals.states.delete("wcs-probe");
1168
+ return internals;
1169
+ }
1170
+ catch {
1171
+ return null;
1172
+ }
1173
+ }
1174
+ _wireStates(map) {
1175
+ if (this._internals === null)
1176
+ return;
1177
+ const states = this._internals.states;
1178
+ for (const [event, toStates] of Object.entries(map)) {
1179
+ this.addEventListener(event, (e) => {
1180
+ const debug = this.hasAttribute("debug-states");
1181
+ for (const [name, on] of Object.entries(toStates(e.detail))) {
1182
+ try {
1183
+ // 式文の三項演算子は ESLint no-unused-expressions に抵触するため if/else。
1184
+ if (on) {
1185
+ states.add(name);
1186
+ }
1187
+ else {
1188
+ states.delete(name);
1189
+ }
1190
+ }
1191
+ catch { /* never-throw */ }
1192
+ if (debug)
1193
+ this.toggleAttribute(`data-wcs-state-${name}`, on);
1194
+ }
1195
+ });
1196
+ }
1086
1197
  }
1087
1198
  get connectedCallbackPromise() {
1088
1199
  return this._connectedCallbackPromise;