tosijs-ui 1.0.1 → 1.0.2

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.
Files changed (93) hide show
  1. package/README.md +4 -2
  2. package/dist/iife.js +70 -60
  3. package/dist/iife.js.map +42 -42
  4. package/dist/index.js +15 -37
  5. package/dist/index.js.map +39 -39
  6. package/dist/version.d.ts +1 -1
  7. package/package.json +2 -2
  8. package/dist/ab-test.js +0 -116
  9. package/dist/babylon-3d.js +0 -292
  10. package/dist/bodymovin-player.js +0 -172
  11. package/dist/bp-loader.js +0 -26
  12. package/dist/carousel.js +0 -308
  13. package/dist/code-editor.js +0 -102
  14. package/dist/color-input.js +0 -112
  15. package/dist/data-table.js +0 -774
  16. package/dist/drag-and-drop.js +0 -386
  17. package/dist/editable-rect.js +0 -450
  18. package/dist/filter-builder.js +0 -468
  19. package/dist/float.js +0 -170
  20. package/dist/form.js +0 -466
  21. package/dist/gamepad.js +0 -115
  22. package/dist/icon-data.js +0 -308
  23. package/dist/icon-types.js +0 -1
  24. package/dist/icons.js +0 -374
  25. package/dist/index-iife.js +0 -4
  26. package/dist/live-example.js +0 -611
  27. package/dist/localize.js +0 -381
  28. package/dist/make-sorter.js +0 -119
  29. package/dist/make-sorter.test.d.ts +0 -1
  30. package/dist/make-sorter.test.js +0 -48
  31. package/dist/mapbox.js +0 -161
  32. package/dist/markdown-viewer.js +0 -173
  33. package/dist/match-shortcut.js +0 -13
  34. package/dist/match-shortcut.test.d.ts +0 -1
  35. package/dist/match-shortcut.test.js +0 -194
  36. package/dist/menu.js +0 -614
  37. package/dist/notifications.js +0 -308
  38. package/dist/password-strength.js +0 -302
  39. package/dist/playwright.config.d.ts +0 -9
  40. package/dist/playwright.config.js +0 -73
  41. package/dist/pop-float.js +0 -231
  42. package/dist/rating.js +0 -192
  43. package/dist/rich-text.js +0 -296
  44. package/dist/segmented.js +0 -298
  45. package/dist/select.js +0 -427
  46. package/dist/side-nav.js +0 -106
  47. package/dist/size-break.js +0 -118
  48. package/dist/sizer.js +0 -92
  49. package/dist/src/ab-test.d.ts +0 -14
  50. package/dist/src/babylon-3d.d.ts +0 -53
  51. package/dist/src/bodymovin-player.d.ts +0 -32
  52. package/dist/src/bp-loader.d.ts +0 -0
  53. package/dist/src/carousel.d.ts +0 -113
  54. package/dist/src/code-editor.d.ts +0 -27
  55. package/dist/src/color-input.d.ts +0 -41
  56. package/dist/src/data-table.d.ts +0 -79
  57. package/dist/src/drag-and-drop.d.ts +0 -2
  58. package/dist/src/editable-rect.d.ts +0 -97
  59. package/dist/src/filter-builder.d.ts +0 -64
  60. package/dist/src/float.d.ts +0 -18
  61. package/dist/src/form.d.ts +0 -68
  62. package/dist/src/gamepad.d.ts +0 -34
  63. package/dist/src/icon-data.d.ts +0 -309
  64. package/dist/src/icon-types.d.ts +0 -7
  65. package/dist/src/icons.d.ts +0 -17
  66. package/dist/src/index.d.ts +0 -37
  67. package/dist/src/live-example.d.ts +0 -51
  68. package/dist/src/localize.d.ts +0 -30
  69. package/dist/src/make-sorter.d.ts +0 -3
  70. package/dist/src/mapbox.d.ts +0 -24
  71. package/dist/src/markdown-viewer.d.ts +0 -15
  72. package/dist/src/match-shortcut.d.ts +0 -9
  73. package/dist/src/menu.d.ts +0 -60
  74. package/dist/src/notifications.d.ts +0 -106
  75. package/dist/src/password-strength.d.ts +0 -35
  76. package/dist/src/pop-float.d.ts +0 -10
  77. package/dist/src/rating.d.ts +0 -62
  78. package/dist/src/rich-text.d.ts +0 -28
  79. package/dist/src/segmented.d.ts +0 -80
  80. package/dist/src/select.d.ts +0 -43
  81. package/dist/src/side-nav.d.ts +0 -36
  82. package/dist/src/size-break.d.ts +0 -18
  83. package/dist/src/sizer.d.ts +0 -34
  84. package/dist/src/tab-selector.d.ts +0 -91
  85. package/dist/src/tag-list.d.ts +0 -37
  86. package/dist/src/track-drag.d.ts +0 -5
  87. package/dist/src/version.d.ts +0 -1
  88. package/dist/src/via-tag.d.ts +0 -2
  89. package/dist/tab-selector.js +0 -326
  90. package/dist/tag-list.js +0 -375
  91. package/dist/track-drag.js +0 -143
  92. package/dist/version.js +0 -1
  93. package/dist/via-tag.js +0 -102
