@fusion.dev/helix-headless 0.211.1 → 0.212.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.
@@ -0,0 +1,869 @@
1
+ import { __decorate } from "tslib";
2
+ import { auditName, ChildrenController, customElement, drawIcon, FullscreenController, HelixDeviceButtonEvent, HelixDeviceKeyEvent, HelixDeviceScreenshotEvent, HelixDeviceScrollEvent, HelixDeviceSwipeEvent, HelixDeviceTapEvent, HelixDeviceTextEvent, HelixDeviceTouchEvent, HelixElement, hostAria, numberConverter, SPOKEN_RULE, stopPress, TOUCHES_RULE, ToolbarController, words, } from '@fusion.dev/helix-core';
3
+ import { arrowClockwise, arrowCounterClockwise, camera, caretLeft, cornersOut, house, power, speakerHigh, speakerLow, square, } from '@fusion.dev/helix-icons';
4
+ import { nothing } from 'lit';
5
+ import { property, state } from 'lit/decorators.js';
6
+ import { repeat } from 'lit/directives/repeat.js';
7
+ import { html } from 'lit/static-html.js';
8
+ import '../../icon-button/index.js';
9
+ import { gestureOf } from '../gesture.js';
10
+ import { quarterTurns, turn, unturn, unturnDistance } from '../turn.js';
11
+ const MARK_SHOWS = 700;
12
+ const LINE = 16;
13
+ const RESTING_CONTROLS = 'home screenshot';
14
+ const leaves = (event) => event.key === 'Tab' && !event.ctrlKey && !event.metaKey && !event.altKey;
15
+ const pastes = (event) => (event.ctrlKey || event.metaKey) &&
16
+ !event.altKey &&
17
+ (event.code === 'KeyV' || event.key.toLowerCase() === 'v');
18
+ let HelixHeadlessDeviceScreen = class HelixHeadlessDeviceScreen extends HelixElement {
19
+ constructor() {
20
+ super(...arguments);
21
+ this.backLabel = 'Back';
22
+ this.controlsLabel = 'Device controls';
23
+ this.fallbackLabel = 'Device';
24
+ this.forwardShortcuts = false;
25
+ this.fullscreenLabel = 'Fill the screen';
26
+ this.homeLabel = 'Home';
27
+ this.keysHintLabel = 'Keys you press go to the device. Press Tab to leave the screen.';
28
+ this.label = '';
29
+ this.loading = false;
30
+ this.loadingLabel = 'Connecting to the device';
31
+ this.powerLabel = 'Power';
32
+ this.readonly = false;
33
+ this.recentsLabel = 'Recent apps';
34
+ this.roleLabel = 'screen';
35
+ this.rotateLeftLabel = 'Rotate left';
36
+ this.rotateRightLabel = 'Rotate right';
37
+ this.rotation = 0;
38
+ this.screenshotLabel = 'Take a screenshot';
39
+ this.showTouches = false;
40
+ this.volumeDownLabel = 'Volume down';
41
+ this.volumeUpLabel = 'Volume up';
42
+ this.filled = false;
43
+ this.intrinsic = undefined;
44
+ this.marks = [];
45
+ this.fingers = new Map();
46
+ this.crowd = 0;
47
+ this.held = new Map();
48
+ this.heldSince = 0;
49
+ this.nextMark = 0;
50
+ this.timers = new Set();
51
+ this.childWatch = new ChildrenController(this);
52
+ this.controlKeys = new ToolbarController(this, {
53
+ arrows: () => (this.column ? 'vertical' : 'horizontal'),
54
+ members: () => [...(this.partNamed('controls')?.children ?? [])],
55
+ own: true,
56
+ watch: () => this.renderRoot,
57
+ });
58
+ this.fullscreen = new FullscreenController(this, (full) => {
59
+ this.filled = full;
60
+ });
61
+ this.handleMeasure = () => {
62
+ this.measure();
63
+ };
64
+ this.handleSlotChange = () => {
65
+ this.requestUpdate();
66
+ };
67
+ this.handlePointerDown = (event) => {
68
+ if (this.readonly || (event.pointerType !== 'touch' && event.button !== 0)) {
69
+ return;
70
+ }
71
+ const screen = this.screen;
72
+ const box = screen?.getBoundingClientRect();
73
+ const drawn = this.fractionAt(event, false, box);
74
+ if (!(drawn && screen && box)) {
75
+ return;
76
+ }
77
+ event.preventDefault();
78
+ screen.focus({ preventScroll: true });
79
+ try {
80
+ screen.setPointerCapture(event.pointerId);
81
+ }
82
+ catch {
83
+ }
84
+ const mirror = event.altKey && event.pointerType !== 'touch';
85
+ const at = unturn(drawn, this.turns);
86
+ const finger = {
87
+ at,
88
+ box,
89
+ from: at,
90
+ furthest: 0,
91
+ id: this.freeId(mirror),
92
+ landed: performance.now(),
93
+ mirror,
94
+ startX: event.clientX,
95
+ startY: event.clientY,
96
+ };
97
+ this.fingers.set(event.pointerId, finger);
98
+ this.crowd = Math.max(this.crowd, this.touches);
99
+ this.toggleState('touching', true);
100
+ this.sendTouch('down', finger);
101
+ this.holdMarks();
102
+ };
103
+ this.handlePointerMove = (event) => {
104
+ const finger = this.fingers.get(event.pointerId);
105
+ const drawn = finger ? this.fractionAt(event, true, finger.box) : undefined;
106
+ if (!(finger && drawn)) {
107
+ return;
108
+ }
109
+ finger.at = unturn(drawn, this.turns);
110
+ finger.furthest = Math.max(finger.furthest, Math.hypot(event.clientX - finger.startX, event.clientY - finger.startY));
111
+ this.sendTouch('move', finger);
112
+ this.holdMarks();
113
+ };
114
+ this.handlePointerUp = (event) => {
115
+ const finger = this.fingers.get(event.pointerId);
116
+ if (!finger) {
117
+ return;
118
+ }
119
+ const canceled = event.type === 'pointercancel';
120
+ const drawn = canceled ? undefined : this.fractionAt(event, true, finger.box);
121
+ if (drawn) {
122
+ finger.at = unturn(drawn, this.turns);
123
+ }
124
+ this.fingers.delete(event.pointerId);
125
+ this.sendTouch(canceled ? 'cancel' : 'up', finger);
126
+ if (this.fingers.size === 0) {
127
+ const alone = this.crowd === 1 && !canceled;
128
+ this.crowd = 0;
129
+ this.toggleState('touching', false);
130
+ if (alone) {
131
+ this.sendGesture(finger);
132
+ }
133
+ }
134
+ this.holdMarks();
135
+ };
136
+ this.handleWheel = (event) => {
137
+ if (this.readonly || ((event.ctrlKey || event.metaKey) && !this.forwardShortcuts)) {
138
+ return;
139
+ }
140
+ const drawn = this.fractionAt(event, false);
141
+ const box = this.screen?.getBoundingClientRect();
142
+ if (!(drawn && box)) {
143
+ return;
144
+ }
145
+ event.preventDefault();
146
+ const across = event.deltaMode === 1 ? LINE : event.deltaMode === 2 ? box.width : 1;
147
+ const down = event.deltaMode === 1 ? LINE : event.deltaMode === 2 ? box.height : 1;
148
+ const turns = this.turns;
149
+ const at = unturn(drawn, turns);
150
+ const by = unturnDistance({ u: (event.deltaX * across) / box.width, v: (event.deltaY * down) / box.height }, turns);
151
+ const { height, width } = this.sizeFor();
152
+ this.dispatchEvent(new HelixDeviceScrollEvent({
153
+ deltaX: by.u * width,
154
+ deltaY: by.v * height,
155
+ height,
156
+ width,
157
+ x: at.u * width,
158
+ y: at.v * height,
159
+ }));
160
+ };
161
+ this.handleKeydown = (event) => {
162
+ if (this.readonly || event.isComposing || leaves(event) || pastes(event)) {
163
+ return;
164
+ }
165
+ const shortcut = (event.ctrlKey || event.metaKey || event.altKey) && !event.getModifierState?.('AltGraph');
166
+ if (shortcut && !this.forwardShortcuts) {
167
+ return;
168
+ }
169
+ this.held.set(event.code || event.key, event);
170
+ this.sendKey(event, 'down');
171
+ };
172
+ this.handleKeyup = (event) => {
173
+ const id = event.code || event.key;
174
+ if (!this.held.has(id)) {
175
+ return;
176
+ }
177
+ this.held.delete(id);
178
+ this.sendKey(event, 'up');
179
+ };
180
+ this.handleBlur = () => {
181
+ for (const [id, pressed] of this.held) {
182
+ this.held.delete(id);
183
+ this.dispatchEvent(new HelixDeviceKeyEvent({
184
+ altKey: false,
185
+ code: pressed.code,
186
+ ctrlKey: false,
187
+ key: pressed.key,
188
+ metaKey: false,
189
+ phase: 'up',
190
+ repeat: false,
191
+ shiftKey: false,
192
+ }));
193
+ }
194
+ };
195
+ this.handlePaste = (event) => {
196
+ const screen = this.screen;
197
+ const text = event.clipboardData?.getData('text/plain') ?? '';
198
+ if (this.readonly ||
199
+ text === '' ||
200
+ screen === null ||
201
+ this.renderRoot.activeElement !== screen) {
202
+ return;
203
+ }
204
+ event.preventDefault();
205
+ this.dispatchEvent(new HelixDeviceTextEvent({ text }));
206
+ };
207
+ this.handleHoldStart = () => {
208
+ this.heldSince = performance.now();
209
+ };
210
+ }
211
+ static { this.words = 'helix-device-screen'; }
212
+ static { this.markRules = [SPOKEN_RULE, TOUCHES_RULE]; }
213
+ get controlsRaised() {
214
+ return false;
215
+ }
216
+ get column() {
217
+ return this.controlsPlacement === 'start' || this.controlsPlacement === 'end';
218
+ }
219
+ get turns() {
220
+ return quarterTurns(this.rotation);
221
+ }
222
+ get size() {
223
+ const given = (value) => value !== undefined && Number.isFinite(value) && value > 0;
224
+ const { intrinsic, screenHeight: height, screenWidth: width } = this;
225
+ if (given(width) && given(height)) {
226
+ return { height, width };
227
+ }
228
+ if (!intrinsic) {
229
+ return undefined;
230
+ }
231
+ if (given(width)) {
232
+ return { height: (width * intrinsic.height) / intrinsic.width, width };
233
+ }
234
+ if (given(height)) {
235
+ return { height, width: (height * intrinsic.width) / intrinsic.height };
236
+ }
237
+ return intrinsic;
238
+ }
239
+ get stream() {
240
+ return this.knownChildren.find((child) => !child.hasAttribute('slot'));
241
+ }
242
+ get screen() {
243
+ return this.renderRoot?.querySelector?.('[part="screen"]') ?? null;
244
+ }
245
+ draws(name) {
246
+ return (this.controls ?? RESTING_CONTROLS).split(/\s+/).includes(name);
247
+ }
248
+ sizeOf(element) {
249
+ const read = element?.localName === 'video'
250
+ ? {
251
+ height: element.videoHeight,
252
+ width: element.videoWidth,
253
+ }
254
+ : element?.localName === 'img'
255
+ ? {
256
+ height: element.naturalHeight,
257
+ width: element.naturalWidth,
258
+ }
259
+ : element?.localName === 'canvas'
260
+ ? {
261
+ height: element.height,
262
+ width: element.width,
263
+ }
264
+ : undefined;
265
+ return read && read.width > 0 && read.height > 0 ? read : undefined;
266
+ }
267
+ measure() {
268
+ const next = this.sizeOf(this.watched);
269
+ if (next?.width !== this.intrinsic?.width || next?.height !== this.intrinsic?.height) {
270
+ this.intrinsic = next;
271
+ }
272
+ }
273
+ unwatch() {
274
+ const element = this.watched;
275
+ element?.removeEventListener('loadedmetadata', this.handleMeasure);
276
+ element?.removeEventListener('resize', this.handleMeasure);
277
+ element?.removeEventListener('load', this.handleMeasure);
278
+ this.resized?.disconnect();
279
+ this.resized = undefined;
280
+ this.watched = undefined;
281
+ }
282
+ watch() {
283
+ const element = this.stream;
284
+ if (element === this.watched) {
285
+ return;
286
+ }
287
+ this.unwatch();
288
+ this.watched = element;
289
+ element?.addEventListener('loadedmetadata', this.handleMeasure);
290
+ element?.addEventListener('resize', this.handleMeasure);
291
+ element?.addEventListener('load', this.handleMeasure);
292
+ if (element && element.localName === 'canvas') {
293
+ this.resized = new MutationObserver(this.handleMeasure);
294
+ this.resized.observe(element, { attributeFilter: ['height', 'width'], attributes: true });
295
+ }
296
+ }
297
+ fractionAt(event, clamp, box = this.screen?.getBoundingClientRect()) {
298
+ if (!box || box.width === 0 || box.height === 0) {
299
+ return undefined;
300
+ }
301
+ const u = (event.clientX - box.left) / box.width;
302
+ const v = (event.clientY - box.top) / box.height;
303
+ if (clamp) {
304
+ return { u: Math.min(1, Math.max(0, u)), v: Math.min(1, Math.max(0, v)) };
305
+ }
306
+ if (u < 0 || u > 1 || v < 0 || v > 1) {
307
+ return undefined;
308
+ }
309
+ if (this.shape === 'circle' && (u - 0.5) ** 2 + (v - 0.5) ** 2 > 0.25) {
310
+ return undefined;
311
+ }
312
+ return { u, v };
313
+ }
314
+ sizeFor() {
315
+ const known = this.size;
316
+ if (known) {
317
+ return known;
318
+ }
319
+ const box = this.screen?.getBoundingClientRect();
320
+ const sideways = this.turns % 2 === 1;
321
+ const width = (sideways ? box?.height : box?.width) ?? 0;
322
+ const height = (sideways ? box?.width : box?.height) ?? 0;
323
+ const { screenHeight, screenWidth } = this;
324
+ if (screenWidth !== undefined && screenWidth > 0 && width > 0) {
325
+ return { height: (screenWidth * height) / width, width: screenWidth };
326
+ }
327
+ if (screenHeight !== undefined && screenHeight > 0 && height > 0) {
328
+ return { height: screenHeight, width: (screenHeight * width) / height };
329
+ }
330
+ return { height, width };
331
+ }
332
+ sendTouch(phase, finger) {
333
+ const { height, width } = this.sizeFor();
334
+ const send = (id, at) => {
335
+ this.dispatchEvent(new HelixDeviceTouchEvent({ height, id, phase, width, x: at.u * width, y: at.v * height }));
336
+ };
337
+ send(finger.id, finger.at);
338
+ if (finger.mirror) {
339
+ send(finger.id + 1, { u: 1 - finger.at.u, v: 1 - finger.at.v });
340
+ }
341
+ }
342
+ freeId(mirror) {
343
+ const taken = new Set();
344
+ for (const finger of this.fingers.values()) {
345
+ taken.add(finger.id);
346
+ if (finger.mirror) {
347
+ taken.add(finger.id + 1);
348
+ }
349
+ }
350
+ let id = 0;
351
+ while (taken.has(id) || (mirror && taken.has(id + 1))) {
352
+ id += 1;
353
+ }
354
+ return id;
355
+ }
356
+ get touches() {
357
+ let count = 0;
358
+ for (const finger of this.fingers.values()) {
359
+ count += finger.mirror ? 2 : 1;
360
+ }
361
+ return count;
362
+ }
363
+ holdMarks() {
364
+ if (!this.showTouches) {
365
+ if (this.marks.some((mark) => mark.held)) {
366
+ this.marks = this.marks.filter((mark) => !mark.held);
367
+ }
368
+ return;
369
+ }
370
+ const shown = this.marks.filter((mark) => !mark.held);
371
+ const held = [];
372
+ for (const [pointer, finger] of this.fingers) {
373
+ held.push({ at: turn(finger.at, this.turns), held: true, key: -1 - pointer * 2 });
374
+ if (finger.mirror) {
375
+ const opposite = { u: 1 - finger.at.u, v: 1 - finger.at.v };
376
+ held.push({ at: turn(opposite, this.turns), held: true, key: -2 - pointer * 2 });
377
+ }
378
+ }
379
+ this.marks = [...shown, ...held];
380
+ }
381
+ sendGesture(finger) {
382
+ const { height, width } = this.sizeFor();
383
+ const duration = Math.max(0, Math.round(performance.now() - finger.landed));
384
+ if (gestureOf(finger.furthest) === 'tap') {
385
+ this.dispatchEvent(new HelixDeviceTapEvent({
386
+ duration,
387
+ height,
388
+ width,
389
+ x: finger.from.u * width,
390
+ y: finger.from.v * height,
391
+ }));
392
+ return;
393
+ }
394
+ this.dispatchEvent(new HelixDeviceSwipeEvent({
395
+ duration,
396
+ fromX: finger.from.u * width,
397
+ fromY: finger.from.v * height,
398
+ height,
399
+ toX: finger.at.u * width,
400
+ toY: finger.at.v * height,
401
+ width,
402
+ }));
403
+ }
404
+ sendKey(event, phase) {
405
+ event.preventDefault();
406
+ this.dispatchEvent(new HelixDeviceKeyEvent({
407
+ altKey: event.altKey,
408
+ code: event.code,
409
+ ctrlKey: event.ctrlKey,
410
+ key: event.key,
411
+ metaKey: event.metaKey,
412
+ phase,
413
+ repeat: phase === 'down' && event.repeat,
414
+ shiftKey: event.shiftKey,
415
+ }));
416
+ }
417
+ pressed(button, event) {
418
+ const since = this.heldSince;
419
+ this.heldSince = 0;
420
+ const duration = event.detail === 0 || since === 0 ? 0 : Math.round(performance.now() - since);
421
+ this.press(button, duration);
422
+ }
423
+ press(button, duration = 0) {
424
+ this.dispatchEvent(new HelixDeviceButtonEvent({ button, duration: Math.max(0, duration) }));
425
+ }
426
+ screenshot() {
427
+ this.dispatchEvent(new HelixDeviceScreenshotEvent());
428
+ }
429
+ async capture(type = 'image/png', quality) {
430
+ const source = this.stream;
431
+ if (!(source instanceof HTMLVideoElement ||
432
+ source instanceof HTMLImageElement ||
433
+ source instanceof HTMLCanvasElement)) {
434
+ throw new Error(`<${this.localName}> has no stream to capture. Put a <video>, an <img>, or a <canvas> inside it.`);
435
+ }
436
+ const size = this.sizeOf(source);
437
+ if (!size) {
438
+ throw new Error(`<${this.localName}> has no frame to capture yet.`);
439
+ }
440
+ const turns = this.turns;
441
+ const canvas = document.createElement('canvas');
442
+ canvas.width = turns % 2 === 1 ? size.height : size.width;
443
+ canvas.height = turns % 2 === 1 ? size.width : size.height;
444
+ const context = canvas.getContext('2d');
445
+ if (!context) {
446
+ throw new Error(`<${this.localName}> could not draw its frame: the browser gave no 2D context.`);
447
+ }
448
+ context.translate(canvas.width / 2, canvas.height / 2);
449
+ context.rotate((turns * Math.PI) / 2);
450
+ context.drawImage(source, -size.width / 2, -size.height / 2, size.width, size.height);
451
+ return await new Promise((resolve, reject) => {
452
+ canvas.toBlob((blob) => {
453
+ if (blob) {
454
+ resolve(blob);
455
+ }
456
+ else {
457
+ reject(new Error(`<${this.localName}> could not write its frame as ${type}.`));
458
+ }
459
+ }, type, quality);
460
+ });
461
+ }
462
+ showTap(x, y) {
463
+ const at = this.drawnAt(x, y);
464
+ if (at) {
465
+ this.addMark({ at, held: false, key: this.nextMark++ });
466
+ }
467
+ }
468
+ showSwipe(fromX, fromY, toX, toY, duration = 300) {
469
+ const at = this.drawnAt(fromX, fromY);
470
+ const to = this.drawnAt(toX, toY);
471
+ if (at && to) {
472
+ this.addMark({ at, duration: Math.max(0, duration), held: false, key: this.nextMark++, to });
473
+ }
474
+ }
475
+ drawnAt(x, y) {
476
+ const { height, width } = this.sizeFor();
477
+ if (!(width > 0 && height > 0 && Number.isFinite(x) && Number.isFinite(y))) {
478
+ return undefined;
479
+ }
480
+ return turn({ u: x / width, v: y / height }, this.turns);
481
+ }
482
+ addMark(mark) {
483
+ this.marks = [...this.marks, mark];
484
+ const timer = setTimeout(() => {
485
+ this.timers.delete(timer);
486
+ this.marks = this.marks.filter((one) => one.key !== mark.key);
487
+ }, (mark.duration ?? 0) + MARK_SHOWS);
488
+ this.timers.add(timer);
489
+ }
490
+ connectedCallback() {
491
+ super.connectedCallback();
492
+ this.ownerDocument.addEventListener('paste', this.handlePaste, true);
493
+ if (this.hasUpdated) {
494
+ this.requestUpdate();
495
+ }
496
+ }
497
+ disconnectedCallback() {
498
+ super.disconnectedCallback();
499
+ this.ownerDocument.removeEventListener('paste', this.handlePaste, true);
500
+ this.unwatch();
501
+ for (const timer of this.timers) {
502
+ clearTimeout(timer);
503
+ }
504
+ this.timers.clear();
505
+ for (const finger of this.fingers.values()) {
506
+ this.sendTouch('cancel', finger);
507
+ }
508
+ this.fingers.clear();
509
+ this.crowd = 0;
510
+ this.toggleState('touching', false);
511
+ this.handleBlur();
512
+ this.marks = [];
513
+ }
514
+ firstUpdated(changed) {
515
+ super.firstUpdated(changed);
516
+ this.finishHydration();
517
+ auditName(this, () => null, 'device-screen', () => this.label !== '');
518
+ }
519
+ willUpdate(changed) {
520
+ super.willUpdate(changed);
521
+ hostAria(this.internals, { label: this.label });
522
+ this.watch();
523
+ this.measure();
524
+ this.toggleState('empty', this.childrenKnown && this.stream === undefined);
525
+ this.toggleState('fullscreen', this.filled);
526
+ this.toggleState('loading', this.loading);
527
+ this.toggleState('readonly', this.readonly);
528
+ this.toggleState('sideways', this.turns % 2 === 1);
529
+ if (changed.has('showTouches')) {
530
+ this.holdMarks();
531
+ }
532
+ }
533
+ renderControls() {
534
+ const names = [
535
+ 'back',
536
+ 'home',
537
+ 'recents',
538
+ 'power',
539
+ 'volume',
540
+ 'rotate',
541
+ 'screenshot',
542
+ 'fullscreen',
543
+ ];
544
+ const own = this.knownChildren.some((child) => child.getAttribute('slot') === 'controls');
545
+ if (!(own || names.some((name) => this.draws(name)))) {
546
+ return nothing;
547
+ }
548
+ const button = this.tag('icon-button');
549
+ const off = this.readonly;
550
+ const raised = this.controlsRaised;
551
+ return html `<div
552
+ part="controls"
553
+ role="toolbar"
554
+ aria-label=${this.controlsLabel}
555
+ aria-orientation=${this.column ? 'vertical' : nothing}
556
+ >
557
+ ${this.draws('back')
558
+ ? html `<${button}
559
+ part="back"
560
+ exportparts="button: back-button, live: back-live, spinner: back-spinner, text: back-text"
561
+ ?disabled=${off}
562
+ label=${this.backLabel}
563
+ ?raised=${raised}
564
+ @click=${(event) => this.pressed('back', event)}
565
+ @helix-press=${stopPress}
566
+ @pointerdown=${this.handleHoldStart}
567
+ >
568
+ <slot name="back-icon">${drawIcon(caretLeft)}</slot>
569
+ </${button}>`
570
+ : nothing}
571
+ ${this.draws('home')
572
+ ? html `<${button}
573
+ part="home"
574
+ exportparts="button: home-button, live: home-live, spinner: home-spinner, text: home-text"
575
+ ?disabled=${off}
576
+ label=${this.homeLabel}
577
+ ?raised=${raised}
578
+ @click=${(event) => this.pressed('home', event)}
579
+ @helix-press=${stopPress}
580
+ @pointerdown=${this.handleHoldStart}
581
+ >
582
+ <slot name="home-icon">${drawIcon(house)}</slot>
583
+ </${button}>`
584
+ : nothing}
585
+ ${this.draws('recents')
586
+ ? html `<${button}
587
+ part="recents"
588
+ exportparts="button: recents-button, live: recents-live, spinner: recents-spinner, text: recents-text"
589
+ ?disabled=${off}
590
+ label=${this.recentsLabel}
591
+ ?raised=${raised}
592
+ @click=${(event) => this.pressed('recents', event)}
593
+ @helix-press=${stopPress}
594
+ @pointerdown=${this.handleHoldStart}
595
+ >
596
+ <slot name="recents-icon">${drawIcon(square)}</slot>
597
+ </${button}>`
598
+ : nothing}
599
+ ${this.draws('power')
600
+ ? html `<${button}
601
+ part="power"
602
+ exportparts="button: power-button, live: power-live, spinner: power-spinner, text: power-text"
603
+ ?disabled=${off}
604
+ label=${this.powerLabel}
605
+ ?raised=${raised}
606
+ @click=${(event) => this.pressed('power', event)}
607
+ @helix-press=${stopPress}
608
+ @pointerdown=${this.handleHoldStart}
609
+ >
610
+ <slot name="power-icon">${drawIcon(power)}</slot>
611
+ </${button}>`
612
+ : nothing}
613
+ ${this.draws('volume')
614
+ ? html `<${button}
615
+ part="volume-up"
616
+ exportparts="button: volume-up-button, live: volume-up-live, spinner: volume-up-spinner, text: volume-up-text"
617
+ ?disabled=${off}
618
+ label=${this.volumeUpLabel}
619
+ ?raised=${raised}
620
+ @click=${(event) => this.pressed('volume-up', event)}
621
+ @helix-press=${stopPress}
622
+ @pointerdown=${this.handleHoldStart}
623
+ >
624
+ <slot name="volume-up-icon">${drawIcon(speakerHigh)}</slot>
625
+ </${button}>
626
+ <${button}
627
+ part="volume-down"
628
+ exportparts="button: volume-down-button, live: volume-down-live, spinner: volume-down-spinner, text: volume-down-text"
629
+ ?disabled=${off}
630
+ label=${this.volumeDownLabel}
631
+ ?raised=${raised}
632
+ @click=${(event) => this.pressed('volume-down', event)}
633
+ @helix-press=${stopPress}
634
+ @pointerdown=${this.handleHoldStart}
635
+ >
636
+ <slot name="volume-down-icon">${drawIcon(speakerLow)}</slot>
637
+ </${button}>`
638
+ : nothing}
639
+ ${this.draws('rotate')
640
+ ? html `<${button}
641
+ part="rotate-left"
642
+ exportparts="button: rotate-left-button, live: rotate-left-live, spinner: rotate-left-spinner, text: rotate-left-text"
643
+ ?disabled=${off}
644
+ label=${this.rotateLeftLabel}
645
+ ?raised=${raised}
646
+ @click=${(event) => this.pressed('rotate-left', event)}
647
+ @helix-press=${stopPress}
648
+ @pointerdown=${this.handleHoldStart}
649
+ >
650
+ <slot name="rotate-left-icon">${drawIcon(arrowCounterClockwise)}</slot>
651
+ </${button}>
652
+ <${button}
653
+ part="rotate-right"
654
+ exportparts="button: rotate-right-button, live: rotate-right-live, spinner: rotate-right-spinner, text: rotate-right-text"
655
+ ?disabled=${off}
656
+ label=${this.rotateRightLabel}
657
+ ?raised=${raised}
658
+ @click=${(event) => this.pressed('rotate-right', event)}
659
+ @helix-press=${stopPress}
660
+ @pointerdown=${this.handleHoldStart}
661
+ >
662
+ <slot name="rotate-right-icon">${drawIcon(arrowClockwise)}</slot>
663
+ </${button}>`
664
+ : nothing}
665
+ ${this.draws('screenshot')
666
+ ? html `<${button}
667
+ part="screenshot"
668
+ exportparts="button: screenshot-button, live: screenshot-live, spinner: screenshot-spinner, text: screenshot-text"
669
+ label=${this.screenshotLabel}
670
+ ?raised=${raised}
671
+ @click=${() => this.screenshot()}
672
+ @helix-press=${stopPress}
673
+ >
674
+ <slot name="screenshot-icon">${drawIcon(camera)}</slot>
675
+ </${button}>`
676
+ : nothing}
677
+ <slot name="controls" @slotchange=${this.handleSlotChange}></slot>
678
+ ${this.draws('fullscreen')
679
+ ? html `<${button}
680
+ part="fullscreen"
681
+ exportparts="button: fullscreen-button, live: fullscreen-live, spinner: fullscreen-spinner, text: fullscreen-text"
682
+ aria-pressed=${this.filled ? 'true' : 'false'}
683
+ label=${this.fullscreenLabel}
684
+ ?raised=${raised}
685
+ @click=${() => void this.fullscreen.toggle()}
686
+ @helix-press=${stopPress}
687
+ >
688
+ <slot name="fullscreen-icon">${drawIcon(cornersOut)}</slot>
689
+ </${button}>`
690
+ : nothing}
691
+ </div>`;
692
+ }
693
+ renderMarks() {
694
+ return html `${repeat(this.marks, (mark) => mark.key, (mark) => {
695
+ const place = `--device-screen-touch-x: ${(mark.at.u * 100).toFixed(2)}%; --device-screen-touch-y: ${(mark.at.v * 100).toFixed(2)}%`;
696
+ const travel = mark.to
697
+ ? `; --device-screen-touch-to-x: ${(mark.to.u * 100).toFixed(2)}%; --device-screen-touch-to-y: ${(mark.to.v * 100).toFixed(2)}%; --device-screen-touch-duration: ${mark.duration ?? 0}ms`
698
+ : '';
699
+ return html `<span part="touch${mark.held ? ' touch-held' : ''}" aria-hidden="true" style=${place + travel}></span>`;
700
+ })}`;
701
+ }
702
+ render() {
703
+ const size = this.size;
704
+ const turns = this.turns;
705
+ const ratio = size
706
+ ? turns % 2 === 1
707
+ ? size.height / size.width
708
+ : size.width / size.height
709
+ : undefined;
710
+ const facts = `${ratio === undefined ? '' : `--device-screen-ratio: ${Number(ratio.toFixed(5))}; `}--device-screen-rotation: ${turns * 90}deg`;
711
+ const name = this.label || this.getAttribute('aria-label') || this.fallbackLabel;
712
+ const interactive = !this.readonly;
713
+ const placement = this.controlsPlacement;
714
+ const controls = this.renderControls();
715
+ return html `<div part="region" style=${facts}>
716
+ <div part="header">
717
+ <slot name="header"></slot>
718
+ </div>
719
+ ${placement === undefined || placement === 'top' ? controls : nothing}
720
+ <div part="device">
721
+ ${placement === 'start' ? controls : nothing}
722
+ <div part="bezel">
723
+ <div
724
+ part="screen"
725
+ role=${interactive ? 'application' : 'img'}
726
+ aria-label=${name}
727
+ aria-roledescription=${this.roleLabel || nothing}
728
+ aria-describedby=${interactive ? 'keys-hint' : nothing}
729
+ tabindex=${interactive ? '0' : nothing}
730
+ ?data-touches=${interactive}
731
+ @blur=${this.handleBlur}
732
+ @keydown=${this.handleKeydown}
733
+ @keyup=${this.handleKeyup}
734
+ @pointercancel=${this.handlePointerUp}
735
+ @pointerdown=${this.handlePointerDown}
736
+ @pointermove=${this.handlePointerMove}
737
+ @pointerup=${this.handlePointerUp}
738
+ @wheel=${this.handleWheel}
739
+ >
740
+ <div part="picture">
741
+ <slot @slotchange=${this.handleSlotChange}></slot>
742
+ </div>
743
+ ${this.renderMarks()}
744
+ <div part="loading" aria-hidden="true">
745
+ ${this.loading ? html `<slot name="loading">${this.loadingLabel}</slot>` : nothing}
746
+ </div>
747
+ </div>
748
+ ${this.childrenKnown && this.stream === undefined && !this.loading
749
+ ? html `<div part="empty"><slot name="empty"></slot></div>`
750
+ : nothing}
751
+ <slot name="bezel"></slot>
752
+ </div>
753
+ ${placement === 'end' ? controls : nothing}
754
+ </div>
755
+ ${placement === 'bottom' ? controls : nothing}
756
+ <div part="footer">
757
+ <slot name="footer"></slot>
758
+ </div>
759
+ <span part="keys-hint" id="keys-hint" aria-hidden="true" data-spoken>${interactive ? this.keysHintLabel : ''}</span>
760
+ <span part="live" role="status" data-spoken>${this.loading ? this.loadingLabel : ''}</span>
761
+ </div>`;
762
+ }
763
+ };
764
+ __decorate([
765
+ words,
766
+ property({ attribute: 'back-label' })
767
+ ], HelixHeadlessDeviceScreen.prototype, "backLabel", void 0);
768
+ __decorate([
769
+ property()
770
+ ], HelixHeadlessDeviceScreen.prototype, "controls", void 0);
771
+ __decorate([
772
+ words,
773
+ property({ attribute: 'controls-label' })
774
+ ], HelixHeadlessDeviceScreen.prototype, "controlsLabel", void 0);
775
+ __decorate([
776
+ property({ attribute: 'controls-placement', reflect: true })
777
+ ], HelixHeadlessDeviceScreen.prototype, "controlsPlacement", void 0);
778
+ __decorate([
779
+ words,
780
+ property({ attribute: 'fallback-label' })
781
+ ], HelixHeadlessDeviceScreen.prototype, "fallbackLabel", void 0);
782
+ __decorate([
783
+ property({ attribute: 'forward-shortcuts', type: Boolean })
784
+ ], HelixHeadlessDeviceScreen.prototype, "forwardShortcuts", void 0);
785
+ __decorate([
786
+ words,
787
+ property({ attribute: 'fullscreen-label' })
788
+ ], HelixHeadlessDeviceScreen.prototype, "fullscreenLabel", void 0);
789
+ __decorate([
790
+ words,
791
+ property({ attribute: 'home-label' })
792
+ ], HelixHeadlessDeviceScreen.prototype, "homeLabel", void 0);
793
+ __decorate([
794
+ words,
795
+ property({ attribute: 'keys-hint-label' })
796
+ ], HelixHeadlessDeviceScreen.prototype, "keysHintLabel", void 0);
797
+ __decorate([
798
+ property()
799
+ ], HelixHeadlessDeviceScreen.prototype, "label", void 0);
800
+ __decorate([
801
+ property({ type: Boolean })
802
+ ], HelixHeadlessDeviceScreen.prototype, "loading", void 0);
803
+ __decorate([
804
+ words,
805
+ property({ attribute: 'loading-label' })
806
+ ], HelixHeadlessDeviceScreen.prototype, "loadingLabel", void 0);
807
+ __decorate([
808
+ words,
809
+ property({ attribute: 'power-label' })
810
+ ], HelixHeadlessDeviceScreen.prototype, "powerLabel", void 0);
811
+ __decorate([
812
+ property({ type: Boolean })
813
+ ], HelixHeadlessDeviceScreen.prototype, "readonly", void 0);
814
+ __decorate([
815
+ words,
816
+ property({ attribute: 'recents-label' })
817
+ ], HelixHeadlessDeviceScreen.prototype, "recentsLabel", void 0);
818
+ __decorate([
819
+ words,
820
+ property({ attribute: 'role-label' })
821
+ ], HelixHeadlessDeviceScreen.prototype, "roleLabel", void 0);
822
+ __decorate([
823
+ words,
824
+ property({ attribute: 'rotate-left-label' })
825
+ ], HelixHeadlessDeviceScreen.prototype, "rotateLeftLabel", void 0);
826
+ __decorate([
827
+ words,
828
+ property({ attribute: 'rotate-right-label' })
829
+ ], HelixHeadlessDeviceScreen.prototype, "rotateRightLabel", void 0);
830
+ __decorate([
831
+ property({ converter: numberConverter('rotation', 0, 'degrees') })
832
+ ], HelixHeadlessDeviceScreen.prototype, "rotation", void 0);
833
+ __decorate([
834
+ property({ attribute: 'screen-height', converter: numberConverter('screen-height', undefined) })
835
+ ], HelixHeadlessDeviceScreen.prototype, "screenHeight", void 0);
836
+ __decorate([
837
+ words,
838
+ property({ attribute: 'screenshot-label' })
839
+ ], HelixHeadlessDeviceScreen.prototype, "screenshotLabel", void 0);
840
+ __decorate([
841
+ property({ attribute: 'screen-width', converter: numberConverter('screen-width', undefined) })
842
+ ], HelixHeadlessDeviceScreen.prototype, "screenWidth", void 0);
843
+ __decorate([
844
+ property({ reflect: true })
845
+ ], HelixHeadlessDeviceScreen.prototype, "shape", void 0);
846
+ __decorate([
847
+ property({ attribute: 'show-touches', type: Boolean })
848
+ ], HelixHeadlessDeviceScreen.prototype, "showTouches", void 0);
849
+ __decorate([
850
+ words,
851
+ property({ attribute: 'volume-down-label' })
852
+ ], HelixHeadlessDeviceScreen.prototype, "volumeDownLabel", void 0);
853
+ __decorate([
854
+ words,
855
+ property({ attribute: 'volume-up-label' })
856
+ ], HelixHeadlessDeviceScreen.prototype, "volumeUpLabel", void 0);
857
+ __decorate([
858
+ state()
859
+ ], HelixHeadlessDeviceScreen.prototype, "filled", void 0);
860
+ __decorate([
861
+ state()
862
+ ], HelixHeadlessDeviceScreen.prototype, "intrinsic", void 0);
863
+ __decorate([
864
+ state()
865
+ ], HelixHeadlessDeviceScreen.prototype, "marks", void 0);
866
+ HelixHeadlessDeviceScreen = __decorate([
867
+ customElement('helix-headless-device-screen')
868
+ ], HelixHeadlessDeviceScreen);
869
+ export { HelixHeadlessDeviceScreen };