@e280/sly 0.2.0-3 → 0.2.0-5

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 (61) hide show
  1. package/README.md +217 -17
  2. package/package.json +1 -1
  3. package/s/demo/demo.bundle.ts +6 -1
  4. package/s/demo/views/demo.ts +1 -0
  5. package/s/demo/views/incredi.ts +28 -0
  6. package/s/dom/dom.ts +25 -13
  7. package/s/index.html.ts +1 -0
  8. package/s/index.ts +1 -0
  9. package/s/loot/drag-and-drops.ts +82 -0
  10. package/s/loot/{drop.ts → drops.ts} +8 -17
  11. package/s/loot/helpers.ts +3 -3
  12. package/s/loot/index.ts +2 -2
  13. package/s/views/attributes.ts +6 -6
  14. package/s/views/base-element.ts +84 -0
  15. package/s/views/use.ts +1 -1
  16. package/s/views/view.ts +4 -4
  17. package/x/demo/demo.bundle.js +5 -1
  18. package/x/demo/demo.bundle.js.map +1 -1
  19. package/x/demo/demo.bundle.min.js +17 -14
  20. package/x/demo/demo.bundle.min.js.map +4 -4
  21. package/x/demo/views/demo.js +1 -0
  22. package/x/demo/views/demo.js.map +1 -1
  23. package/x/demo/views/incredi.d.ts +12 -0
  24. package/x/demo/views/incredi.js +21 -0
  25. package/x/demo/views/incredi.js.map +1 -0
  26. package/x/dom/dom.d.ts +6 -5
  27. package/x/dom/dom.js +19 -11
  28. package/x/dom/dom.js.map +1 -1
  29. package/x/index.d.ts +1 -0
  30. package/x/index.html +3 -2
  31. package/x/index.html.js +1 -0
  32. package/x/index.html.js.map +1 -1
  33. package/x/index.js +1 -0
  34. package/x/index.js.map +1 -1
  35. package/x/loot/drag-and-drops.d.ts +30 -0
  36. package/x/loot/drag-and-drops.js +63 -0
  37. package/x/loot/drag-and-drops.js.map +1 -0
  38. package/x/loot/{drop.d.ts → drops.d.ts} +3 -5
  39. package/x/loot/drops.js +25 -0
  40. package/x/loot/drops.js.map +1 -0
  41. package/x/loot/helpers.d.ts +3 -3
  42. package/x/loot/helpers.js +3 -3
  43. package/x/loot/helpers.js.map +1 -1
  44. package/x/loot/index.d.ts +2 -2
  45. package/x/loot/index.js +2 -2
  46. package/x/loot/index.js.map +1 -1
  47. package/x/views/attributes.d.ts +1 -1
  48. package/x/views/attributes.js +5 -5
  49. package/x/views/attributes.js.map +1 -1
  50. package/x/views/base-element.d.ts +14 -0
  51. package/x/views/base-element.js +62 -0
  52. package/x/views/base-element.js.map +1 -0
  53. package/x/views/use.js.map +1 -1
  54. package/x/views/view.js +4 -4
  55. package/x/views/view.js.map +1 -1
  56. package/s/loot/drag-drop.ts +0 -76
  57. package/x/loot/drag-drop.d.ts +0 -29
  58. package/x/loot/drag-drop.js +0 -54
  59. package/x/loot/drag-drop.js.map +0 -1
  60. package/x/loot/drop.js +0 -32
  61. package/x/loot/drop.js.map +0 -1