package/dist/form.js DELETED
@@ -1,466 +0,0 @@
1
- /*#
2
- # forms
3
-
4
- `<xin-form>` and `<xin-field>` can be used to quickly create forms complete with
5
- [client-side validation](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#built-in_form_validation_examples).
6
-
7
- ```js
8
- const form = preview.querySelector('xin-form')
9
- preview.querySelector('.submit').addEventListener('click', form.submit)
10
- ```
11
- ```html
12
- <xin-form value='{"formInitializer": "initial value from form"}'>
13
- <h3 slot="header">Example Form Header</h3>
14
- <xin-field caption="Required field" key="required"></xin-field>
15
- <xin-field optional key="optional"><i>Optional</i> Field</xin-field>
16
- <xin-field key="text" type="text" placeholder="type it in here">Tell us a long story</xin-field>
17
- <xin-field caption="Zip Code" placeholder="12345 or 12345-6789" key="zipcode" pattern="\d{5}(-\d{4})?"></xin-field>
18
- <xin-field caption="Date" key="date" type="date"></xin-field>
19
- <xin-field caption="Number" key="number" type="number"></xin-field>
20
- <xin-field caption="Range" key="range" type="range" min="0" max="10"></xin-field>
21
- <xin-field key="boolean" type="checkbox">😃 <b>Agreed?!</b></xin-field>
22
- <xin-field key="color" type="color" value="pink">
23
- favorite color
24
- </xin-field>
25
- <xin-field key="select">
26
- Custom Field
27
- <select slot="input">
28
- <option>This</option>
29
- <option>That</option>
30
- <option>The Other</option>
31
- </select>
32
- </xin-field>
33
- <xin-field key="tags">
34
- Tag List
35
- <xin-tag-list editable slot="input" available-tags="pick me,no pick me"></xin-tag-list>
36
- </xin-field>
37
- <xin-field key="rating">
38
- Rate this form!
39
- <xin-rating slot="input"></xin-rating>
40
- </xin-field>
41
- <xin-field key="like">
42
- Do you like it?
43
- <xin-segmented
44
- choices="yes=Yes:thumbsUp,no=No:thumbsDown"
45
- slot="input"
46
- ></xin-segmented>
47
- </xin-field>
48
- <xin-field key="relationship">
49
- Relationship Status
50
- <xin-segmented
51
- style="--segmented-direction: column; --segmented-align-items: stretch"
52
- choices="couple=In a relationship,single=Single"
53
- other="It's complicated…"
54
- slot="input"
55
- ></xin-segmented>
56
- </xin-field>
57
- <xin-field key="amount" fixed-precision="2" type="number" prefix="$" suffix="(USD)">
58
- What's it worth?
59
- </xin-field>
60
- <xin-field key="valueInitializer" value="initial value from field">
61
- Initialized by field
62
- </xin-field>
63
- <xin-field key="formInitializer">
64
- Initialized by form
65
- </xin-field>
66
- <button slot="footer" class="submit">Submit</button>
67
- </xin-form>
68
- ```
69
- ```css
70
- .preview xin-form {
71
- height: 100%;
72
- }
73
-
74
- .preview ::part(header), .preview ::part(footer) {
75
- background: var(--inset-bg);
76
- justify-content: center;
77
- padding: calc(var(--spacing) * 0.5) var(--spacing);
78
- }
79
-
80
- .preview h3, .preview h4 {
81
- margin: 0;
82
- padding: 0;
83
- }
84
-
85
- .preview ::part(content) {
86
- padding: var(--spacing);
87
- gap: var(--spacing);
88
- background: var(--background);
89
- }
90
-
91
- .preview label {
92
- display: grid;
93
- grid-template-columns: 180px auto 100px;
94
- gap: var(--spacing);
95
- }
96
-
97
- .preview label [part="caption"] {
98
- text-align: right;
99
- }
100
-
101
- .preview label:has(:invalid:required)::after {
102
- content: '* required'
103
- }
104
-
105
- @media (max-width: 500px) {
106
- .preview label [part="caption"] {
107
- text-align: center;
108
- }
109
-
110
- .preview label {
111
- display: flex;
112
- flex-direction: column;
113
- position: relative;
114
- align-items: stretch;
115
- gap: calc(var(--spacing) * 0.5);
116
- }
117
-
118
- .preview label:has(:invalid:required)::after {
119
- position: absolute;
120
- top: 0;
121
- right: 0;
122
- content: '*'
123
- }
124
-
125
- .preview xin-field [part="field"],
126
- .preview xin-field [part="input"] > * {
127
- display: flex;
128
- justify-content: center;
129
- }
130
- }
131
-
132
- .preview :invalid {
133
- box-shadow: inset 0 0 0 2px #F008;
134
- }
135
- ```
136
-
137
- ## `<xin-form>`
138
-
139
- `<xin-form>` prevents the default form behavior when a `submit` event is triggered and instead validates the
140
- form contents (generating feedback if desired) and calls its `submitCallback(value: {[key: string]: any}, isValid: boolean): void`
141
- method.
142
-
143
- `<xin-form>` offers a `fields` proxy that allows values stored in the form to be updated. Any changes will trigger a `change`
144
- event on the `<xin-form>` (in addition to any events fired by form fields).
145
-
146
- `<xin-form>` instances have `value` and `isValid` properties you can access any time. Note that `isValid` is computed
147
- and triggers form validation.
148
-
149
- `<xin-form>` has `header` and `footer` `<slot>`s in addition to default `<slot>`, which is tucked inside a `<form>` element.
150
-
151
- ## `<xin-field>`
152
-
153
- `<xin-field>` is a simple web-component with no shadowDOM that combines an `<input>` field wrapped with a `<label>`. Any
154
- content of the custom-element will become the `caption` or you can simply set the `caption` attribute.
155
-
156
- You can replace the default `<input>` field by adding an element to the slot `input` (it's a `xinSlot`) whereupon
157
- the `value` of that element will be used instead of the built-in `<input>`. (The `<input>` is retained and
158
- is used to drive form-validation.)
159
-
160
- `<xin-field>` supports the following attributes:
161
-
162
- - `caption` labels the field
163
- - `key` determines the form property the field will populate
164
- - `type` determines the data-type: '' | 'checkbox' | 'number' | 'range' | 'date' | 'text' | 'color'
165
- - `optional` turns off the `required` attribute (fields are required by default)
166
- - `pattern` is an (optional) regex pattern
167
- - `placeholder` is an (optional) placeholder
168
-
169
- The `text` type populates the `input` slot with a `<textarea>` element.
170
-
171
- The `color` type populates the `input` slot with a `<xin-color>` element (and thus supports colors with alpha values).
172
-
173
- <xin-css-var-editor element-selector="xin-field" target-selector=".preview"></xin-css-var-editor>
174
- */
175
- import { Component as XinComponent, elements, varDefault, } from 'xinjs';
176
- import { colorInput } from './color-input';
177
- const { form, slot, xinSlot, label, input, span } = elements;
178
- function attr(element, name, value) {
179
- if (value !== '' && value !== false) {
180
- element.setAttribute(name, value);
181
- }
182
- else {
183
- element.removeAttribute(name);
184
- }
185
- }
186
- function getInputValue(input) {
187
- switch (input.type) {
188
- case 'checkbox':
189
- return input.checked;
190
- case 'radio': {
191
- const picked = input.parentElement?.querySelector(`input[type="radio"][name="${input.name}"]:checked`);
192
- return picked ? picked.value : null;
193
- }
194
- case 'range':
195
- case 'number':
196
- return Number(input.value);
197
- default:
198
- return Array.isArray(input.value) && input.value.length === 0
199
- ? null
200
- : input.value;
201
- }
202
- }
203
- function setElementValue(input, value) {
204
- if (!(input instanceof HTMLElement)) {
205
- // do nothing
206
- }
207
- else if (input instanceof HTMLInputElement) {
208
- switch (input.type) {
209
- case 'checkbox':
210
- input.checked = value;
211
- break;
212
- case 'radio':
213
- input.checked = value === input.value;
214
- break;
215
- default:
216
- input.value = String(value || '');
217
- }
218
- }
219
- else {
220
- if (value != null || input.value != null) {
221
- ;
222
- input.value = String(value || '');
223
- }
224
- }
225
- }
226
- export class XinField extends XinComponent {
227
- caption = '';
228
- key = '';
229
- type = '';
230
- optional = false;
231
- pattern = '';
232
- placeholder = '';
233
- min = '';
234
- max = '';
235
- step = '';
236
- fixedPrecision = -1;
237
- value = null;
238
- content = label(xinSlot({ part: 'caption' }), span({ part: 'field' }, xinSlot({ part: 'input', name: 'input' }), input({ part: 'valueHolder' })));
239
- constructor() {
240
- super();
241
- this.initAttributes('caption', 'key', 'type', 'optional', 'pattern', 'placeholder', 'min', 'max', 'step', 'fixedPrecision', 'prefix', 'suffix');
242
- }
243
- valueChanged = false;
244
- handleChange = () => {
245
- const { input, valueHolder } = this.parts;
246
- const inputElement = (input.children[0] || valueHolder);
247
- if (inputElement !== valueHolder) {
248
- valueHolder.value = inputElement.value;
249
- }
250
- this.value = getInputValue(inputElement);
251
- this.valueChanged = true;
252
- const form = this.closest('xin-form');
253
- if (form && this.key !== '') {
254
- switch (this.type) {
255
- case 'checkbox':
256
- form.fields[this.key] = inputElement.checked;
257
- break;
258
- case 'number':
259
- case 'range':
260
- if (this.fixedPrecision > -1) {
261
- inputElement.value = Number(inputElement.value).toFixed(this.fixedPrecision);
262
- form.fields[this.key] = Number(inputElement.value);
263
- }
264
- else {
265
- form.fields[this.key] = Number(inputElement.value);
266
- }
267
- break;
268
- default:
269
- form.fields[this.key] = inputElement.value;
270
- }
271
- }
272
- };
273
- initialize(form) {
274
- const initialValue = form.fields[this.key] !== undefined ? form.fields[this.key] : this.value;
275
- if (initialValue != null && initialValue !== '') {
276
- if (form.fields[this.key] == null)
277
- form.fields[this.key] = initialValue;
278
- this.value = initialValue;
279
- }
280
- }
281
- connectedCallback() {
282
- super.connectedCallback();
283
- const { input, valueHolder } = this.parts;
284
- const form = this.closest(XinForm.tagName);
285
- if (form instanceof XinForm) {
286
- this.initialize(form);
287
- }
288
- valueHolder.addEventListener('change', this.handleChange);
289
- input.addEventListener('change', this.handleChange, true);
290
- }
291
- render() {
292
- if (this.valueChanged) {
293
- this.valueChanged = false;
294
- return;
295
- }
296
- const { input, caption, valueHolder, field } = this.parts;
297
- if (caption.textContent?.trim() === '') {
298
- caption.append(this.caption !== '' ? this.caption : this.key);
299
- }
300
- if (this.type === 'text') {
301
- input.textContent = '';
302
- const textarea = elements.textarea({ value: this.value });
303
- if (this.placeholder) {
304
- textarea.setAttribute('placeholder', this.placeholder);
305
- }
306
- input.append(textarea);
307
- }
308
- else if (this.type === 'color') {
309
- input.textContent = '';
310
- input.append(colorInput({ value: this.value }));
311
- }
312
- else if (input.children.length === 0) {
313
- attr(valueHolder, 'placeholder', this.placeholder);
314
- attr(valueHolder, 'type', this.type);
315
- attr(valueHolder, 'pattern', this.pattern);
316
- attr(valueHolder, 'min', this.min);
317
- attr(valueHolder, 'max', this.max);
318
- attr(valueHolder, 'step', this.step);
319
- }
320
- setElementValue(valueHolder, this.value);
321
- setElementValue(input.children[0], this.value);
322
- this.prefix
323
- ? field.setAttribute('prefix', this.prefix)
324
- : field.removeAttribute('prefix');
325
- this.suffix
326
- ? field.setAttribute('suffix', this.suffix)
327
- : field.removeAttribute('suffix');
328
- valueHolder.classList.toggle('hidden', input.children.length > 0);
329
- if (input.children.length > 0) {
330
- valueHolder.setAttribute('tabindex', '-1');
331
- }
332
- else {
333
- valueHolder.removeAttribute('tabindex');
334
- }
335
- input.style.display = input.children.length === 0 ? 'none' : '';
336
- attr(valueHolder, 'required', !this.optional);
337
- }
338
- }
339
- export class XinForm extends XinComponent {
340
- context = {};
341
- value = {};
342
- get isValid() {
343
- const widgets = [...this.querySelectorAll('*')].filter((widget) => widget.required !== undefined);
344
- return widgets.find((widget) => !widget.reportValidity()) === undefined;
345
- }
346
- static styleSpec = {
347
- ':host': {
348
- display: 'flex',
349
- flexDirection: 'column',
350
- },
351
- ':host::part(header), :host::part(footer)': {
352
- display: 'flex',
353
- },
354
- ':host::part(content)': {
355
- display: 'flex',
356
- flexDirection: 'column',
357
- overflow: 'hidden auto',
358
- height: '100%',
359
- width: '100%',
360
- position: 'relative',
361
- boxSizing: 'border-box',
362
- },
363
- ':host form': {
364
- display: 'flex',
365
- flex: '1 1 auto',
366
- position: 'relative',
367
- overflow: 'hidden',
368
- },
369
- };
370
- content = [
371
- slot({ part: 'header', name: 'header' }),
372
- form({ part: 'form' }, slot({ part: 'content' })),
373
- slot({ part: 'footer', name: 'footer' }),
374
- ];
375
- getField = (key) => {
376
- return this.querySelector(`xin-field[key="${key}"]`);
377
- };
378
- get fields() {
379
- if (typeof this.value === 'string') {
380
- try {
381
- this.value = JSON.parse(this.value);
382
- }
383
- catch (e) {
384
- console.log('<xin-form> could not use its value, expects valid JSON');
385
- this.value = {};
386
- }
387
- }
388
- const { getField } = this;
389
- const dispatch = this.dispatchEvent.bind(this);
390
- return new Proxy(this.value, {
391
- get(target, prop) {
392
- return target[prop];
393
- },
394
- set(target, prop, newValue) {
395
- if (target[prop] !== newValue) {
396
- target[prop] = newValue;
397
- const field = getField(prop);
398
- if (field) {
399
- field.value = newValue;
400
- }
401
- dispatch(new Event('change'));
402
- }
403
- return true;
404
- },
405
- });
406
- }
407
- set fields(values) {
408
- const fields = [...this.querySelectorAll(XinField.tagName)];
409
- for (const field of fields) {
410
- field.value = values[field.key];
411
- }
412
- }
413
- submit = () => {
414
- this.parts.form.dispatchEvent(new Event('submit'));
415
- };
416
- handleSubmit = (event) => {
417
- event.preventDefault();
418
- event.stopPropagation();
419
- this.submitCallback(this.value, this.isValid);
420
- };
421
- submitCallback = (value, isValid) => {
422
- console.log('override submitCallback to handle this data', {
423
- value,
424
- isValid,
425
- });
426
- };
427
- connectedCallback() {
428
- super.connectedCallback();
429
- const { form } = this.parts;
430
- form.addEventListener('submit', this.handleSubmit);
431
- }
432
- }
433
- export const xinField = XinField.elementCreator({
434
- tag: 'xin-field',
435
- styleSpec: {
436
- ':host [part="field"]': {
437
- position: 'relative',
438
- display: 'flex',
439
- alignItems: 'center',
440
- gap: varDefault.prefixSuffixGap('8px'),
441
- },
442
- ':host [part="field"][prefix]::before': {
443
- content: 'attr(prefix)',
444
- },
445
- ':host [part="field"][suffix]::after': {
446
- content: 'attr(suffix)',
447
- },
448
- ':host [part="field"] > *, :host [part="input"] > *': {
449
- width: '100%',
450
- },
451
- ':host textarea': {
452
- resize: 'none',
453
- },
454
- ':host input[type="checkbox"]': {
455
- width: 'fit-content',
456
- },
457
- ':host .hidden': {
458
- position: 'absolute',
459
- pointerEvents: 'none',
460
- opacity: 0,
461
- },
462
- },
463
- });
464
- export const xinForm = XinForm.elementCreator({
465
- tag: 'xin-form',
466
- });
package/dist/gamepad.js DELETED
@@ -1,115 +0,0 @@
1
- /*#
2
- # gamepads
3
-
4
- A couple of utility functions for dealing with gamepads and XRInputs.
5
-
6
- `gamepadState()` gives you a condensed version of active gamepad state
7
-
8
- `gamepadText()` provides the above in minimal text form for debugging
9
-
10
- ```js
11
- const { elements } = xinjs
12
- const { gamepadText } = xinjsui
13
-
14
- const pre = elements.pre()
15
- preview.append(pre)
16
-
17
- const interval = setInterval(() => {
18
- if (!pre.closest('body')) {
19
- clearInterval(interval)
20
- } else {
21
- pre.textContent = gamepadText()
22
- }
23
- }, 100)
24
- ```
25
-
26
- ## XRInput Devices
27
-
28
- > This is experimental, the API is changing and stuff doesn't work. Hopefully it
29
- > will become a lot more generally useful once Apple's VisionPro comes out.
30
-
31
- `xrControllers(babylonjsXRHelper)` returns an `XinXRControllerMap` structure that tries to
32
- conveniently render the current state of XR controls. (The babylonjs API for this is horrific!)
33
-
34
- `xrControllerText(controllerMap)` renders the map output by the above in a compact form
35
- which is useful when debugging.
36
- */
37
- export function gamepadState() {
38
- const gamepads = navigator
39
- .getGamepads()
40
- .filter((p) => p !== null);
41
- return gamepads.map((p) => {
42
- const { id, axes, buttons } = p;
43
- return {
44
- id,
45
- axes,
46
- buttons: buttons
47
- .map((button, index) => {
48
- const { pressed, value } = button;
49
- return {
50
- index,
51
- pressed,
52
- value,
53
- };
54
- })
55
- .filter((b) => b.pressed || b.value !== 0)
56
- .reduce((map, button) => {
57
- map[button.index] = button.value;
58
- return map;
59
- }, {}),
60
- };
61
- });
62
- }
63
- export function gamepadText() {
64
- const state = gamepadState();
65
- return state.length === 0
66
- ? 'no active gamepads'
67
- : state
68
- .map(({ id, axes, buttons }) => {
69
- const axesText = axes.map((a) => a.toFixed(2)).join(' ');
70
- const buttonText = Object.keys(buttons)
71
- .map((key) => `[${key}](${buttons[Number(key)].toFixed(2)})`)
72
- .join(' ');
73
- return `${id}\n${axesText}\n${buttonText}`;
74
- })
75
- .join('\n');
76
- }
77
- export function xrControllers(xrHelper) {
78
- const controllers = {};
79
- xrHelper.input.onControllerAddedObservable.add((controller) => {
80
- controller.onMotionControllerInitObservable.add((mc) => {
81
- const state = {};
82
- const componentIds = mc.getComponentIds();
83
- componentIds.forEach((componentId) => {
84
- const component = mc.getComponent(componentId);
85
- state[componentId] = { pressed: component.pressed };
86
- component.onButtonStateChangedObservable.add(() => {
87
- state[componentId].pressed = component.pressed;
88
- });
89
- // TODO does this work?! inquiring minds…
90
- if (component.onAxisValueChangedObservable) {
91
- state[componentId].axes = [];
92
- component.onAxisValueChangedObservable.add((axes) => {
93
- state[componentId].axes = axes;
94
- });
95
- }
96
- });
97
- controllers[mc.handedness] = state;
98
- });
99
- });
100
- return controllers;
101
- }
102
- export function xrControllersText(controllers) {
103
- if (controllers === undefined || Object.keys(controllers).length === 0) {
104
- return 'no xr inputs';
105
- }
106
- return Object.keys(controllers)
107
- .map((controllerId) => {
108
- const state = controllers[controllerId];
109
- const buttonText = Object.keys(state)
110
- .filter((componentId) => state[componentId].pressed)
111
- .join(' ');
112
- return `${controllerId}\n${buttonText}`;
113
- })
114
- .join('\n');
115
- }