@rogieking/figui3 1.0.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.
Files changed (3) hide show
  1. package/fig.css +1326 -0
  2. package/fig.js +984 -0
  3. package/package.json +4 -0
package/fig.js ADDED
@@ -0,0 +1,984 @@
1
+ function uniqueId() {
2
+ return Date.now().toString(36) + Math.random().toString(36).substring(2)
3
+ }
4
+ function supportsPopover() {
5
+ return HTMLElement.prototype.hasOwnProperty("popover");
6
+ }
7
+
8
+
9
+ /* Button */
10
+ class FigButton extends HTMLElement {
11
+ constructor() {
12
+ super()
13
+ this.attachShadow({ mode: 'open' })
14
+ }
15
+ connectedCallback() {
16
+ this.render()
17
+ }
18
+ render() {
19
+ this.shadowRoot.innerHTML = `
20
+ <style>
21
+ button, button:hover, button:active {
22
+ padding: 0;
23
+ appearance: none;
24
+ display: block;
25
+ border: 0;
26
+ font: inherit;
27
+ background: transparent;
28
+ }
29
+ </style>
30
+ <button>
31
+ <slot></slot>
32
+ </button>
33
+ `;
34
+ this.button = this.shadowRoot.querySelector('button')
35
+ }
36
+ static get observedAttributes() {
37
+ return ['disabled'];
38
+ }
39
+ attributeChangedCallback(name, oldValue, newValue) {
40
+ if (this.button) {
41
+ this.button[name] = newValue
42
+ if (newValue === "false") {
43
+ this.button.removeAttribute(name)
44
+ }
45
+ }
46
+ }
47
+ }
48
+ window.customElements.define('fig-button', FigButton);
49
+
50
+ /* Dropdown */
51
+ class FigDropdown extends HTMLElement {
52
+ constructor() {
53
+ super();
54
+ this.attachShadow({ mode: 'open' });
55
+ }
56
+
57
+ connectedCallback() {
58
+ this.render()
59
+ }
60
+
61
+ render() {
62
+ this.select = document.createElement('select')
63
+ this.optionsSlot = document.createElement('slot')
64
+
65
+ this.appendChild(this.select)
66
+ this.shadowRoot.appendChild(this.optionsSlot)
67
+
68
+ // Move slotted options into the select element
69
+ this.optionsSlot.addEventListener('slotchange', this.slotChange.bind(this));
70
+ }
71
+ slotChange() {
72
+ while (this.select.firstChild) {
73
+ this.select.firstChild.remove()
74
+ }
75
+ this.optionsSlot.assignedNodes().forEach(node => {
76
+ if (node.nodeName === 'OPTION') {
77
+ this.select.appendChild(node.cloneNode(true))
78
+ }
79
+ })
80
+ }
81
+ }
82
+
83
+ customElements.define('fig-dropdown', FigDropdown);
84
+
85
+ /* Tooltip */
86
+ class FigTooltip extends HTMLElement {
87
+ constructor() {
88
+ super()
89
+ this.action = this.getAttribute("action") || "hover"
90
+ this.delay = parseInt(this.getAttribute("delay")) || 500
91
+ this.isOpen = false
92
+ }
93
+ connectedCallback() {
94
+ this.setup()
95
+ this.setupEventListeners()
96
+ }
97
+
98
+ disconnectedCallback() {
99
+ this.destroy()
100
+ }
101
+
102
+ setup() {
103
+
104
+ }
105
+
106
+ render() {
107
+ this.destroy()
108
+ this.popup = document.createElement('span');
109
+ this.popup.setAttribute("class", "fig-tooltip")
110
+ this.popup.style.position = "fixed"
111
+ this.popup.style.pointerEvents = "none"
112
+ this.popup.innerText = this.getAttribute("text")
113
+ document.body.append(this.popup)
114
+ }
115
+
116
+ destroy() {
117
+ if (this.popup) {
118
+ this.popup.remove()
119
+ }
120
+ document.body.addEventListener("click", this.hidePopupOutsideClick)
121
+ }
122
+
123
+ setupEventListeners() {
124
+ if (this.action === "hover") {
125
+ this.addEventListener("mouseenter", this.showDelayedPopup.bind(this));
126
+ this.addEventListener("mouseleave", this.hidePopup.bind(this));
127
+ } else if (this.action === "click") {
128
+ this.addEventListener("click", this.showDelayedPopup.bind(this));
129
+ document.body.addEventListener("click", this.hidePopupOutsideClick.bind(this))
130
+ }
131
+ }
132
+
133
+ getOffset() {
134
+ const defaultOffset = { left: 8, top: 4, right: 8, bottom: 4 };
135
+ const offsetAttr = this.getAttribute("offset");
136
+ if (!offsetAttr) return defaultOffset;
137
+
138
+ const [left, top, right, bottom] = offsetAttr.split(",").map(Number);
139
+ return {
140
+ left: isNaN(left) ? defaultOffset.left : left,
141
+ top: isNaN(top) ? defaultOffset.top : top,
142
+ right: isNaN(right) ? defaultOffset.right : right,
143
+ bottom: isNaN(bottom) ? defaultOffset.bottom : bottom
144
+ };
145
+ }
146
+
147
+ showDelayedPopup() {
148
+ this.render()
149
+ clearTimeout(this.timeout)
150
+ this.timeout = setTimeout(this.showPopup.bind(this), this.delay)
151
+ }
152
+
153
+ showPopup() {
154
+ const rect = this.getBoundingClientRect()
155
+ const popupRect = this.popup.getBoundingClientRect()
156
+ const offset = this.getOffset()
157
+
158
+ // Position the tooltip above the element
159
+ let top = rect.top - popupRect.height - offset.top
160
+ let left = rect.left + (rect.width - popupRect.width) / 2
161
+ this.popup.setAttribute("position", "top")
162
+
163
+ // Adjust if tooltip would go off-screen
164
+ if (top < 0) {
165
+ this.popup.setAttribute("position", "bottom")
166
+ top = rect.bottom + offset.bottom; // Position below instead
167
+ }
168
+ if (left < offset.left) {
169
+ left = offset.left;
170
+ } else if (left + popupRect.width > window.innerWidth - offset.right) {
171
+
172
+ left = window.innerWidth - popupRect.width - offset.right;
173
+ }
174
+
175
+ this.popup.style.top = `${top}px`;
176
+ this.popup.style.left = `${left}px`;
177
+ this.popup.style.opacity = "1";
178
+ this.popup.style.display = "block"
179
+ this.popup.style.pointerEvents = "all"
180
+ this.popup.style.zIndex = parseInt((new Date()).getTime() / 1000)
181
+
182
+ this.isOpen = true
183
+ }
184
+
185
+ hidePopup() {
186
+ clearTimeout(this.timeout)
187
+ this.popup.style.opacity = "0"
188
+ this.popup.style.display = "block"
189
+ this.popup.style.pointerEvents = "none"
190
+ this.destroy()
191
+ this.isOpen = false
192
+ }
193
+
194
+ hidePopupOutsideClick(event) {
195
+ if (this.isOpen && !this.popup.contains(event.target)) {
196
+ this.hidePopup()
197
+ }
198
+ }
199
+ }
200
+
201
+ customElements.define("fig-tooltip", FigTooltip);
202
+
203
+ /* Popover */
204
+ class FigPopover extends FigTooltip {
205
+ static observedAttributes = ["action", "size"];
206
+
207
+ constructor() {
208
+ super()
209
+ this.action = this.getAttribute("action") || "click"
210
+ this.delay = parseInt(this.getAttribute("delay")) || 0
211
+ }
212
+ render() {
213
+ //this.destroy()
214
+ //if (!this.popup) {
215
+ this.popup = this.popup || this.querySelector("[popover]")
216
+ this.popup.setAttribute("class", "fig-popover")
217
+ this.popup.style.position = "fixed"
218
+ this.popup.style.display = "block"
219
+ this.popup.style.pointerEvents = "none"
220
+ document.body.append(this.popup)
221
+ //}
222
+ }
223
+
224
+ destroy() {
225
+
226
+ }
227
+ }
228
+ customElements.define("fig-popover", FigPopover);
229
+
230
+ /* Dialog */
231
+ class FigDialog extends HTMLElement {
232
+ constructor() {
233
+ super();
234
+ this.attachShadow({ mode: 'open' })
235
+ this.dialog = document.createElement('dialog')
236
+ this.contentSlot = document.createElement('slot')
237
+ }
238
+
239
+ connectedCallback() {
240
+ this.render()
241
+ }
242
+
243
+ disconnectedCallback() {
244
+ this.contentSlot.removeEventListener('slotchange', this.slotChange)
245
+ }
246
+
247
+ render() {
248
+ this.appendChild(this.dialog)
249
+ this.shadowRoot.appendChild(this.contentSlot)
250
+ this.contentSlot.addEventListener('slotchange', this.slotChange.bind(this))
251
+ }
252
+
253
+ slotChange() {
254
+ while (this.dialog.firstChild) {
255
+ this.dialog.firstChild.remove()
256
+ }
257
+ this.contentSlot.assignedNodes().forEach(node => {
258
+ if (node !== this.dialog) {
259
+ this.dialog.appendChild(node.cloneNode(true))
260
+ }
261
+ })
262
+ }
263
+
264
+ static get observedAttributes() {
265
+ return ['open'];
266
+ }
267
+
268
+ attributeChangedCallback(name, oldValue, newValue) {
269
+ switch (name) {
270
+ case "open":
271
+ if (this?.show) {
272
+ this[newValue === "true" ? "show" : "close"]()
273
+ }
274
+ break;
275
+ }
276
+ }
277
+
278
+ /* Public methods */
279
+ show() {
280
+ console.log("show dialog", this.dialog, this.dialog?.show)
281
+ this.dialog.show()
282
+ }
283
+ close() {
284
+ this.dialog.close()
285
+ }
286
+ }
287
+ customElements.define("fig-dialog", FigDialog);
288
+
289
+ /*
290
+ class FigDialog extends FigTooltip {
291
+
292
+ constructor() {
293
+ super()
294
+ this.action = this.getAttribute("action") || "click"
295
+ this.delay = parseInt(this.getAttribute("delay")) || 0
296
+ this.dialog = document.createElement("dialog")
297
+ this.header = document.createElement("fig-header")
298
+ this.header.innerHTML = `<span>${this.getAttribute("title") || "Title"}</span>`
299
+ if (this.getAttribute("closebutton") !== "false") {
300
+ this.closeButton = document.createElement("fig-button")
301
+ this.closeButton.setAttribute("icon", "true")
302
+ this.closeButton.setAttribute("variant", "ghost")
303
+ this.closeButton.setAttribute("fig-dialog-close", "true")
304
+ let closeIcon = document.createElement("fig-icon")
305
+ closeIcon.setAttribute("class", "close")
306
+ this.closeButton.append(closeIcon)
307
+ this.header.append(this.closeButton)
308
+ }
309
+ this.dialog.append(this.header)
310
+ }
311
+ render() {
312
+ this.popup = this.popup || this.dialog
313
+ document.body.append(this.popup)
314
+ }
315
+ setup() {
316
+ this.dialog.querySelectorAll("[fig-dialog-close]").forEach(e => e.addEventListener("click", this.hidePopup.bind(this)))
317
+ this.dialog.append(this.querySelector("fig-content") || "")
318
+ }
319
+ hidePopup() {
320
+ this.popup.close()
321
+ }
322
+ showPopup() {
323
+ this.popup.style.zIndex = parseInt((new Date()).getTime() / 1000)
324
+ if (this.getAttribute("modal") === "true") {
325
+ this.popup.showModal()
326
+ } else {
327
+ this.popup.show()
328
+ }
329
+ }
330
+ destroy() {
331
+
332
+ }
333
+ }
334
+ customElements.define("fig-dialog", FigDialog);
335
+ */
336
+
337
+
338
+ class FigPopover2 extends HTMLElement {
339
+ #popover
340
+ #trigger
341
+ #id
342
+ #delay
343
+ #timeout
344
+ #action
345
+
346
+ constructor() {
347
+ super()
348
+ }
349
+ connectedCallback() {
350
+ this.#popover = this.querySelector('[popover]')
351
+ this.#trigger = this
352
+ this.#delay = Number(this.getAttribute("delay")) || 0
353
+ this.#action = this.getAttribute("trigger-action") || "click"
354
+ this.#id = `tooltip-${uniqueId()}`
355
+ if (this.#popover) {
356
+ this.#popover.setAttribute("id", this.#id)
357
+ this.#popover.setAttribute("role", "tooltip")
358
+ this.#popover.setAttribute("popover", "manual")
359
+ this.#popover.style['position-anchor'] = `--${this.#id}`
360
+
361
+ this.#trigger.setAttribute("popovertarget", this.#id)
362
+ this.#trigger.setAttribute("popovertargetaction", "toggle")
363
+ this.#trigger.style['anchor-name'] = `--${this.#id}`
364
+
365
+ if (this.#action === "hover") {
366
+ this.#trigger.addEventListener("mouseover", this.handleOpen.bind(this))
367
+ this.#trigger.addEventListener("mouseout", this.handleClose.bind(this))
368
+ } else {
369
+ this.#trigger.addEventListener("click", this.handleToggle.bind(this))
370
+ }
371
+
372
+ document.body.append(this.#popover)
373
+ }
374
+ }
375
+
376
+ handleClose() {
377
+ clearTimeout(this.#timeout)
378
+ this.#popover.hidePopover()
379
+ }
380
+ handleToggle() {
381
+ if (this.#popover.matches(':popover-open')) {
382
+ this.handleClose()
383
+ } else {
384
+ this.handleOpen()
385
+ }
386
+ }
387
+ handleOpen() {
388
+ clearTimeout(this.#timeout)
389
+ this.#timeout = setTimeout(() => {
390
+ this.#popover.showPopover()
391
+ }, this.#delay)
392
+ }
393
+ }
394
+ window.customElements.define('fig-popover-2', FigPopover2);
395
+
396
+
397
+ /* Tabs */
398
+ class FigTabs extends HTMLElement {
399
+ constructor() {
400
+ super()
401
+ }
402
+ connectedCallback() {
403
+ const tabs = this.querySelectorAll('fig-tab')
404
+ const name = this.getAttribute("name") || ("tabs-" + this.uniqueId())
405
+ for (const tab of tabs) {
406
+ let input = document.createElement('input')
407
+ input.type = "radio"
408
+ input.name = name
409
+ input.checked = tab.hasAttribute("selected")
410
+ input.value = tab.getAttribute("value") || this.slugify(tab.innerText)
411
+ tab.setAttribute("label", tab.innerText)
412
+ tab.append(input)
413
+ input.addEventListener("input", this.handleInput.bind(this))
414
+ }
415
+ }
416
+ uniqueId() {
417
+ return Date.now().toString(36) + Math.random().toString(36).substring(2)
418
+ }
419
+ slugify(text) {
420
+ return text
421
+ .toLowerCase()
422
+ .trim()
423
+ .replace(/[^\w\s-]/g, '')
424
+ .replace(/[\s_-]+/g, '-')
425
+ .replace(/^-+|-+$/g, '');
426
+ }
427
+ handleInput() {
428
+ const radios = this.querySelectorAll("[type=radio]")
429
+ for (const radio of radios) {
430
+ if (radio.checked) {
431
+ this.value = radio.value
432
+ radio.parentNode.setAttribute("selected", "")
433
+ } else {
434
+ radio.parentNode.removeAttribute("selected")
435
+ }
436
+ }
437
+ }
438
+ }
439
+ window.customElements.define('fig-tabs', FigTabs);
440
+
441
+ /* Segmented Control */
442
+ class FigSegmentedControl extends HTMLElement {
443
+ constructor() {
444
+ super()
445
+ }
446
+ connectedCallback() {
447
+ const segments = this.querySelectorAll('fig-segment')
448
+ const name = this.getAttribute("name") || "segmented-control"
449
+ for (const segment of segments) {
450
+ let input = document.createElement('input')
451
+ input.type = "radio"
452
+ input.name = name
453
+ input.checked = segment.hasAttribute("selected")
454
+ input.value = segment.getAttribute("value")
455
+ segment.append(input)
456
+ input.addEventListener("input", this.handleInput.bind(this))
457
+ }
458
+ }
459
+ handleInput() {
460
+ this.value = this.querySelector(":checked").value
461
+ }
462
+ }
463
+ window.customElements.define('fig-segmented-control', FigSegmentedControl);
464
+
465
+
466
+
467
+ /* Slider */
468
+ class FigSlider extends HTMLElement {
469
+ #typeDefaults = {
470
+ range: { min: 0, max: 100, step: 1 },
471
+ hue: { min: 0, max: 255, step: 1 },
472
+ opacity: { min: 0, max: 1, step: 0.01, color: "#FF0000" }
473
+ }
474
+ constructor() {
475
+ super()
476
+ }
477
+ connectedCallback() {
478
+
479
+ this.value = this.getAttribute("value")
480
+ this.type = this.getAttribute("type") || "range"
481
+
482
+ const defaults = this.#typeDefaults[this.type]
483
+ this.min = this.getAttribute("min") || defaults.min
484
+ this.max = this.getAttribute("max") || defaults.max
485
+ this.step = this.getAttribute("step") || defaults.step
486
+ this.color = this.getAttribute("color") || defaults?.color
487
+ this.disabled = this.getAttribute("disabled") ? true : false
488
+
489
+ if (this.color) {
490
+ this.style.setProperty("--color", this.color)
491
+ }
492
+
493
+ let html = ''
494
+ let slider = `<div class="slider">
495
+ <input
496
+ type="range"
497
+ ${this.disabled ? "disabled" : ""}
498
+ min="${this.min}"
499
+ max="${this.max}"
500
+ step="${this.step}"
501
+ class="${this.type}"
502
+ value="${this.value}">
503
+ </div>`
504
+ if (this.getAttribute("text")) {
505
+ html = `${slider}
506
+ <fig-input-text
507
+ placeholder="##"
508
+ type="number"
509
+ min="${this.min}"
510
+ max="${this.max}"
511
+ step="${this.step}"
512
+ value="${this.value}">
513
+ </fig-input-text>`
514
+ } else {
515
+ html = slider
516
+ }
517
+ this.innerHTML = html
518
+
519
+ this.textInput = this.querySelector("input[type=number]")
520
+ this.slider = this.querySelector('.slider')
521
+
522
+ this.input = this.querySelector('[type=range]')
523
+ this.input.addEventListener("input", this.handleChange.bind(this))
524
+
525
+ if (this.textInput) {
526
+ this.textInput.addEventListener("change", this.handleChange.bind(this))
527
+ }
528
+ }
529
+ static get observedAttributes() {
530
+ return ['value', 'step', 'min', 'max', 'type', 'disabled']
531
+ }
532
+
533
+ focus() {
534
+ this.input.focus()
535
+ }
536
+
537
+ attributeChangedCallback(name, oldValue, newValue) {
538
+ if (this.input) {
539
+ console.log('attributeChangedCallback:', name, oldValue, newValue)
540
+ switch (name) {
541
+ case "color":
542
+ this.color = newValue
543
+ this.style.setProperty("--color", this.color)
544
+ break;
545
+ case "type":
546
+ this.input.className = newValue
547
+ break;
548
+ case "disabled":
549
+ this.disabled = this.input.disabled = (newValue === "true" || newValue === undefined && newValue !== null)
550
+ if (this.textInput) {
551
+ this.textInput.disabled = this.disabled
552
+ }
553
+ break;
554
+ default:
555
+ this[name] = this.input[name] = newValue
556
+ if (this.textInput) {
557
+ this.textInput[name] = newValue
558
+ }
559
+ break;
560
+ }
561
+ }
562
+ }
563
+
564
+ syncProps() {
565
+ if (!CSS.supports('animation-timeline: scroll()')) {
566
+ let complete = this.input.value / (this.input.max - this.input.min)
567
+ this.slider.style.setProperty('--slider-percent', `${complete * 100}%`)
568
+ }
569
+ }
570
+ handleChange(event) {
571
+ this.value = event.target.value
572
+ this.input.value = this.value
573
+ if (this.textInput) {
574
+ this.textInput.value = this.value
575
+ }
576
+ this.syncProps()
577
+ }
578
+ }
579
+ window.customElements.define('fig-slider', FigSlider);
580
+
581
+ class FigInputText extends HTMLElement {
582
+ constructor() {
583
+ super()
584
+ }
585
+ connectedCallback() {
586
+ const append = this.querySelector('[slot=append]')
587
+ const prepend = this.querySelector('[slot=prepend]')
588
+ this.multiline = this.hasAttribute("multiline") || false
589
+ this.value = this.getAttribute("value")
590
+ this.type = this.getAttribute("type") || "text"
591
+ this.placeholder = this.getAttribute("placeholder")
592
+
593
+ let html = `<input
594
+ type="${this.type}"
595
+ placeholder="${this.placeholder}"
596
+ value="${this.value}" />`
597
+ if (this.multiline) {
598
+ html = `<textarea
599
+ placeholder="${this.placeholder}">${this.value}</textarea>`
600
+ }
601
+ this.innerHTML = html
602
+
603
+ this.input = this.querySelector('input,textarea')
604
+ this.input.addEventListener('input', this.handleInput.bind(this))
605
+
606
+ if (prepend) {
607
+ prepend.addEventListener('click', this.focus.bind(this))
608
+ this.prepend(prepend)
609
+ }
610
+ if (append) {
611
+ append.addEventListener('click', this.focus.bind(this))
612
+ this.append(append)
613
+ }
614
+ }
615
+ focus() {
616
+ this.input.focus()
617
+ }
618
+ handleInput() {
619
+ this.value = this.input.value
620
+ }
621
+
622
+ static get observedAttributes() {
623
+ return ['value', 'placeholder', 'label', 'disabled', 'type'];
624
+ }
625
+
626
+ attributeChangedCallback(name, oldValue, newValue) {
627
+ if (this.input) {
628
+ switch (name) {
629
+ case "label":
630
+ this.disabled = this.input.disabled = newValue
631
+ break;
632
+ default:
633
+ this[name] = this.input[name] = newValue
634
+ break;
635
+ }
636
+ }
637
+ }
638
+ }
639
+ window.customElements.define('fig-input-text', FigInputText);
640
+
641
+
642
+ /* Form Field */
643
+ class FigField extends HTMLElement {
644
+ constructor() {
645
+ super()
646
+ }
647
+ connectedCallback() {
648
+ this.label = this.querySelector('label')
649
+ this.input = Array.from(this.childNodes).find(node => node.nodeName.toLowerCase().startsWith("fig-"))
650
+ console.log('input:', this.input)
651
+ if (this.input) {
652
+ this.label.addEventListener('click', this.focus.bind(this))
653
+ }
654
+ }
655
+ focus() {
656
+ console.log('input:', this.input)
657
+ this.input.focus()
658
+ }
659
+ }
660
+ window.customElements.define('fig-field', FigField);
661
+
662
+
663
+ /* Color swatch */
664
+ class FigInputColor extends HTMLElement {
665
+ #rgba
666
+ #swatch
667
+ textInput
668
+ #alphaInput
669
+ constructor() {
670
+ super()
671
+ }
672
+ connectedCallback() {
673
+ this.#rgba = this.convertToRGBA(this.getAttribute("value"))
674
+ const alpha = (this.#rgba.a * 100).toFixed(0)
675
+ this.value = this.rgbAlphaToHex(
676
+ {
677
+ r: this.#rgba.r,
678
+ g: this.#rgba.g,
679
+ b: this.#rgba.b
680
+ },
681
+ alpha
682
+ )
683
+ let html = ``
684
+ if (this.getAttribute("text")) {
685
+ let label = `<fig-input-text placeholder="Text" value="${this.getAttribute("value")}"></fig-input-text>`
686
+ if (this.getAttribute("alpha")) {
687
+ label += `<fig-tooltip text="Opacity">
688
+ <fig-input-text
689
+ placeholder="##"
690
+ type="number"
691
+ min="0"
692
+ max="100"
693
+ value="${alpha}">
694
+ <span slot="append">%</slot>
695
+ </fig-input-text>
696
+ </fig-tooltip>`
697
+ }
698
+ html = `<div class="input-combo">
699
+ <input type="color" value="${this.value}" />
700
+ ${label}
701
+ </div>`
702
+ } else {
703
+ html = `<input type="color" value="${this.value}" />`
704
+ }
705
+ this.innerHTML = html
706
+
707
+ this.style.setProperty('--alpha', this.#rgba.a)
708
+
709
+
710
+ this.#swatch = this.querySelector('[type=color]')
711
+ this.textInput = this.querySelector('[type=text]')
712
+ this.#alphaInput = this.querySelector('[type=number]')
713
+
714
+
715
+ this.#swatch.disabled = this.hasAttribute('disabled')
716
+ this.#swatch.addEventListener('input', this.handleInput.bind(this))
717
+
718
+ if (this.textInput) {
719
+ this.textInput.value = this.#swatch.value = this.rgbAlphaToHex(this.#rgba, 1)
720
+ }
721
+
722
+ if (this.#alphaInput) {
723
+ this.#alphaInput.addEventListener('input', this.handleAlphaInput.bind(this))
724
+ }
725
+
726
+ }
727
+ handleAlphaInput(event) {
728
+ //do not propagate to onInput handler for web component
729
+ event.stopPropagation()
730
+ this.#rgba = this.convertToRGBA(this.#swatch.value)
731
+ this.#rgba.a = Number(this.#alphaInput.value) / 100
732
+ this.value = this.rgbAlphaToHex(
733
+ {
734
+ r: this.#rgba.r,
735
+ g: this.#rgba.g,
736
+ b: this.#rgba.b
737
+ },
738
+ this.#rgba.a
739
+ )
740
+ this.style.setProperty('--alpha', this.#rgba.a)
741
+ const e = new CustomEvent('input', {
742
+ bubbles: true,
743
+ cancelable: true
744
+ });
745
+ this.dispatchEvent(e)
746
+ }
747
+
748
+ focus() {
749
+ this.#swatch.focus()
750
+ }
751
+
752
+ handleInput(event) {
753
+ //do not propagate to onInput handler for web component
754
+ event.stopPropagation()
755
+
756
+ let alpha = this.#rgba.a
757
+ this.#rgba = this.convertToRGBA(this.#swatch.value)
758
+ this.#rgba.a = alpha
759
+ if (this.textInput) {
760
+ this.textInput.value = this.#swatch.value
761
+ }
762
+ this.style.setProperty('--alpha', this.#rgba.a)
763
+ this.value = this.rgbAlphaToHex(
764
+ {
765
+ r: this.#rgba.r,
766
+ g: this.#rgba.g,
767
+ b: this.#rgba.b
768
+ },
769
+ alpha
770
+ )
771
+ this.alpha = alpha
772
+ if (this.#alphaInput) {
773
+ this.#alphaInput.value = this.#rgba.a.toFixed(0)
774
+ }
775
+ const e = new CustomEvent('input', {
776
+ bubbles: true,
777
+ cancelable: true
778
+ });
779
+ this.dispatchEvent(e)
780
+ }
781
+
782
+ static get observedAttributes() {
783
+ return ['value', 'style'];
784
+ }
785
+
786
+ attributeChangedCallback(name, oldValue, newValue) {
787
+ //this[name] = newValue;
788
+ }
789
+
790
+ rgbAlphaToHex({ r, g, b }, a = 1) {
791
+ // Ensure r, g, b are integers between 0 and 255
792
+ r = Math.max(0, Math.min(255, Math.round(r)));
793
+ g = Math.max(0, Math.min(255, Math.round(g)));
794
+ b = Math.max(0, Math.min(255, Math.round(b)));
795
+
796
+ // Ensure alpha is between 0 and 1
797
+ a = Math.max(0, Math.min(1, a));
798
+
799
+ // Convert to hex and pad with zeros if necessary
800
+ const hexR = r.toString(16).padStart(2, '0');
801
+ const hexG = g.toString(16).padStart(2, '0');
802
+ const hexB = b.toString(16).padStart(2, '0');
803
+
804
+ // If alpha is 1, return 6-digit hex
805
+ if (a === 1) {
806
+ return `#${hexR}${hexG}${hexB}`;
807
+ }
808
+
809
+ // Otherwise, include alpha in 8-digit hex
810
+ const alpha = Math.round(a * 255);
811
+ const hexA = alpha.toString(16).padStart(2, '0');
812
+ return `#${hexR}${hexG}${hexB}${hexA}`;
813
+ }
814
+
815
+ convertToRGBA(color) {
816
+ let r, g, b, a = 1;
817
+
818
+ // Handle hex colors
819
+ if (color.startsWith('#')) {
820
+ let hex = color.slice(1);
821
+ if (hex.length === 8) {
822
+ a = parseInt(hex.slice(6), 16) / 255;
823
+ hex = hex.slice(0, 6);
824
+ }
825
+ r = parseInt(hex.slice(0, 2), 16);
826
+ g = parseInt(hex.slice(2, 4), 16);
827
+ b = parseInt(hex.slice(4, 6), 16);
828
+ }
829
+ // Handle rgba colors
830
+ else if (color.startsWith('rgba') || color.startsWith('rgb')) {
831
+ let matches = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/);
832
+ if (matches) {
833
+ r = parseInt(matches[1]);
834
+ g = parseInt(matches[2]);
835
+ b = parseInt(matches[3]);
836
+ a = matches[4] ? parseFloat(matches[4]) : 1;
837
+ }
838
+ }
839
+ // Handle hsla colors
840
+ else if (color.startsWith('hsla') || color.startsWith('hsl')) {
841
+ let matches = color.match(/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*(\d+(?:\.\d+)?))?\)/);
842
+ if (matches) {
843
+ let h = parseInt(matches[1]) / 360;
844
+ let s = parseInt(matches[2]) / 100;
845
+ let l = parseInt(matches[3]) / 100;
846
+ a = matches[4] ? parseFloat(matches[4]) : 1;
847
+
848
+ if (s === 0) {
849
+ r = g = b = l; // achromatic
850
+ } else {
851
+ let hue2rgb = (p, q, t) => {
852
+ if (t < 0) t += 1;
853
+ if (t > 1) t -= 1;
854
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
855
+ if (t < 1 / 2) return q;
856
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
857
+ return p;
858
+ };
859
+
860
+ let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
861
+ let p = 2 * l - q;
862
+ r = hue2rgb(p, q, h + 1 / 3);
863
+ g = hue2rgb(p, q, h);
864
+ b = hue2rgb(p, q, h - 1 / 3);
865
+ }
866
+
867
+ r = Math.round(r * 255);
868
+ g = Math.round(g * 255);
869
+ b = Math.round(b * 255);
870
+ }
871
+ }
872
+ // If it's not recognized, return null
873
+ else {
874
+ return null;
875
+ }
876
+
877
+ return { r, g, b, a };
878
+ }
879
+ }
880
+ window.customElements.define('fig-input-color', FigInputColor);
881
+
882
+ /* Checkbox */
883
+ class FigCheckbox extends HTMLElement {
884
+
885
+ constructor() {
886
+ super()
887
+ this.input = document.createElement("input")
888
+ this.input.setAttribute("id", uniqueId())
889
+ this.input.setAttribute("type", "checkbox")
890
+ this.labelElement = document.createElement("label")
891
+ this.labelElement.setAttribute("for", this.input.id)
892
+ }
893
+ connectedCallback() {
894
+ this.checked = this.input.checked = this.hasAttribute("checked") ? this.getAttribute('checked').toLowerCase() === "true" : false
895
+ this.input.addEventListener("change", this.handleInput.bind(this))
896
+
897
+ if (this.hasAttribute('disabled')) {
898
+ this.input.disabled = true
899
+ }
900
+ if (this.hasAttribute('indeterminate')) {
901
+ this.input.indeterminate = true
902
+ this.input.setAttribute("indeterminate", "true")
903
+ }
904
+
905
+ this.append(this.input)
906
+ this.append(this.labelElement)
907
+
908
+ this.render()
909
+ }
910
+ static get observedAttributes() {
911
+ return ["on", "disabled", "label", "checked"];
912
+ }
913
+
914
+ render() {
915
+
916
+ }
917
+
918
+ focus() {
919
+ this.input.focus()
920
+ }
921
+
922
+ disconnectedCallback() {
923
+ this.input.remove()
924
+ }
925
+
926
+ attributeChangedCallback(name, oldValue, newValue) {
927
+ console.log(`Attribute ${name} change:`, oldValue, newValue);
928
+ switch (name) {
929
+ case "label":
930
+ this.labelElement.innerText = newValue
931
+ break;
932
+ case "checked":
933
+ this.checked = this.input.checked = this.hasAttribute("checked") ? true : false
934
+ break;
935
+ }
936
+ }
937
+
938
+ handleInput(e) {
939
+ this.input.indeterminate = false
940
+ this.input.removeAttribute("indeterminate")
941
+ this.value = this.input.value
942
+ }
943
+
944
+ }
945
+ window.customElements.define('fig-checkbox', FigCheckbox);
946
+
947
+ /* Switch */
948
+ class FigSwitch extends FigCheckbox {
949
+ render() {
950
+ this.input.setAttribute("class", "switch")
951
+ this.on = this.input.checked = this.hasAttribute("on") ? this.getAttribute('on').toLowerCase() === "true" : false
952
+ }
953
+ }
954
+ window.customElements.define('fig-switch', FigSwitch);
955
+
956
+
957
+ /* Template */
958
+ class MyCustomElement extends HTMLElement {
959
+ static observedAttributes = ["color", "size"];
960
+
961
+ constructor() {
962
+ // Always call super first in constructor
963
+ super();
964
+ }
965
+
966
+ connectedCallback() {
967
+ console.log("Custom element added to page.");
968
+ }
969
+
970
+ disconnectedCallback() {
971
+ console.log("Custom element removed from page.");
972
+ }
973
+
974
+ adoptedCallback() {
975
+ console.log("Custom element moved to new page.");
976
+ }
977
+
978
+ attributeChangedCallback(name, oldValue, newValue) {
979
+ console.log(`Attribute ${name} has changed.`);
980
+ }
981
+ }
982
+
983
+ customElements.define("my-custom-element", MyCustomElement);
984
+