package/README.md CHANGED
@@ -9,7 +9,9 @@ sly replaces its predecessor, [slate](https://github.com/benevolent-games/slate)
9
9
 
10
10
  - 🍋 **views** — hooks-based, shadow-dom'd, componentizable
11
11
  - 🪄 **dom** — the "it's not jquery" multitool
12
+ - 🪵 **base element** — for a more classical experience
12
13
  - 🫛 **ops** — tools for async operations and loading spinners
14
+ - 🪙 **loot** — drag-and-drop facilities
13
15
  - 🧪 **testing page** — https://sly.e280.org/
14
16
 
15
17
 
@@ -45,11 +47,10 @@ view(use => () => html`<p>hello world</p>`)
45
47
  - views automatically rerender whenever any [strata-compatible](https://github.com/e280/strata) state changes
46
48
 
47
49
  ### 🍋 view example
48
- - **import stuff**
49
- ```ts
50
- import {view, dom} from "@e280/sly"
51
- import {html, css} from "lit"
52
- ```
50
+ ```ts
51
+ import {view, dom} from "@e280/sly"
52
+ import {html, css} from "lit"
53
+ ```
53
54
  - **declare a view**
54
55
  ```ts
55
56
  export const CounterView = view(use => (start: number) => {
@@ -267,6 +268,85 @@ view(use => () => html`<p>hello world</p>`)
267
268
 
268
269
 
269
270
 
271
+ <br/><br/>
272
+
273
+ ## 🦝🪵 sly base element
274
+ > *the classic experience*
275
+
276
+ ```ts
277
+ import {BaseElement, Use, attributes, dom} from "@e280/sly"
278
+ import {html, css} from "lit"
279
+ ```
280
+
281
+ `BaseElement` is a class-based approach to create a custom element web component.
282
+
283
+ it lets you expose js properties on the element instance, which helps you setup a better developer experience for people interacting with your element through the dom.
284
+
285
+ base element enjoys the same `use` hooks as views.
286
+
287
+ ### 🪵 base element setup
288
+ - **declare your element class**
289
+ ```ts
290
+ export class MyElement extends BaseElement {
291
+ static styles = css`span{color:orange}`
292
+
293
+ // custom property
294
+ start = 10
295
+
296
+ // custom attributes
297
+ attrs = attributes(this, {
298
+ multiply: Number,
299
+ })
300
+
301
+ // custom methods
302
+ hello() {
303
+ return "world"
304
+ }
305
+
306
+ render(use: Use) {
307
+ const $count = use.signal(1)
308
+ const increment = () => $count.value++
309
+
310
+ const {start} = this
311
+ const {multiply = 1} = this.attrs
312
+ const result = start + (multiply * $count())
313
+
314
+ return html`
315
+ <span>${result}</span>
316
+ <button @click="${increment}">+</button>
317
+ `
318
+ }
319
+ }
320
+ ```
321
+ - **register your element to the dom**
322
+ ```ts
323
+ dom.register({MyElement})
324
+ ```
325
+
326
+ ### 🪵 base element usage
327
+ - **place the element in your html body**
328
+ ```html
329
+ <body>
330
+ <my-element></my-element>
331
+ </body>
332
+ ```
333
+ - **now you can interact with it**
334
+ ```ts
335
+ const myElement = dom<MyElement>("my-element")
336
+
337
+ // js property
338
+ myElement.start = 100
339
+
340
+ // html attributes
341
+ myElement.attr.multiply = 2
342
+
343
+ // methods
344
+ myElement.hello()
345
+ // "world"
346
+ ```
347
+
348
+
349
+
270
350
  <br/><br/>
271
351
 
272
352
  ## 🦝🪄 sly dom
@@ -277,51 +357,51 @@ import {dom} from "@e280/sly"
277
357
  ```
278
358
 
279
359
  ### 🪄 dom queries
280
- - require an element
360
+ - `require` an element
281
361
  ```ts
282
362
  dom(".demo")
283
363
  // HTMLElement (or throws)
284
364
  ```
285
- - maybe get an element
365
+ - `maybe` get an element
286
366
  ```ts
287
367
  dom.maybe(".demo")
288
368
  // HTMLElement | undefined
289
369
  ```
290
- - select all elements
370
+ - `select` all elements
291
371
  ```ts
292
372
  dom.all(".demo ul li")
293
373
  // HTMLElement[]
294
374
  ```
295
- - within a specific container
375
+ - `in` the scope of an element
296
376
  ```ts
297
- dom.in(element).require("li")
377
+ dom(element).require("li")
298
378
  // HTMLElement (or throws)
299
379
  ```
300
380
  ```ts
301
- dom.in(element).maybe("li")
381
+ dom(element).maybe("li")
302
382
  // HTMLElement | undefined
303
383
  ```
304
384
  ```ts
305
- dom.in(element).all("li")
385
+ dom(element).all("li")
306
386
  // HTMLElement[]
307
387
  ```
308
388
 
309
389
  ### 🪄 dom utilities
310
- - register web components
390
+ - `register` web components
311
391
  ```ts
312
392
  dom.register({MyComponent, AnotherCoolComponent})
313
393
  // <my-component>
314
394
  // <another-cool-component>
315
395
  ```
316
- - render content into an element
396
+ - `render` content into an element
317
397
  ```ts
318
- dom.render(element, html`<p>hello world</p>`)
398
+ dom(element).render(html`<p>hello world</p>`)
319
399
  ```
320
400
  ```ts
321
- dom.in(element).render(html`<p>hello world</p>`)
401
+ dom.in(".demo").render(html`<p>hello world</p>`)
322
402
  ```
323
403
  ```ts
324
- dom.in(".demo").render(html`<p>hello world</p>`)
404
+ dom.render(element, html`<p>hello world</p>`)
325
405
  ```
326
406
 
327
407
 
@@ -455,6 +535,126 @@ import {Pod, podium, Op, makeLoader, anims} from "@e280/sly"
455
535
 
456
536
 
457
537
 
538
+ <br/><br/>
539
+
540
+ ## 🦝🪙 loot
541
+ > *drag-and-drop facilities*
542
+
543
+ ```ts
544
+ import {loot, ev, view} from "@e280/sly"
545
+ ```
546
+
547
+ ### 🪙 `loot.Drops`
548
+ > *accept the user dropping stuff like files onto the page*
549
+ - **setup drops**
550
+ ```ts
551
+ const drops = new loot.Drops({
552
+ predicate: loot.hasFiles,
553
+ acceptDrop: event => {
554
+ const files = loot.files(event)
555
+ console.log("files dropped", files)
556
+ },
557
+ })
558
+ ```
559
+ - **attach event listeners to your dropzone,** one of these ways:
560
+ - **view example**
561
+ ```ts
562
+ view(() => () => html`
563
+ <div
564
+ ?data-indicator="${drops.$indicator()}"
565
+ @dragover="${drops.dragover}"
566
+ @dragleave="${drops.dragleave}"
567
+ @drop="${drops.drop}">
568
+ my dropzone
569
+ </div>
570
+ `)
571
+ ```
572
+ - **vanilla-js whole-page example**
573
+ ```ts
574
+ // attach listeners to accept drops and stuff
575
+ ev(document.body, {
576
+ dragover: drops.dragover,
577
+ dragleave: drops.dragleave,
578
+ drop: drops.drop,
579
+ })
580
+
581
+ // update indicator attribute on body
582
+ drops.$indicator.on(indicator => {
583
+ if (indicator) document.body.setAttribute("data-indicator", "")
584
+ else document.body.removeAttribute("data-indicator")
585
+ })
586
+ ```
587
+ - **flashy css indicator for the dropzone,** so the user knows your app is eager to accept the drop
588
+ ```css
589
+ [data-indicator] {
590
+ border: 0.5em dashed cyan;
591
+ }
592
+ ```
593
+
594
+ ### 🪙 `loot.DragAndDrops`
595
+ > *setup drag-and-drops between items within your page*
596
+ - **declare types for your grabbable and hoverable things**
597
+ ```ts
598
+ // money that can be picked up and dragged
599
+ type Money = {value: number}
600
+ // dnd will call this a "draggy"
601
+
602
+ // bag that money can be dropped into
603
+ type Bag = {id: number}
604
+ // dnd will call this a "droppy"
605
+ ```
606
+ - **make your dnd**
607
+ ```ts
608
+ const dnd = new loot.DragAndDrops<Money, Bag>({
609
+ acceptDrop: (event, money, bag) => {
610
+ console.log("drop!", {money, bag})
611
+ },
612
+ })
613
+ ```
614
+ - **attach dragzone listeners**
615
+ (there can be many dragzones...)
616
+ ```ts
617
+ view(use => () => {
618
+ const money = use.once((): Money => ({value: 280}))
619
+ const dragzone = use.once(() => dnd.dragzone(() => money))
620
+
621
+ return html`
622
+ <div
623
+ draggable="${dragzone.draggable}"
624
+ @dragstart="${dragzone.dragstart}"
625
+ @dragend="${dragzone.dragend}">
626
+ money ${money.value}
627
+ </div>
628
+ `
629
+ })
630
+ ```
631
+ - **attach dropzone listeners**
632
+ (there can be many dropzones...)
633
+ ```ts
634
+ view(use => () => {
635
+ const bag = use.once((): Bag => ({id: 1}))
636
+ const dropzone = use.once(() => dnd.dropzone(() => bag))
637
+ const indicator = !!(dnd.dragging && dnd.hovering === bag)
638
+
639
+ return html`
640
+ <div
641
+ ?data-indicator="${indicator}"
642
+ @dragenter="${dropzone.dragenter}"
643
+ @dragleave="${dropzone.dragleave}"
644
+ @dragover="${dropzone.dragover}"
645
+ @drop="${dropzone.drop}">
646
+ bag ${bag.id}
647
+ </div>
648
+ `
649
+ })
650
+ ```
651
+
652
+ ### 🪙 loot helpers
653
+ - **`loot.hasFiles(event)`** — return true if `DragEvent` contains any files (useful in `predicate`)
654
+ - **`loot.files(event)`** — returns an array of files in a drop's `DragEvent` (useful in `acceptDrop`)
655
+
656
+
657
+
458
658
  <br/><br/>
459
659
 
460
660
  ## 🦝🧑‍💻 sly is by e280
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/sly",
3
- "version": "0.2.0-3",
3
+ "version": "0.2.0-5",
4
4
  "description": "web shadow views",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,9 +2,14 @@
2
2
  import {dom} from "../dom/dom.js"
3
3
  import {DemoView} from "./views/demo.js"
4
4
  import {CounterView} from "./views/counter.js"
5
+ import {IncrediElement} from "./views/incredi.js"
5
6
 
6
7
  dom.in(".demo").render(DemoView())
7
- dom.register({DemoCounter: CounterView.component(1)})
8
+
9
+ dom.register({
10
+ IncrediElement,
11
+ DemoCounter: CounterView.component(1),
12
+ })
8
13
 
9
14
  console.log("🦝 sly")
10
15
 
@@ -19,6 +19,7 @@ const styles = css`
19
19
  :host {
20
20
  display: flex;
21
21
  flex-direction: column;
22
+ align-items: center;
22
23
  gap: 1em;
23
24
  }
24
25
  `
@@ -0,0 +1,28 @@
1
+
2
+ import {css, html} from "lit"
3
+ import {nap, repeat} from "@e280/stz"
4
+
5
+ import {Use} from "../../views/use.js"
6
+ import {attributes} from "../../views/attributes.js"
7
+ import {BaseElement} from "../../views/base-element.js"
8
+
9
+ export class IncrediElement extends BaseElement {
10
+ static styles = css`span{color:orange}`
11
+ attrs = attributes(this, {value: Number})
12
+ something = {whatever: "rofl"}
13
+
14
+ render(use: Use) {
15
+ const {value = 1} = this.attrs
16
+ const $count = use.signal(0)
17
+
18
+ use.mount(() => repeat(async() => {
19
+ await nap(10)
20
+ await $count($count() + 1)
21
+ }))
22
+
23
+ return html`
24
+ <span>${$count() * value}</span>
25
+ `
26
+ }
27
+ }
28
+
package/s/dom/dom.ts CHANGED
@@ -4,20 +4,29 @@ import {register} from "./register.js"
4
4
  import {Content} from "../views/types.js"
5
5
  import {Queryable, Renderable} from "./types.js"
6
6
 
7
+ function require<E extends Element>(
8
+ container: Queryable,
9
+ selector: string,
10
+ ) {
11
+ const e = container.querySelector<E>(selector)
12
+ if (!e) throw new Error(`element not found (${selector})`)
13
+ return e
14
+ }
15
+
7
16
  export class Dom<C extends Queryable> {
8
17
  constructor(public element: C) {}
9
18
 
10
- in<E extends HTMLElement>(elementOrSelector: E | string) {
11
- return new Dom(
12
- typeof elementOrSelector === "string"
13
- ? this.require<E>(elementOrSelector)
14
- : elementOrSelector
19
+ in<E extends HTMLElement>(selectorOrElement: string | E) {
20
+ return new Dom<E>(
21
+ (typeof selectorOrElement === "string")
22
+ ? require(this.element, selectorOrElement) as E
23
+ : selectorOrElement
15
24
  )
16
25
  }
17
26
 
18
27
  require<E extends Element = HTMLElement>(selector: string) {
19
28
  const e = this.element.querySelector<E>(selector)
20
- if (!e) throw new Error(`$1 ${selector} not found`)
29
+ if (!e) throw new Error(`element not found (${selector})`)
21
30
  return e
22
31
  }
23
32
 
@@ -34,18 +43,21 @@ export class Dom<C extends Queryable> {
34
43
  }
35
44
  }
36
45
 
37
- export function dom(selector: string) {
38
- return new Dom(document).require(selector)
46
+ export function dom<E extends Queryable>(selector: string): E
47
+ export function dom<E extends Queryable>(element: E): Dom<E>
48
+ export function dom<E extends Queryable>(selectorOrElement: string | E): E | Dom<E> {
49
+ return (typeof selectorOrElement === "string")
50
+ ? require(document, selectorOrElement) as E
51
+ : new Dom(selectorOrElement)
39
52
  }
40
53
 
41
54
  const doc = new Dom(document)
42
- dom.register = register
43
- dom.render = (container: Renderable, ...content: Content[]) => {
44
- return render(content, container)
45
- }
46
-
47
55
  dom.in = doc.in.bind(doc)
48
56
  dom.require = doc.require.bind(doc)
49
57
  dom.maybe = doc.maybe.bind(doc)
50
58
  dom.all = doc.all.bind(doc)
59
+ dom.register = register
60
+ dom.render = (container: Renderable, ...content: Content[]) => {
61
+ return render(content, container)
62
+ }
51
63
 
package/s/index.html.ts CHANGED
@@ -29,6 +29,7 @@ export default ssg.page(import.meta.url, async orb => ({
29
29
  <h1>sly testing page</h1>
30
30
  <p><a href="https://github.com/e280/sly">github.com/e280/sly</a></p>
31
31
  <p class=lil>v${orb.packageVersion()}</p>
32
+ <incredi-element></incredi-element>
32
33
  <demo-counter>component</demo-counter>
33
34
  <div class=demo></div>
34
35
  `,
package/s/index.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./ops/types.js"
14
14
  export * as loot from "./loot/index.js"
15
15
 
16
16
  export * from "./views/attributes.js"
17
+ export * from "./views/base-element.js"
17
18
  export * from "./views/css-reset.js"
18
19
  export * from "./views/types.js"
19
20
  export * from "./views/use.js"
@@ -0,0 +1,82 @@
1
+
2
+ import {signal} from "@e280/strata"
3
+ import {Drops} from "./drops.js"
4
+ import {outsideCurrentTarget} from "./helpers.js"
5
+
6
+ /** respond to user dragging-and-dropping things around on a webpage */
7
+ export class DragAndDrops<Draggy, Droppy> {
8
+
9
+ /** what is currently being dragged */
10
+ $draggy = signal<Draggy | undefined>(undefined)
11
+
12
+ /** what dropzone are we curently hovering over */
13
+ $droppy = signal<Droppy | undefined>(undefined)
14
+
15
+ constructor(private params: {
16
+
17
+ /** accept a dropped item that was declared within this system */
18
+ acceptDrop: (event: DragEvent, draggy: Draggy, droppy: Droppy) => void
19
+
20
+ /** also accept drops on the side */
21
+ backchannelDrops?: Drops
22
+ }) {}
23
+
24
+ get dragging() {
25
+ return this.$draggy()
26
+ }
27
+
28
+ get hovering() {
29
+ return this.$droppy()
30
+ }
31
+
32
+ /** make event listeners to attach to your dragzone(s) */
33
+ dragzone = (getDraggy: () => Draggy) => ({
34
+ draggable: "true",
35
+
36
+ dragstart: (_: DragEvent) => {
37
+ this.$draggy.value = getDraggy()
38
+ },
39
+
40
+ dragend: (_: DragEvent) => {
41
+ this.$draggy.value = undefined
42
+ this.$droppy.value = undefined
43
+ },
44
+ })
45
+
46
+ /** make event listeners to attach to your dropzones(s) */
47
+ dropzone = (getDroppy: () => Droppy) => ({
48
+ dragenter: (_: DragEvent) => {},
49
+
50
+ dragover: (event: DragEvent) => {
51
+ event.preventDefault()
52
+ if (this.$draggy())
53
+ this.$droppy.value = getDroppy()
54
+ else
55
+ this.params.backchannelDrops?.dragover(event)
56
+ },
57
+
58
+ dragleave: (event: DragEvent) => {
59
+ if (outsideCurrentTarget(event))
60
+ this.$droppy.value = undefined
61
+ this.params.backchannelDrops?.dragleave(event)
62
+ },
63
+
64
+ drop: (event: DragEvent) => {
65
+ event.preventDefault()
66
+ const {acceptDrop} = this.params
67
+ const draggy = this.$draggy()
68
+ const droppy = this.$droppy()
69
+ try {
70
+ if (draggy && droppy)
71
+ acceptDrop(event, draggy, droppy)
72
+ else
73
+ this.params.backchannelDrops?.drop(event)
74
+ }
75
+ finally {
76
+ this.$draggy.value = undefined
77
+ this.$droppy.value = undefined
78
+ }
79
+ },
80
+ })
81
+ }
82
+
@@ -1,10 +1,10 @@
1
1
 
2
2
  import {signal} from "@e280/strata"
3
- import {dragIsOutsideCurrentTarget} from "./helpers.js"
3
+ import {outsideCurrentTarget} from "./helpers.js"
4
4
 
5
- /** dropzone that accepts dropped stuff like files */
6
- export class Drop {
7
- #$indicator = signal(false)
5
+ /** dropzone that accepts user-dropped stuff like files */
6
+ export class Drops {
7
+ $indicator = signal(false)
8
8
 
9
9
  constructor(private params: {
10
10
 
@@ -15,28 +15,19 @@ export class Drop {
15
15
  acceptDrop: (event: DragEvent) => void
16
16
  }) {}
17
17
 
18
- get indicator() {
19
- return this.#$indicator.value
20
- }
21
-
22
- resetIndicator = () => {
23
- this.#$indicator.value = false
24
- }
25
-
26
18
  dragover = (event: DragEvent) => {
27
19
  event.preventDefault()
28
- if (this.params.predicate(event))
29
- this.#$indicator.value = true
20
+ this.$indicator.value = this.params.predicate(event)
30
21
  }
31
22
 
32
23
  dragleave = (event: DragEvent) => {
33
- if (dragIsOutsideCurrentTarget(event))
34
- this.#$indicator.value = false
24
+ if (outsideCurrentTarget(event))
25
+ this.$indicator.value = false
35
26
  }
36
27
 
37
28
  drop = (event: DragEvent) => {
38
29
  event.preventDefault()
39
- this.#$indicator.value = false
30
+ this.$indicator.value = false
40
31
  if (this.params.predicate(event))
41
32
  this.params.acceptDrop(event)
42
33
  }
package/s/loot/helpers.ts CHANGED
@@ -1,18 +1,18 @@
1
1
 
2
- export function dragHasFiles(event: DragEvent) {
2
+ export function hasFiles(event: DragEvent) {
3
3
  return !!(
4
4
  event.dataTransfer &&
5
5
  event.dataTransfer.types.includes("Files")
6
6
  )
7
7
  }
8
8
 
9
- export function droppedFiles(event: DragEvent) {
9
+ export function files(event: DragEvent) {
10
10
  return event.dataTransfer
11
11
  ? Array.from(event.dataTransfer.files)
12
12
  : []
13
13
  }
14
14
 
15
- export function dragIsOutsideCurrentTarget(event: DragEvent) {
15
+ export function outsideCurrentTarget(event: DragEvent) {
16
16
  const isCursorOutsideViewport = !event.relatedTarget || (
17
17
  event.clientX === 0 &&
18
18
  event.clientY === 0
package/s/loot/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- export * from "./drag-drop.js"
3
- export * from "./drop.js"
2
+ export * from "./drag-and-drops.js"
3
+ export * from "./drops.js"
4
4
  export * from "./helpers.js"
5
5
 
@@ -26,6 +26,12 @@ export type AttrTypes<A extends AttrSpec> = {
26
26
  [P in keyof A]: AttrType<A[P]>
27
27
  }
28
28
 
29
+ export function onAttrChange(element: HTMLElement, fn: () => void) {
30
+ const observer = new MutationObserver(fn)
31
+ observer.observe(element, {attributes: true})
32
+ return () => observer.disconnect()
33
+ }
34
+
29
35
  export const attributes = <A extends AttrSpec>(
30
36
  element: HTMLElement,
31
37
  spec: A,
@@ -81,9 +87,3 @@ export const attributes = <A extends AttrSpec>(
81
87
 
82
88
  }) as any as AttrTypes<A>
83
89
 
84
- export function onAttrChange(element: HTMLElement, fn: () => void) {
85
- const observer = new MutationObserver(fn)
86
- observer.observe(element, {attributes: true})
87
- return () => observer.disconnect()
88
- }
89
-