@e280/sly 0.2.4 → 0.3.0-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.
Files changed (256) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +302 -614
  3. package/package.json +6 -8
  4. package/s/_archive/README.md +1221 -0
  5. package/s/{base → _archive/base}/element.ts +5 -2
  6. package/s/_archive/view/index.ts +7 -0
  7. package/s/_archive/view/types.ts +45 -0
  8. package/s/{view → _archive/view}/utils/parts/capsule.ts +9 -2
  9. package/s/demo/demo.bundle.ts +2 -9
  10. package/s/demo/views/counter-light.ts +13 -0
  11. package/s/demo/views/counter-shadow.ts +16 -0
  12. package/s/demo/views/demo.ts +24 -18
  13. package/s/demo/views/loaders.ts +7 -7
  14. package/s/index.html.ts +30 -33
  15. package/s/index.ts +0 -2
  16. package/s/loaders/make.ts +1 -1
  17. package/s/loaders/parts/ascii-anim.ts +6 -8
  18. package/s/loaders/parts/error-display.ts +9 -9
  19. package/s/tests.test.ts +1 -4
  20. package/s/view/common/css-reset.ts +19 -0
  21. package/s/view/hooks/plumbing/hooks.ts +28 -0
  22. package/s/view/hooks/plumbing/hookscope.ts +12 -0
  23. package/s/view/hooks/use-css.ts +14 -0
  24. package/s/view/hooks/use-cx.ts +41 -0
  25. package/s/view/hooks/use-life.ts +17 -0
  26. package/s/view/hooks/use-mount.ts +30 -0
  27. package/s/view/hooks/use-name.ts +10 -0
  28. package/s/view/hooks/use-once.ts +9 -0
  29. package/s/view/hooks/use-op.ts +12 -0
  30. package/s/view/hooks/use-ref.ts +11 -0
  31. package/s/view/hooks/use-signal.ts +16 -0
  32. package/s/view/hooks/use-state.ts +20 -0
  33. package/s/view/hooks/use-wake.ts +8 -0
  34. package/s/view/index.ts +17 -4
  35. package/s/view/light.ts +50 -0
  36. package/s/view/parts/apply-attrs.ts +22 -0
  37. package/s/view/parts/apply-styles.ts +21 -0
  38. package/s/view/parts/cx.ts +26 -0
  39. package/s/view/parts/reactivity.ts +22 -0
  40. package/s/view/parts/sly-shadow.ts +8 -0
  41. package/s/view/shadow.ts +93 -0
  42. package/s/view/types.ts +15 -34
  43. package/x/demo/demo.bundle.js +2 -8
  44. package/x/demo/demo.bundle.js.map +1 -1
  45. package/x/demo/demo.bundle.min.js +45 -58
  46. package/x/demo/demo.bundle.min.js.map +4 -4
  47. package/x/demo/views/counter-light.d.ts +1 -0
  48. package/x/demo/views/counter-light.js +10 -0
  49. package/x/demo/views/counter-light.js.map +1 -0
  50. package/x/demo/views/counter-shadow.d.ts +1 -0
  51. package/x/demo/views/counter-shadow.js +12 -0
  52. package/x/demo/views/counter-shadow.js.map +1 -0
  53. package/x/demo/views/demo.d.ts +1 -4
  54. package/x/demo/views/demo.js +23 -18
  55. package/x/demo/views/demo.js.map +1 -1
  56. package/x/demo/views/loaders.d.ts +1 -1
  57. package/x/demo/views/loaders.js +7 -7
  58. package/x/demo/views/loaders.js.map +1 -1
  59. package/x/index.d.ts +0 -2
  60. package/x/index.html +30 -140
  61. package/x/index.html.js +31 -31
  62. package/x/index.html.js.map +1 -1
  63. package/x/index.js +0 -2
  64. package/x/index.js.map +1 -1
  65. package/x/loaders/make.d.ts +1 -1
  66. package/x/loaders/parts/ascii-anim.d.ts +1 -1
  67. package/x/loaders/parts/ascii-anim.js +6 -7
  68. package/x/loaders/parts/ascii-anim.js.map +1 -1
  69. package/x/loaders/parts/error-display.d.ts +1 -1
  70. package/x/loaders/parts/error-display.js +9 -9
  71. package/x/loaders/parts/error-display.js.map +1 -1
  72. package/x/tests.test.js +1 -4
  73. package/x/tests.test.js.map +1 -1
  74. package/x/view/common/css-reset.js +17 -0
  75. package/x/view/common/css-reset.js.map +1 -0
  76. package/x/view/hooks/plumbing/hooks.d.ts +11 -0
  77. package/x/view/hooks/plumbing/hooks.js +26 -0
  78. package/x/view/hooks/plumbing/hooks.js.map +1 -0
  79. package/x/view/hooks/plumbing/hookscope.d.ts +10 -0
  80. package/x/view/hooks/plumbing/hookscope.js +12 -0
  81. package/x/view/hooks/plumbing/hookscope.js.map +1 -0
  82. package/x/view/hooks/use-css.d.ts +4 -0
  83. package/x/view/hooks/use-css.js +10 -0
  84. package/x/view/hooks/use-css.js.map +1 -0
  85. package/x/view/hooks/use-cx.d.ts +10 -0
  86. package/x/view/hooks/use-cx.js +33 -0
  87. package/x/view/hooks/use-cx.js.map +1 -0
  88. package/x/view/hooks/use-life.d.ts +2 -0
  89. package/x/view/hooks/use-life.js +13 -0
  90. package/x/view/hooks/use-life.js.map +1 -0
  91. package/x/{base/utils/mounts.d.ts → view/hooks/use-mount.d.ts} +1 -0
  92. package/x/{base/utils/mounts.js → view/hooks/use-mount.js} +7 -1
  93. package/x/view/hooks/use-mount.js.map +1 -0
  94. package/x/view/hooks/use-name.d.ts +2 -0
  95. package/x/view/hooks/use-name.js +8 -0
  96. package/x/view/hooks/use-name.js.map +1 -0
  97. package/x/view/hooks/use-once.d.ts +2 -0
  98. package/x/view/hooks/use-once.js +7 -0
  99. package/x/view/hooks/use-once.js.map +1 -0
  100. package/x/view/hooks/use-op.d.ts +3 -0
  101. package/x/view/hooks/use-op.js +9 -0
  102. package/x/view/hooks/use-op.js.map +1 -0
  103. package/x/view/hooks/use-ref.d.ts +5 -0
  104. package/x/view/hooks/use-ref.js +11 -0
  105. package/x/view/hooks/use-ref.js.map +1 -0
  106. package/x/view/hooks/use-signal.d.ts +3 -0
  107. package/x/view/hooks/use-signal.js +12 -0
  108. package/x/view/hooks/use-signal.js.map +1 -0
  109. package/x/view/hooks/use-state.d.ts +1 -0
  110. package/x/view/hooks/use-state.js +17 -0
  111. package/x/view/hooks/use-state.js.map +1 -0
  112. package/x/view/hooks/use-wake.d.ts +2 -0
  113. package/x/view/hooks/use-wake.js +6 -0
  114. package/x/view/hooks/use-wake.js.map +1 -0
  115. package/x/view/index.d.ts +15 -4
  116. package/x/view/index.js +15 -4
  117. package/x/view/index.js.map +1 -1
  118. package/x/view/light.d.ts +2 -0
  119. package/x/view/light.js +41 -0
  120. package/x/view/light.js.map +1 -0
  121. package/x/view/parts/apply-attrs.d.ts +2 -0
  122. package/x/view/parts/apply-attrs.js +22 -0
  123. package/x/view/parts/apply-attrs.js.map +1 -0
  124. package/x/{base/utils → view/parts}/apply-styles.js.map +1 -1
  125. package/x/view/parts/cx.d.ts +12 -0
  126. package/x/view/parts/cx.js +24 -0
  127. package/x/view/parts/cx.js.map +1 -0
  128. package/x/view/parts/reactivity.d.ts +5 -0
  129. package/x/view/parts/reactivity.js +18 -0
  130. package/x/view/parts/reactivity.js.map +1 -0
  131. package/x/view/parts/sly-shadow.d.ts +3 -0
  132. package/x/view/parts/sly-shadow.js +7 -0
  133. package/x/view/parts/sly-shadow.js.map +1 -0
  134. package/x/view/shadow.d.ts +6 -0
  135. package/x/view/shadow.js +72 -0
  136. package/x/view/shadow.js.map +1 -0
  137. package/x/view/types.d.ts +13 -21
  138. package/s/demo/views/counter.ts +0 -50
  139. package/s/demo/views/fastcount.ts +0 -29
  140. package/x/base/css-reset.js +0 -19
  141. package/x/base/css-reset.js.map +0 -1
  142. package/x/base/element.d.ts +0 -19
  143. package/x/base/element.js +0 -52
  144. package/x/base/element.js.map +0 -1
  145. package/x/base/index.d.ts +0 -5
  146. package/x/base/index.js +0 -6
  147. package/x/base/index.js.map +0 -1
  148. package/x/base/types.d.ts +0 -3
  149. package/x/base/types.js +0 -3
  150. package/x/base/types.js.map +0 -1
  151. package/x/base/use.d.ts +0 -59
  152. package/x/base/use.js +0 -129
  153. package/x/base/use.js.map +0 -1
  154. package/x/base/utils/attr-watcher.d.ts +0 -8
  155. package/x/base/utils/attr-watcher.js +0 -20
  156. package/x/base/utils/attr-watcher.js.map +0 -1
  157. package/x/base/utils/mounts.js.map +0 -1
  158. package/x/base/utils/reactor.d.ts +0 -5
  159. package/x/base/utils/reactor.js +0 -25
  160. package/x/base/utils/reactor.js.map +0 -1
  161. package/x/base/utils/states.d.ts +0 -13
  162. package/x/base/utils/states.js +0 -41
  163. package/x/base/utils/states.js.map +0 -1
  164. package/x/base/utils/use-attrs.d.ts +0 -11
  165. package/x/base/utils/use-attrs.js +0 -18
  166. package/x/base/utils/use-attrs.js.map +0 -1
  167. package/x/demo/views/counter.d.ts +0 -374
  168. package/x/demo/views/counter.js +0 -42
  169. package/x/demo/views/counter.js.map +0 -1
  170. package/x/demo/views/fastcount.d.ts +0 -12
  171. package/x/demo/views/fastcount.js +0 -21
  172. package/x/demo/views/fastcount.js.map +0 -1
  173. package/x/spa/index.barrel.d.ts +0 -4
  174. package/x/spa/index.barrel.js +0 -3
  175. package/x/spa/index.barrel.js.map +0 -1
  176. package/x/spa/index.d.ts +0 -2
  177. package/x/spa/index.js +0 -2
  178. package/x/spa/index.js.map +0 -1
  179. package/x/spa/plumbing/braces.d.ts +0 -12
  180. package/x/spa/plumbing/braces.js +0 -55
  181. package/x/spa/plumbing/braces.js.map +0 -1
  182. package/x/spa/plumbing/primitives.d.ts +0 -22
  183. package/x/spa/plumbing/primitives.js +0 -65
  184. package/x/spa/plumbing/primitives.js.map +0 -1
  185. package/x/spa/plumbing/router-core.d.ts +0 -13
  186. package/x/spa/plumbing/router-core.js +0 -38
  187. package/x/spa/plumbing/router-core.js.map +0 -1
  188. package/x/spa/plumbing/types.d.ts +0 -35
  189. package/x/spa/plumbing/types.js +0 -2
  190. package/x/spa/plumbing/types.js.map +0 -1
  191. package/x/spa/router.d.ts +0 -13
  192. package/x/spa/router.js +0 -39
  193. package/x/spa/router.js.map +0 -1
  194. package/x/spa/spa.test.d.ts +0 -15
  195. package/x/spa/spa.test.js +0 -78
  196. package/x/spa/spa.test.js.map +0 -1
  197. package/x/view/utils/contextualize.d.ts +0 -13
  198. package/x/view/utils/contextualize.js +0 -18
  199. package/x/view/utils/contextualize.js.map +0 -1
  200. package/x/view/utils/make-component.d.ts +0 -5
  201. package/x/view/utils/make-component.js +0 -17
  202. package/x/view/utils/make-component.js.map +0 -1
  203. package/x/view/utils/make-view.d.ts +0 -2
  204. package/x/view/utils/make-view.js +0 -32
  205. package/x/view/utils/make-view.js.map +0 -1
  206. package/x/view/utils/parts/capsule.d.ts +0 -12
  207. package/x/view/utils/parts/capsule.js +0 -50
  208. package/x/view/utils/parts/capsule.js.map +0 -1
  209. package/x/view/utils/parts/chain.d.ts +0 -13
  210. package/x/view/utils/parts/chain.js +0 -26
  211. package/x/view/utils/parts/chain.js.map +0 -1
  212. package/x/view/utils/parts/context.d.ts +0 -9
  213. package/x/view/utils/parts/context.js +0 -10
  214. package/x/view/utils/parts/context.js.map +0 -1
  215. package/x/view/utils/parts/directive.d.ts +0 -5
  216. package/x/view/utils/parts/directive.js +0 -20
  217. package/x/view/utils/parts/directive.js.map +0 -1
  218. package/x/view/utils/parts/naked.d.ts +0 -18
  219. package/x/view/utils/parts/naked.js +0 -57
  220. package/x/view/utils/parts/naked.js.map +0 -1
  221. package/x/view/utils/parts/sly-view.d.ts +0 -6
  222. package/x/view/utils/parts/sly-view.js +0 -16
  223. package/x/view/utils/parts/sly-view.js.map +0 -1
  224. package/x/view/view.d.ts +0 -11
  225. package/x/view/view.js +0 -15
  226. package/x/view/view.js.map +0 -1
  227. /package/s/{base → _archive/base}/css-reset.ts +0 -0
  228. /package/s/{base → _archive/base}/index.ts +0 -0
  229. /package/s/{base → _archive/base}/types.ts +0 -0
  230. /package/s/{base → _archive/base}/use.ts +0 -0
  231. /package/s/{base → _archive/base}/utils/apply-styles.ts +0 -0
  232. /package/s/{base → _archive/base}/utils/attr-watcher.ts +0 -0
  233. /package/s/{base → _archive/base}/utils/mounts.ts +0 -0
  234. /package/s/{base → _archive/base}/utils/reactor.ts +0 -0
  235. /package/s/{base → _archive/base}/utils/states.ts +0 -0
  236. /package/s/{base → _archive/base}/utils/use-attrs.ts +0 -0
  237. /package/s/{spa → _archive/spa}/index.barrel.ts +0 -0
  238. /package/s/{spa → _archive/spa}/index.ts +0 -0
  239. /package/s/{spa → _archive/spa}/plumbing/braces.ts +0 -0
  240. /package/s/{spa → _archive/spa}/plumbing/primitives.ts +0 -0
  241. /package/s/{spa → _archive/spa}/plumbing/router-core.ts +0 -0
  242. /package/s/{spa → _archive/spa}/plumbing/types.ts +0 -0
  243. /package/s/{spa → _archive/spa}/router.ts +0 -0
  244. /package/s/{spa → _archive/spa}/spa.test.ts +0 -0
  245. /package/s/{view → _archive/view}/utils/contextualize.ts +0 -0
  246. /package/s/{view → _archive/view}/utils/make-component.ts +0 -0
  247. /package/s/{view → _archive/view}/utils/make-view.ts +0 -0
  248. /package/s/{view → _archive/view}/utils/parts/chain.ts +0 -0
  249. /package/s/{view → _archive/view}/utils/parts/context.ts +0 -0
  250. /package/s/{view → _archive/view}/utils/parts/directive.ts +0 -0
  251. /package/s/{view → _archive/view}/utils/parts/naked.ts +0 -0
  252. /package/s/{view → _archive/view}/utils/parts/sly-view.ts +0 -0
  253. /package/s/{view → _archive/view}/view.ts +0 -0
  254. /package/x/{base → view/common}/css-reset.d.ts +0 -0
  255. /package/x/{base/utils → view/parts}/apply-styles.d.ts +0 -0
  256. /package/x/{base/utils → view/parts}/apply-styles.js +0 -0
@@ -0,0 +1,1221 @@
1
+
2
+ <div align="center"><img alt="" width="256" src="./assets/favicon.png"/></div>
3
+
4
+ # 🦝 sly
5
+ > *mischievous shadow views*
6
+
7
+ [@e280](https://e280.org/)'s new [lit](https://lit.dev/)-based frontend webdev library.
8
+
9
+ - 🍋 [**#views**](#views) — hooks-based, shadow-dom'd, template-literal'd
10
+ - 🪵 [**#base-element**](#base-element) — for a more classical experience
11
+ - 🪄 [**#dom**](#dom) — the "it's not jquery" multitool
12
+ - 🫛 [**#ops**](#ops) — reactive tooling for async operations
13
+ - ⏳ [**#loaders**](#loaders) — animated loading spinners for rendering ops
14
+ - 💅 [**#spa**](#spa) — hash routing for your spa-day
15
+ - 🪙 [**#loot**](#loot) — drag-and-drop facilities
16
+ - 🧪 https://sly.e280.org/ — our testing page
17
+ - **✨[shiny](https://shiny.e280.org/)✨** — our wip component library
18
+
19
+
20
+
21
+ <br/><br/>
22
+
23
+ ## 🦝 sly and friends
24
+ > `@e280/sly`
25
+
26
+ ```sh
27
+ npm install @e280/sly lit @e280/strata @e280/stz
28
+ ```
29
+
30
+ > [!NOTE]
31
+ > - 🔥 [lit](https://lit.dev/), for html rendering
32
+ > - ⛏️ [@e280/strata](https://github.com/e280/strata), for state management (signals, state trees)
33
+ > - 🏂 [@e280/stz](https://github.com/e280/stz), our ts standard library
34
+ > - 🐢 [@e280/scute](https://github.com/e280/scute), our buildy-bundly-buddy
35
+
36
+ > [!TIP]
37
+ > you can import everything in sly from `@e280/sly`,
38
+ > or from specific subpackages like `@e280/sly/view`, `@e280/sly/dom`, etc...
39
+
40
+
41
+
42
+ <br/><br/>
43
+ <a id="views"></a>
44
+
45
+ ## 🍋🦝 sly views
46
+ > *modern views, in lightness, or darkness...*
47
+
48
+ - 🪶 **no compile step** — just god's honest javascript, via [lit](https://lit.dev/)-html tagged-template-literals
49
+ - 🪝 **hooks-based** — declarative rendering with [modern hooks](#hooks) familiar to react devs
50
+ - ⚡ **reactive** — views auto-rerender whenever any [strata](https://github.com/e280/strata)-compatible state changes
51
+
52
+ ```ts
53
+ import {html} from "lit"
54
+ import {light, shadow, dom} from "@e280/sly"
55
+
56
+ export const MyLightView = light(() => html`<p>blinded by the light</p>`)
57
+
58
+ export const MyShadowView = shadow(() => html`<p>shrouded in darkness</p>`)
59
+ ```
60
+
61
+ ### 🍋 light views
62
+ > *just pretend it's react*
63
+
64
+ - **define a light view**
65
+ ```ts
66
+ import {html} from "lit"
67
+ import {light, useSignal} from "@e280/sly"
68
+
69
+ export const MyCounter = light((start: number) => {
70
+ const $count = useSignal(start)
71
+ const increment = () => $count.value++
72
+
73
+ return html`
74
+ <button @click="${increment}">${$count.value}</button>
75
+ `
76
+ })
77
+ ```
78
+ - **render it into the dom**
79
+ ```ts
80
+ dom.in(".demo").render(html`
81
+ <h1>my cool counter demo</h1>
82
+ ${MyCounter(123)}
83
+ `)
84
+ ```
85
+
86
+ ### 🍋 shadow views
87
+ > *each shadow view gets its own cozy [shadow-dom](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) bubble and supports [slots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)*
88
+
89
+ - **define a shadow view**
90
+ ```ts
91
+ import {css, html} from "lit"
92
+ import {shadow, useName, useCss, useSignal} from "@e280/sly"
93
+
94
+ export const MyShadowCounter = shadow((start: number) => {
95
+ useName("shadow-counter")
96
+ useCss(css`button { color: cyan }`)
97
+
98
+ const $count = useSignal(start)
99
+ const increment = () => $count.value++
100
+
101
+ return html`
102
+ <button @click="${increment}">${$count()}</button>
103
+ <slot></slot>
104
+ `
105
+ })
106
+ ```
107
+ - **render it into the dom**
108
+ ```ts
109
+ dom.in(".demo").render(html`
110
+ <h1>my cool counter demo</h1>
111
+ ${MyShadowCounter(234)}
112
+ `)
113
+ ```
114
+ - **.with to nest children or set attrs**
115
+ ```ts
116
+ dom.in(".demo").render(html`
117
+ <h1>my cool counter demo</h1>
118
+
119
+ ${MyShadowCounter.with({
120
+ props: [234],
121
+ attrs: {"data-whatever": 555},
122
+ children: html`
123
+ <p>woah, slotting support!</p>
124
+ `,
125
+ })}
126
+ `)
127
+ ```
128
+ - **oh, you can do custom shadow config if needed**
129
+ ```ts
130
+ const MyCustomShadow = shadow.config(() => {
131
+ const host = document.createElement("div")
132
+ const shadow = host.attachShadow({mode: "open"})
133
+ return {host, shadow}
134
+ })(() => html`<p>shrouded in darkness</p>`)
135
+ ```
136
+
137
+ <a id="hooks"></a>
138
+
139
+ ### 🍋 hooks reference
140
+
141
+ #### 👮 follow the hooks rules
142
+ > just like [react hooks](https://react.dev/warnings/invalid-hook-call-warning), the execution order of sly's `use` hooks actually matters..
143
+ > you must not call these hooks under `if` conditionals, or `for` loops, or in callbacks, or after a conditional `return` statement, or anything like that.. *otherwise, heed my warning: weird bad stuff will happen..*
144
+
145
+ #### 🌚 shadow-only hooks
146
+ - **useName** — *(shadow only)* — set the "data-view" attr value
147
+ ```ts
148
+ useName("squarepants")
149
+ // <div data-view="squarepants">
150
+ ```
151
+ - **useCss** — *(shadow only)* — attach stylesheets (use lit's `css`!) to the shadow root
152
+ ```ts
153
+ useCss(css1, css2, css3)
154
+ ```
155
+ - **useHost** — *(shadow only)* — get the host element
156
+ ```ts
157
+ const host = useHost()
158
+ ```
159
+ - **useShadow** — *(shadow only)* — get the shadow root
160
+ ```ts
161
+ const shadow = useShadow()
162
+ ```
163
+
164
+ #### 🌞 universal hooks
165
+ - **useState** — react-like hook to create some reactive state (we prefer signals)
166
+ ```ts
167
+ const [count, setCount] = useState(0)
168
+
169
+ const increment = () => setCount(n => n + 1)
170
+ ```
171
+ - **useRef** — react-like hook to make a non-reactive box for a value
172
+ ```ts
173
+ const ref = useRef(0)
174
+
175
+ ref.current // 0
176
+ ref.current = 1 // does not trigger rerender
177
+ ```
178
+ - **useSignal** — create a [strata](https://github.com/e280/strata) signal
179
+ ```ts
180
+ const $count = useSignal(1)
181
+
182
+ // read the signal
183
+ $count()
184
+
185
+ // write the signal
186
+ $count(2)
187
+ ```
188
+ - see [strata readme](https://github.com/e280/strata)
189
+ - **useDerived** — create a [strata](https://github.com/e280/strata) derived signal
190
+ ```ts
191
+ const $product = useDerived(() => $count() * $whatever())
192
+ ```
193
+ - see [strata readme](https://github.com/e280/strata)
194
+ - **useOnce** — run fn at initialization, and return a value
195
+ ```ts
196
+ const whatever = use.once(() => {
197
+ console.log("happens one time")
198
+ return 123
199
+ })
200
+
201
+ whatever // 123
202
+ ```
203
+ - **useMount** — setup mount/unmount lifecycle
204
+ ```ts
205
+ useMount(() => {
206
+ console.log("mounted")
207
+ return () => console.log("unmounted")
208
+ })
209
+ ```
210
+ - **useWake** — run fn each time mounted, and return value
211
+ ```ts
212
+ const whatever = use.wake(() => {
213
+ console.log("mounted")
214
+ return 123
215
+ })
216
+
217
+ whatever // 123
218
+ ```
219
+ - **useLife** — mount/unmount lifecycle, but also return a value
220
+ ```ts
221
+ const whatever = use.life(() => {
222
+ console.log("mounted")
223
+ const value = 123
224
+ return [value, () => console.log("unmounted")]
225
+ })
226
+
227
+ whatever // 123
228
+ ```
229
+ - **useRender** — returns a fn to rerender the view (debounced)
230
+ ```ts
231
+ const render = useRender()
232
+
233
+ render().then(() => console.log("render done"))
234
+ ```
235
+ - **useRendered** — get a promise that resolves *after* the next render
236
+ ```ts
237
+ useRendered().then(() => console.log("rendered"))
238
+ ```
239
+ - **useOp** — start loading an op based on an async fn
240
+ ```ts
241
+ const op = useOp(async() => {
242
+ await nap(5000)
243
+ return 123
244
+ })
245
+ ```
246
+ - **useOpPromise** — start loading an op based on a promise
247
+ ```ts
248
+ const op = use.op.promise(doAsyncWork())
249
+ ```
250
+
251
+ ### 🍋 happy hooks recipes
252
+ - make a ticker — mount, cycle, and nap
253
+ ```ts
254
+ import {cycle, nap} from "@e280/stz"
255
+ ```
256
+ ```ts
257
+ const $seconds = useSignal(0)
258
+
259
+ useMount(() => cycle(async() => {
260
+ await nap(1000)
261
+ $seconds.value++
262
+ }))
263
+ ```
264
+ - wake + rendered, to do something after each mount's first render
265
+ ```ts
266
+ useWake(() => useRendered.then(() => {
267
+ console.log("after first render")
268
+ }))
269
+ ```
270
+
271
+
272
+
273
+ <br/><br/>
274
+ <a id="legacy-views"></a>
275
+
276
+ ## 🍋🦝 LEGACY sly views
277
+ > `@e280/sly/view`
278
+ > *the crown jewel of sly*
279
+
280
+ ```ts
281
+ view(use => () => html`<p>hello world</p>`)
282
+ ```
283
+
284
+ - 🪶 **no compile step** — just god's honest javascript, via [lit](https://lit.dev/)-html tagged-template-literals
285
+ - 🥷 **shadow dom'd** — each view gets its own cozy [shadow](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) bubble, and supports [slots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
286
+ - 🪝 **hooks-based** — declarative rendering with the [`use`](#use) family of ergonomic hooks
287
+ - ⚡ **reactive** — they auto-rerender whenever any [strata](https://github.com/e280/strata)-compatible state changes
288
+ - 🧐 **not components, per se** — they're comfy typescript-native ui building blocks [(technically, lit directives)](https://lit.dev/docs/templates/custom-directives/)
289
+ - 🧩 **componentizable** — any view can be magically converted into a proper [web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)
290
+
291
+ ### 🍋 view example
292
+ ```ts
293
+ import {view, dom, BaseElement} from "@e280/sly"
294
+ import {html, css} from "lit"
295
+ ```
296
+ - **declare view**
297
+ ```ts
298
+ export const CounterView = view(use => (start: number) => {
299
+ use.styles(css`p {color: green}`)
300
+
301
+ const $count = use.signal(start)
302
+ const increment = () => $count.value++
303
+
304
+ return html`
305
+ <button @click="${increment}">
306
+ ${$count.value}
307
+ </button>
308
+ `
309
+ })
310
+ ```
311
+ - `$count` is a [strata signal](https://github.com/e280/strata#readme) *(we like those)*
312
+ - **inject view into dom**
313
+ ```ts
314
+ dom.in(".app").render(html`
315
+ <h1>cool counter demo</h1>
316
+ ${CounterView(1)}
317
+ `)
318
+ ```
319
+ - 🤯 **register view as web component**
320
+ ```ts
321
+ dom.register({
322
+ MyCounter: CounterView
323
+ .component()
324
+ .props(() => [1]),
325
+ })
326
+ ```
327
+ ```html
328
+ <my-counter></my-counter>
329
+ ```
330
+
331
+ ### 🍋 view settings
332
+ - optional settings for views you should know about
333
+ ```ts
334
+ export const CoolView = view
335
+ .settings({mode: "open", delegatesFocus: true})
336
+ .render(use => (greeting: string) => html`😎 ${greeting} <slot></slot>`)
337
+ ```
338
+ - all [attachShadow params](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters) (like `mode` and `delegatesFocus`) are valid `settings`
339
+ - note the `<slot></slot>` we'll use in the next example lol
340
+
341
+ ### 🍋 view chains
342
+ - views have this sick chaining syntax for supplying more stuff at the template injection site
343
+ ```ts
344
+ dom.in(".app").render(html`
345
+ <h2>cool example</h2>
346
+ ${CoolView
347
+ .props("hello")
348
+ .attr("class", "hero")
349
+ .children(html`<em>spongebob</em>`)
350
+ .render()}
351
+ `)
352
+ ```
353
+ - `props` — provide props and start a view chain
354
+ - `attr` — set html attributes on the `<sly-view>` host element
355
+ - `children` — add nested [slottable](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots) content
356
+ - `render` — end the view chain and render the lit directive
357
+
358
+ ### 🍋 view/component universality
359
+ - **you can start with a view,**
360
+ ```ts
361
+ export const GreeterView = view(use => (name: string) => {
362
+ return html`<p>hello ${name}</p>`
363
+ })
364
+ ```
365
+ - view usage
366
+ ```ts
367
+ GreeterView("pimsley")
368
+ ```
369
+ **then you can convert it to a component.**
370
+ ```ts
371
+ export class GreeterComponent extends (
372
+ GreeterView
373
+ .component()
374
+ .props(component => [component.getAttribute("name") ?? "unknown"])
375
+ ) {}
376
+ ```
377
+ - html usage
378
+ ```html
379
+ <greeter-component name="pimsley"></greeter-component>
380
+ ```
381
+ - **you can start with a component,**
382
+ ```ts
383
+ export class GreeterComponent extends (
384
+ view(use => (name: string) => {
385
+ return html`<p>hello ${name}</p>`
386
+ })
387
+ .component()
388
+ .props(component => [component.getAttribute("name") ?? "unknown"])
389
+ ) {}
390
+ ```
391
+ - html usage
392
+ ```html
393
+ <greeter-component name="pimsley"></greeter-component>
394
+ ```
395
+ **and it already has `.view` ready for you.**
396
+ - view usage
397
+ ```ts
398
+ GreeterComponent.view("pimsley")
399
+ ```
400
+ - **understanding `.component(BaseElement)` and `.props(fn)`**
401
+ - `.props` takes a fn that is called every render, which returns the props given to the view
402
+ ```ts
403
+ .props(() => ["pimsley"])
404
+ ```
405
+ the props fn receives the component instance, so you can query html attributes or instance properties
406
+ ```ts
407
+ .props(component => [component.getAttribute("name") ?? "unknown"])
408
+ ```
409
+ - `.component` accepts a subclass of `BaseElement`, so you can define your own properties and methods for your component class
410
+ ```ts
411
+ const GreeterComponent = GreeterView
412
+
413
+ // declare your own custom class
414
+ .component(class extends BaseElement {
415
+ $name = signal("jim raynor")
416
+ updateName(name: string) {
417
+ this.$name.value = name
418
+ }
419
+ })
420
+
421
+ // props gets the right types on 'component'
422
+ .props(component => [component.$name.value])
423
+ ```
424
+ - `.component` provides the devs interacting with your component, with noice typings
425
+ ```ts
426
+ dom<GreeterComponent>("greeter-component").updateName("mortimer")
427
+ ```
428
+ - typescript class wizardry
429
+ - ❌ smol-brain approach exports class value, but NOT the typings
430
+ ```ts
431
+ export const GreeterComponent = (...)
432
+ ```
433
+ - ✅ giga-brain approach exports class value AND the typings
434
+ ```ts
435
+ export class GreeterComponent extends (...) {}
436
+ ```
437
+ - **register web components to the dom**
438
+ ```ts
439
+ dom.register({GreeterComponent})
440
+ ```
441
+ - **oh and don't miss out on the insta-component shorthand**
442
+ ```ts
443
+ dom.register({
444
+ QuickComponent: view.component(use => html`⚡ incredi`),
445
+ })
446
+ ```
447
+
448
+ <a id="use"></a>
449
+
450
+ ### 🍋 "use" hooks reference
451
+ - 👮 **follow the hooks rules**
452
+ > just like [react hooks](https://react.dev/warnings/invalid-hook-call-warning), the execution order of sly's `use` hooks actually matters..
453
+ > you must not call these hooks under `if` conditionals, or `for` loops, or in callbacks, or after a conditional `return` statement, or anything like that.. *otherwise, heed my warning: weird bad stuff will happen..*
454
+ - **use.name** — set the "view" attr value, eg `<sly-view view="squarepants">`
455
+ ```ts
456
+ use.name("squarepants")
457
+ ```
458
+ - **use.styles** — attach stylesheets into the view's shadow dom
459
+ ```ts
460
+ use.styles(css1, css2, css3)
461
+ ```
462
+ *(alias `use.css`)*
463
+ - **use.signal** — create a [strata signal](https://github.com/e280/strata)
464
+ ```ts
465
+ const $count = use.signal(1)
466
+
467
+ // read the signal
468
+ $count()
469
+
470
+ // write the signal
471
+ $count(2)
472
+ ```
473
+ - `derived` signals
474
+ ```ts
475
+ const $product = use.derived(() => $count() * $whatever())
476
+ ```
477
+ - `lazy` signals
478
+ ```ts
479
+ const $product = use.lazy(() => $count() * $whatever())
480
+ ```
481
+ - go read the [strata readme](https://github.com/e280/strata) about this stuff
482
+ - **use.once** — run fn at initialization, and return a value
483
+ ```ts
484
+ const whatever = use.once(() => {
485
+ console.log("happens only once")
486
+ return 123
487
+ })
488
+
489
+ whatever // 123
490
+ ```
491
+ - **use.mount** — setup mount/unmount lifecycle
492
+ ```ts
493
+ use.mount(() => {
494
+ console.log("view mounted")
495
+
496
+ return () => {
497
+ console.log("view unmounted")
498
+ }
499
+ })
500
+ ```
501
+ - **use.wake** — run fn each time mounted, and return value
502
+ ```ts
503
+ const whatever = use.wake(() => {
504
+ console.log("view mounted")
505
+ return 123
506
+ })
507
+
508
+ whatever // 123
509
+ ```
510
+ - **use.life** — mount/unmount lifecycle, but also return a value
511
+ ```ts
512
+ const v = use.life(() => {
513
+ console.log("mounted")
514
+ const value = 123
515
+ return [value, () => console.log("unmounted")]
516
+ })
517
+
518
+ v // 123
519
+ ```
520
+ - **use.events** — attach event listeners to the element (auto-cleaned up)
521
+ ```ts
522
+ use.events({
523
+ keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
524
+ keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
525
+ })
526
+ ```
527
+ - **use.states** — [internal states](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states) helper
528
+ ```ts
529
+ const states = use.states()
530
+ states.assign("active", "cool")
531
+ ```
532
+ ```css
533
+ [view="my-view"]::state(active) { color: yellow; }
534
+ [view="my-view"]::state(cool) { outline: 1px solid cyan; }
535
+ ```
536
+ - **use.attrs** — ergonomic typed html attribute access
537
+ - `use.attrs` is similar to [#dom.attrs](#dom.attrs)
538
+ ```ts
539
+ const attrs = use.attrs({
540
+ name: String,
541
+ count: Number,
542
+ active: Boolean,
543
+ })
544
+ ```
545
+ ```ts
546
+ attrs.name // "chase"
547
+ attrs.count // 123
548
+ attrs.active // true
549
+ ```
550
+ - use.attrs.{strings/numbers/booleans}
551
+ ```ts
552
+ use.attrs.strings.name // "chase"
553
+ use.attrs.numbers.count // 123
554
+ use.attrs.booleans.active // true
555
+ ```
556
+ - use.attrs.on
557
+ ```ts
558
+ use.attrs.on(() => console.log("an attribute changed"))
559
+ ```
560
+ - **use.render** — rerender the view (debounced)
561
+ ```ts
562
+ use.render()
563
+ ```
564
+ - **use.renderNow** — rerender the view instantly (not debounced)
565
+ ```ts
566
+ use.renderNow()
567
+ ```
568
+ - **use.rendered** — promise that resolves *after* the next render
569
+ ```ts
570
+ use.rendered.then(() => {
571
+ const slot = use.shadow.querySelector("slot")
572
+ console.log(slot)
573
+ })
574
+ ```
575
+ - **use.op** — start with an op based on an async fn
576
+ ```ts
577
+ const op = use.op(async() => {
578
+ await nap(5000)
579
+ return 123
580
+ })
581
+ ```
582
+ - **use.op.promise** — start with an op based on a promise
583
+ ```ts
584
+ const op = use.op.promise(doAsyncWork())
585
+ ```
586
+
587
+ ### 🍋 "use" recipes
588
+ - make a ticker — mount, cycle, and nap
589
+ ```ts
590
+ import {cycle, nap} from "@e280/stz"
591
+ ```
592
+ ```ts
593
+ const $seconds = use.signal(0)
594
+
595
+ use.mount(() => cycle(async() => {
596
+ await nap(1000)
597
+ $seconds.value++
598
+ }))
599
+ ```
600
+ - wake + rendered, to do something after each mount's first render
601
+ ```ts
602
+ use.wake(() => use.rendered.then(() => {
603
+ console.log("after first render")
604
+ }))
605
+ ```
606
+
607
+
608
+
609
+ <br/><br/>
610
+ <a id="base-element"></a>
611
+
612
+ ## 🪵🦝 sly base element
613
+ > `@e280/sly/base`
614
+ > *the classic experience*
615
+
616
+ ```ts
617
+ import {BaseElement, Use, dom} from "@e280/sly"
618
+ import {html, css} from "lit"
619
+ ```
620
+
621
+ `BaseElement` is more of an old-timey class-based "boomer" approach to making web components, but with a millennial twist — its `render` method gives you the same `use` hooks that views enjoy.
622
+
623
+ 👮 a *BaseElement* is not a *View*, and cannot be converted into a *View*.
624
+
625
+ ### 🪵 let's clarify some sly terminology
626
+ - "Element"
627
+ - an html element; any subclass of the browser's HTMLElement
628
+ - all genuine ["web components"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) are elements
629
+ - "BaseElement"
630
+ - sly's own subclass of the browser-native HTMLElement
631
+ - is a true element and web component (can be registered to the dom)
632
+ - "View"
633
+ - sly's own magic concept that uses a lit-directive to render stuff
634
+ - NOT an element or web component (can NOT be registered to the dom)
635
+ - NOT related to BaseElement
636
+ - can be converted into a Component via `view.component().props(() => [])`
637
+ - "Component"
638
+ - a sly view that has been converted into an element
639
+ - is a true element and web component (can be registered to the dom)
640
+ - actually a subclass of BaseElement
641
+ - actually contains the view on `Component.view`
642
+
643
+ ### 🪵 base element setup
644
+ - **declare your element class**
645
+ ```ts
646
+ export class MyElement extends BaseElement {
647
+ static styles = css`span{color:orange}`
648
+
649
+ // custom property
650
+ $start = signal(10)
651
+
652
+ // custom attributes
653
+ attrs = dom.attrs(this).spec({
654
+ multiply: Number,
655
+ })
656
+
657
+ // custom methods
658
+ hello() {
659
+ return "world"
660
+ }
661
+
662
+ render(use: Use) {
663
+ const $count = use.signal(1)
664
+ const increment = () => $count.value++
665
+
666
+ const {$start} = this
667
+ const {multiply = 1} = this.attrs
668
+ const result = $start() + (multiply * $count())
669
+
670
+ return html`
671
+ <span>${result}</span>
672
+ <button @click="${increment}">+</button>
673
+ `
674
+ }
675
+ }
676
+ ```
677
+ - **register your element to the dom**
678
+ ```ts
679
+ dom.register({MyElement})
680
+ ```
681
+
682
+ ### 🪵 base element usage
683
+ - **place the element in your html body**
684
+ ```html
685
+ <body>
686
+ <my-element></my-element>
687
+ </body>
688
+ ```
689
+ - **now you can interact with it**
690
+ ```ts
691
+ const myElement = dom<MyElement>("my-element")
692
+
693
+ // js property
694
+ myElement.$start(100)
695
+
696
+ // html attributes
697
+ myElement.attrs.multiply = 2
698
+
699
+ // methods
700
+ myElement.hello()
701
+ // "world"
702
+ ```
703
+
704
+
705
+
706
+ <br/><br/>
707
+ <a id="dom"></a>
708
+
709
+ ## 🪄🦝 sly dom
710
+ > `@e280/sly/dom`
711
+ > *the "it's not jquery!" multitool*
712
+
713
+ ```ts
714
+ import {dom} from "@e280/sly"
715
+ ```
716
+
717
+ ### 🪄 dom queries
718
+ - `require` an element
719
+ ```ts
720
+ dom(".demo")
721
+ // HTMLElement (or throws)
722
+ ```
723
+ ```ts
724
+ // alias
725
+ dom.require(".demo")
726
+ // HTMLElement (or throws)
727
+ ```
728
+ - `maybe` get an element
729
+ ```ts
730
+ dom.maybe(".demo")
731
+ // HTMLElement | undefined
732
+ ```
733
+ - `all` matching elements in an array
734
+ ```ts
735
+ dom.all(".demo ul li")
736
+ // HTMLElement[]
737
+ ```
738
+
739
+ ### 🪄 dom.in scope
740
+ - make a scope
741
+ ```ts
742
+ dom.in(".demo") // selector
743
+ // Dom instance
744
+ ```
745
+ ```ts
746
+ dom.in(demoElement) // element
747
+ // Dom instance
748
+ ```
749
+ - run queries in that scope
750
+ ```ts
751
+ dom.in(demoElement).require(".button")
752
+ ```
753
+ ```ts
754
+ dom.in(demoElement).maybe(".button")
755
+ ```
756
+ ```ts
757
+ dom.in(demoElement).all("ol li")
758
+ ```
759
+
760
+ ### 🪄 dom utilities
761
+ - `dom.register` web components
762
+ ```ts
763
+ dom.register({MyComponent, AnotherCoolComponent})
764
+ // <my-component>
765
+ // <another-cool-component>
766
+ ```
767
+ - `dom.register` automatically dashes the tag names (`MyComponent` becomes `<my-component>`)
768
+ - `dom.render` content into an element
769
+ ```ts
770
+ dom.render(element, html`<p>hello world</p>`)
771
+ ```
772
+ ```ts
773
+ dom.in(".demo").render(html`<p>hello world</p>`)
774
+ ```
775
+ - `dom.el` little element builder
776
+ ```ts
777
+ const div = dom.el("div", {"data-whatever": 123, "data-active": true})
778
+ // <div data-whatever="123" data-active></div>
779
+ ```
780
+ - `dom.elmer` make an element with a fluent chain
781
+ ```ts
782
+ const div = dom.elmer("div")
783
+ .attr("data-whatever", 123)
784
+ .attr("data-active")
785
+ .children("hello world")
786
+ .done()
787
+ // HTMLElement
788
+ ```
789
+ - `dom.mk` make an element with a lit template (returns the first)
790
+ ```ts
791
+ const div = dom.mk(html`
792
+ <div data-whatever="123" data-active>
793
+ hello world
794
+ </div>
795
+ `) // HTMLElement
796
+ ```
797
+ - `dom.events` <a id="dom.events"></a> to attach event listeners
798
+ ```ts
799
+ const detach = dom.events(element, {
800
+ keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
801
+ keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
802
+ })
803
+ ```
804
+ ```ts
805
+ const detach = dom.in(".demo").events({
806
+ keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
807
+ keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
808
+ })
809
+ ```
810
+ ```ts
811
+ // unattach those event listeners when you're done
812
+ detach()
813
+ ```
814
+ - `dom.attrs` <a id="dom.attrs"></a> to setup a type-happy html attribute helper
815
+ ```ts
816
+ const attrs = dom.attrs(element).spec({
817
+ name: String,
818
+ count: Number,
819
+ active: Boolean,
820
+ })
821
+ ```
822
+ ```ts
823
+ const attrs = dom.in(".demo").attrs.spec({
824
+ name: String,
825
+ count: Number,
826
+ active: Boolean,
827
+ })
828
+ ```
829
+ ```ts
830
+ attrs.name // "chase"
831
+ attrs.count // 123
832
+ attrs.active // true
833
+ ```
834
+ ```ts
835
+ attrs.name = "zenky"
836
+ attrs.count = 124
837
+ attrs.active = false // removes html attr
838
+ ```
839
+ ```ts
840
+ attrs.name = undefined // removes the attr
841
+ attrs.count = undefined // removes the attr
842
+ ```
843
+ or if you wanna be more loosey-goosey, skip the spec
844
+ ```ts
845
+ const a = dom.in(".demo").attrs
846
+ a.strings.name = "pimsley"
847
+ a.numbers.count = 125
848
+ a.booleans.active = true
849
+ ```
850
+
851
+
852
+
853
+ <br/><br/>
854
+ <a id="ops"></a>
855
+
856
+ ## 🫛🦝 sly ops
857
+ > `@e280/sly/ops`
858
+ > *tools for async operations and loading spinners*
859
+
860
+ ```ts
861
+ import {nap} from "@e280/stz"
862
+ import {Pod, podium, Op, loaders} from "@e280/sly"
863
+ ```
864
+
865
+ ### 🫛 pods: loading/ready/error
866
+ - a pod represents an async operation in terms of json-serializable data
867
+ - there are three kinds of `Pod<V>`
868
+ ```ts
869
+ // loading pod
870
+ ["loading"]
871
+
872
+ // ready pod contains value 123
873
+ ["ready", 123]
874
+
875
+ // error pod contains an error
876
+ ["error", new Error()]
877
+ ```
878
+
879
+ ### 🫛 podium: helps you work with pods
880
+ - get pod status
881
+ ```ts
882
+ podium.status(["ready", 123])
883
+ // "ready"
884
+ ```
885
+ - get pod ready value (or undefined)
886
+ ```ts
887
+ podium.value(["loading"])
888
+ // undefined
889
+
890
+ podium.value(["ready", 123])
891
+ // 123
892
+ ```
893
+ - see more at [podium.ts](./s/ops/podium.ts)
894
+
895
+ ### 🫛 ops: nice pod ergonomics
896
+ - an `Op<V>` wraps a pod with a signal for reactivity
897
+ - create an op
898
+ ```ts
899
+ const op = new Op<number>() // loading status by default
900
+ ```
901
+ ```ts
902
+ const op = Op.loading<number>()
903
+ ```
904
+ ```ts
905
+ const op = Op.ready<number>(123)
906
+ ```
907
+ ```ts
908
+ const op = Op.error<number>(new Error())
909
+ ```
910
+ - 🔥 create an op that calls and tracks an async fn
911
+ ```ts
912
+ const op = Op.load(async() => {
913
+ await nap(4000)
914
+ return 123
915
+ })
916
+ ```
917
+ - await for the next ready value (or thrown error)
918
+ ```ts
919
+ await op // 123
920
+ ```
921
+ - get pod info
922
+ ```ts
923
+ op.pod // ["loading"]
924
+ op.status // "loading"
925
+ op.value // undefined (or value if ready)
926
+ ```
927
+ ```ts
928
+ op.isLoading // true
929
+ op.isReady // false
930
+ op.isError // false
931
+ ```
932
+ - select executes a fn based on the status
933
+ ```ts
934
+ const result = op.select({
935
+ loading: () => "it's loading...",
936
+ ready: value => `dude, it's ready! ${value}`,
937
+ error: err => `dude, there's an error!`,
938
+ })
939
+
940
+ result
941
+ // "dude, it's ready! 123"
942
+ ```
943
+ - morph returns a new pod, transforming the value if ready
944
+ ```ts
945
+ op.morph(n => n + 1)
946
+ // ["ready", 124]
947
+ ```
948
+ - you can combine a number of ops into a single pod like this
949
+ ```ts
950
+ Op.all(Op.ready(123), Op.loading())
951
+ // ["loading"]
952
+ ```
953
+ ```ts
954
+ Op.all(Op.ready(1), Op.ready(2), Op.ready(3))
955
+ // ["ready", [1, 2, 3]]
956
+ ```
957
+ - error if any ops are in error, otherwise
958
+ - loading if any ops are in loading, otherwise
959
+ - ready if all the ops are ready
960
+
961
+
962
+
963
+ <br/><br/>
964
+ <a id="loaders"></a>
965
+
966
+ ## ⏳🦝 sly loaders
967
+ > `@e280/sly/loaders`
968
+ > *animated loading spinners for ops*
969
+
970
+ ```ts
971
+ import {loaders} from "@e280/sly"
972
+ ```
973
+
974
+ ### ⏳ make a loader, choose an anim
975
+ - create a loader fn
976
+ ```ts
977
+ const loader = loaders.make(loaders.anims.dots)
978
+ ```
979
+ - see all the anims available on the testing page https://sly.e280.org/
980
+ - ngl, i made too many.. *i was having fun, okay?*
981
+
982
+ ### ⏳ render an op with it
983
+ - use your loader to render an op
984
+ ```ts
985
+ return html`
986
+ <h2>cool stuff</h2>
987
+
988
+ ${loader(op, value => html`
989
+ <div>${value}</div>
990
+ `)}
991
+ `
992
+ ```
993
+ - when the op is loading, the loading spinner will animate
994
+ - when the op is in error, the error will be displayed
995
+ - when the op is ready, your fn is called and given the value
996
+
997
+
998
+
999
+ <br/><br/>
1000
+ <a id="spa"></a>
1001
+
1002
+ ## 💅🦝 sly spa
1003
+ > `@e280/sly/spa`
1004
+ > *hash router for single-page-apps*
1005
+
1006
+ ```ts
1007
+ import {spa, html} from "@e280/sly"
1008
+ ```
1009
+
1010
+ ### 💅 spa.Router basics
1011
+ - **make a spa router**
1012
+ ```ts
1013
+ const router = new spa.Router({
1014
+ routes: {
1015
+ home: spa.route("#/", async() => html`home`),
1016
+ settings: spa.route("#/settings", async() => html`settings`),
1017
+ user: spa.route("#/user/{userId}", async({userId}) => html`user ${userId}`),
1018
+ },
1019
+ })
1020
+ ```
1021
+ - all route strings must start with `#/`
1022
+ - use braces like `{userId}` to accept string params
1023
+ - home-equivalent hashes like `""` and `"#"` are normalized to `"#/"`
1024
+ - the router has an effect on the appearance of the url in the browser address bar -- the home `#/` is removed, aesthetically, eg, `e280.org/#/` is rewritten to `e280.org` using *history.replaceState*
1025
+ - you can provide `loader` option if you want to specify the loading spinner (defaults to `loaders.make()`)
1026
+ - you can provide `notFound` option, if you want to specify what is shown on invalid routes (defaults to `() => null`)
1027
+ - when `auto` is true (default), the router calls `.refresh()` and `.listen()` in the constructor.. set it to `false` if you want manual control
1028
+ - you can set `auto` option false if you want to omit the default initial refresh and listen calls
1029
+ - **render your current page**
1030
+ ```ts
1031
+ return html`
1032
+ <div class="my-page">
1033
+ ${router.render()}
1034
+ </div>
1035
+ `
1036
+ ```
1037
+ - returns lit content
1038
+ - shows a loading spinner when pages are loading
1039
+ - will display the notFound content for invalid routes (defaults to null)
1040
+ - **perform navigations**
1041
+ - go to settings page
1042
+ ```ts
1043
+ await router.nav.settings.go()
1044
+ // goes to "#/settings"
1045
+ ```
1046
+ - go to user page
1047
+ ```ts
1048
+ await router.nav.user.go("123")
1049
+ // goes to "#/user/123"
1050
+ ```
1051
+
1052
+ ### 💅 spa.Router advanced
1053
+ - **generate a route's hash string**
1054
+ ```ts
1055
+ const hash = router.nav.user.hash("123")
1056
+ // "#/user/123"
1057
+
1058
+ html`<a href="${hash}">user 123</a>`
1059
+ ```
1060
+ - **check if a route is the currently-active one**
1061
+ ```ts
1062
+ const hash = router.nav.user.active
1063
+ // true
1064
+ ```
1065
+ - **force-refresh the router**
1066
+ ```ts
1067
+ await router.refresh()
1068
+ ```
1069
+ - **force-navigate the router by hash**
1070
+ ```ts
1071
+ await router.refresh("#/user/123")
1072
+ ```
1073
+ - **get the current hash string (normalized)**
1074
+ ```ts
1075
+ router.hash
1076
+ // "#/user/123"
1077
+ ```
1078
+ - **the `route(...)` helper fn enables the braces-params syntax**
1079
+ - but, if you wanna do it differently, you *can* implement your own hash parser to do your own funky syntax
1080
+ - **dispose the router when you're done with it**
1081
+ ```ts
1082
+ router.dispose()
1083
+ // stop listening to hashchange events
1084
+ ```
1085
+
1086
+
1087
+
1088
+ <br/><br/>
1089
+ <a id="loot"></a>
1090
+
1091
+ ## 🪙🦝 loot
1092
+ > `@e280/sly/loot`
1093
+ > *drag-and-drop facilities*
1094
+
1095
+ ```ts
1096
+ import {loot, view, dom} from "@e280/sly"
1097
+ import {ev} from "@e280/stz"
1098
+ ```
1099
+
1100
+ ### 🪙 `loot.Drops`
1101
+ > *accept the user dropping stuff like files onto the page*
1102
+ - **setup drops**
1103
+ ```ts
1104
+ const drops = new loot.Drops({
1105
+ predicate: loot.hasFiles,
1106
+ acceptDrop: event => {
1107
+ const files = loot.files(event)
1108
+ console.log("files dropped", files)
1109
+ },
1110
+ })
1111
+ ```
1112
+ - **attach event listeners to your dropzone,** one of these ways:
1113
+ - **view example**
1114
+ ```ts
1115
+ view(() => () => html`
1116
+ <div
1117
+ ?data-indicator="${drops.$indicator()}"
1118
+ @dragover="${drops.dragover}"
1119
+ @dragleave="${drops.dragleave}"
1120
+ @drop="${drops.drop}">
1121
+ my dropzone
1122
+ </div>
1123
+ `)
1124
+ ```
1125
+ - **vanilla-js whole-page example**
1126
+ ```ts
1127
+ // attach listeners to the body
1128
+ ev(document.body, {
1129
+ dragover: drops.dragover,
1130
+ dragleave: drops.dragleave,
1131
+ drop: drops.drop,
1132
+ })
1133
+
1134
+ // sly attribute handler for the body
1135
+ const attrs = dom.attrs(document.body).spec({
1136
+ "data-indicator": Boolean,
1137
+ })
1138
+
1139
+ // sync the data-indicator attribute
1140
+ drops.$indicator.on(bool => attrs["data-indicator"] = bool)
1141
+ ```
1142
+ - **flashy css indicator for the dropzone,** so the user knows your app is eager to accept the drop
1143
+ ```css
1144
+ [data-indicator] {
1145
+ border: 0.5em dashed cyan;
1146
+ }
1147
+ ```
1148
+
1149
+ ### 🪙 `loot.DragAndDrops`
1150
+ > *setup drag-and-drops between items within your page*
1151
+ - **declare types for your draggy and droppy things**
1152
+ ```ts
1153
+ // money that can be picked up and dragged
1154
+ type Money = {value: number}
1155
+ // dnd will call this a "draggy"
1156
+
1157
+ // bag that money can be dropped into
1158
+ type Bag = {id: number}
1159
+ // dnd will call this a "droppy"
1160
+ ```
1161
+ - **make your dnd**
1162
+ ```ts
1163
+ const dnd = new loot.DragAndDrops<Money, Bag>({
1164
+ acceptDrop: (event, money, bag) => {
1165
+ console.log("drop!", {money, bag})
1166
+ },
1167
+ })
1168
+ ```
1169
+ - **attach dragzone listeners** (there can be many dragzones...)
1170
+ ```ts
1171
+ view(use => () => {
1172
+ const money = use.once((): Money => ({value: 280}))
1173
+ const dragzone = use.once(() => dnd.dragzone(() => money))
1174
+
1175
+ return html`
1176
+ <div
1177
+ draggable="${dragzone.draggable}"
1178
+ @dragstart="${dragzone.dragstart}"
1179
+ @dragend="${dragzone.dragend}">
1180
+ money ${money.value}
1181
+ </div>
1182
+ `
1183
+ })
1184
+ ```
1185
+ - **attach dropzone listeners** (there can be many dropzones...)
1186
+ ```ts
1187
+ view(use => () => {
1188
+ const bag = use.once((): Bag => ({id: 1}))
1189
+ const dropzone = use.once(() => dnd.dropzone(() => bag))
1190
+ const indicator = !!(dnd.dragging && dnd.hovering === bag)
1191
+
1192
+ return html`
1193
+ <div
1194
+ ?data-indicator="${indicator}"
1195
+ @dragenter="${dropzone.dragenter}"
1196
+ @dragleave="${dropzone.dragleave}"
1197
+ @dragover="${dropzone.dragover}"
1198
+ @drop="${dropzone.drop}">
1199
+ bag ${bag.id}
1200
+ </div>
1201
+ `
1202
+ })
1203
+ ```
1204
+
1205
+ ### 🪙 loot helpers
1206
+ - **`loot.hasFiles(event)`** — return true if `DragEvent` contains any files (useful in `predicate`)
1207
+ - **`loot.files(event)`** — returns an array of files in a drop's `DragEvent` (useful in `acceptDrop`)
1208
+
1209
+
1210
+
1211
+ <br/><br/>
1212
+ <a id="e280"></a>
1213
+
1214
+ ## 🧑‍💻🦝 sly is by e280
1215
+ reward us with github stars
1216
+ build with us at https://e280.org/ but only if you're cool
1217
+
1218
+
1219
+
1220
+ <br/><br/>
1221
+