@e280/sly 0.2.0-2 → 0.2.0-21

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 (285) hide show
  1. package/README.md +552 -98
  2. package/package.json +12 -5
  3. package/s/base/element.ts +76 -0
  4. package/s/base/index.ts +5 -0
  5. package/s/{views → base}/use.ts +17 -18
  6. package/s/base/utils/attr-watcher.ts +22 -0
  7. package/s/base/utils/reactor.ts +32 -0
  8. package/s/base/utils/use-attrs.ts +27 -0
  9. package/s/demo/demo.bundle.ts +9 -5
  10. package/s/demo/views/counter.ts +21 -24
  11. package/s/demo/views/demo.ts +10 -6
  12. package/s/demo/views/fastcount.ts +29 -0
  13. package/s/demo/views/loaders.ts +7 -7
  14. package/s/dom/attrs/attrs.ts +21 -0
  15. package/s/dom/attrs/parts/attr-fns.ts +38 -0
  16. package/s/dom/attrs/parts/attr-proxies.ts +35 -0
  17. package/s/dom/attrs/parts/attr-spec.ts +29 -0
  18. package/s/dom/attrs/parts/on-attrs.ts +8 -0
  19. package/s/dom/dom.ts +46 -16
  20. package/s/dom/index.ts +4 -0
  21. package/s/dom/parts/el.ts +14 -0
  22. package/s/dom/parts/eve.ts +24 -0
  23. package/s/dom/{register.ts → parts/register.ts} +2 -7
  24. package/s/dom/types.ts +39 -2
  25. package/s/index.html.ts +4 -2
  26. package/s/index.ts +7 -18
  27. package/s/loaders/index.barrel.ts +9 -0
  28. package/s/loaders/index.ts +3 -0
  29. package/s/loaders/make.ts +14 -0
  30. package/s/loaders/mock.ts +11 -0
  31. package/s/{ops/loaders → loaders}/parts/anims.ts +1 -1
  32. package/s/{ops/loaders → loaders}/parts/ascii-anim.ts +4 -3
  33. package/s/{ops/loaders → loaders}/parts/error-display.ts +2 -2
  34. package/s/loaders/types.ts +6 -0
  35. package/s/loot/drag-and-drops.ts +82 -0
  36. package/s/loot/{drop.ts → drops.ts} +8 -17
  37. package/s/loot/helpers.ts +3 -3
  38. package/s/loot/index.barrel.ts +5 -0
  39. package/s/loot/index.ts +1 -3
  40. package/s/ops/index.ts +5 -0
  41. package/s/ops/op.ts +3 -2
  42. package/s/spa/index.barrel.ts +6 -0
  43. package/s/spa/index.ts +3 -0
  44. package/s/spa/plumbing/braces.ts +76 -0
  45. package/s/spa/plumbing/primitives.ts +85 -0
  46. package/s/spa/plumbing/router-core.ts +49 -0
  47. package/s/spa/plumbing/types.ts +45 -0
  48. package/s/spa/router.ts +49 -0
  49. package/s/spa/spa.test.ts +91 -0
  50. package/s/tests.test.ts +4 -1
  51. package/s/view/index.ts +6 -0
  52. package/s/view/types.ts +40 -0
  53. package/s/view/utils/make-component.ts +34 -0
  54. package/s/view/utils/make-view.ts +48 -0
  55. package/s/view/utils/parts/capsule.ts +67 -0
  56. package/s/view/utils/parts/chain.ts +33 -0
  57. package/s/view/utils/parts/context.ts +10 -0
  58. package/s/view/utils/parts/directive.ts +29 -0
  59. package/s/view/utils/parts/set-attrs.ts +33 -0
  60. package/s/view/utils/parts/sly-view.ts +15 -0
  61. package/s/view/view.ts +24 -0
  62. package/x/base/css-reset.js.map +1 -0
  63. package/x/base/element.d.ts +19 -0
  64. package/x/base/element.js +52 -0
  65. package/x/base/element.js.map +1 -0
  66. package/x/base/index.d.ts +3 -0
  67. package/x/base/index.js +4 -0
  68. package/x/base/index.js.map +1 -0
  69. package/x/{views → base}/use.d.ts +5 -5
  70. package/x/{views → base}/use.js +9 -11
  71. package/x/base/use.js.map +1 -0
  72. package/x/base/utils/apply-styles.js.map +1 -0
  73. package/x/base/utils/attr-watcher.d.ts +8 -0
  74. package/x/base/utils/attr-watcher.js +20 -0
  75. package/x/base/utils/attr-watcher.js.map +1 -0
  76. package/x/base/utils/mounts.js.map +1 -0
  77. package/x/base/utils/reactor.d.ts +5 -0
  78. package/x/base/utils/reactor.js +25 -0
  79. package/x/base/utils/reactor.js.map +1 -0
  80. package/x/base/utils/use-attrs.d.ts +11 -0
  81. package/x/base/utils/use-attrs.js +19 -0
  82. package/x/base/utils/use-attrs.js.map +1 -0
  83. package/x/demo/demo.bundle.js +8 -4
  84. package/x/demo/demo.bundle.js.map +1 -1
  85. package/x/demo/demo.bundle.min.js +19 -22
  86. package/x/demo/demo.bundle.min.js.map +4 -4
  87. package/x/demo/views/counter.d.ts +374 -1
  88. package/x/demo/views/counter.js +19 -22
  89. package/x/demo/views/counter.js.map +1 -1
  90. package/x/demo/views/demo.d.ts +4 -1
  91. package/x/demo/views/demo.js +10 -5
  92. package/x/demo/views/demo.js.map +1 -1
  93. package/x/demo/views/fastcount.d.ts +12 -0
  94. package/x/demo/views/fastcount.js +21 -0
  95. package/x/demo/views/fastcount.js.map +1 -0
  96. package/x/demo/views/loaders.js +6 -6
  97. package/x/demo/views/loaders.js.map +1 -1
  98. package/x/dom/attrs/attrs.d.ts +20 -0
  99. package/x/dom/attrs/attrs.js +17 -0
  100. package/x/dom/attrs/attrs.js.map +1 -0
  101. package/x/dom/attrs/parts/attr-fns.d.ts +13 -0
  102. package/x/dom/attrs/parts/attr-fns.js +42 -0
  103. package/x/dom/attrs/parts/attr-fns.js.map +1 -0
  104. package/x/dom/attrs/parts/attr-proxies.d.ts +8 -0
  105. package/x/dom/attrs/parts/attr-proxies.js +21 -0
  106. package/x/dom/attrs/parts/attr-proxies.js.map +1 -0
  107. package/x/dom/attrs/parts/attr-spec.d.ts +3 -0
  108. package/x/dom/attrs/parts/attr-spec.js +21 -0
  109. package/x/dom/attrs/parts/attr-spec.js.map +1 -0
  110. package/x/dom/attrs/parts/on-attrs.d.ts +2 -0
  111. package/x/dom/attrs/parts/on-attrs.js +7 -0
  112. package/x/dom/attrs/parts/on-attrs.js.map +1 -0
  113. package/x/dom/dom.d.ts +22 -7
  114. package/x/dom/dom.js +32 -12
  115. package/x/dom/dom.js.map +1 -1
  116. package/x/dom/index.d.ts +2 -0
  117. package/x/dom/index.js +3 -0
  118. package/x/dom/index.js.map +1 -0
  119. package/x/dom/parts/dashify.js.map +1 -0
  120. package/x/dom/parts/el.d.ts +2 -0
  121. package/x/dom/parts/el.js +7 -0
  122. package/x/dom/parts/el.js.map +1 -0
  123. package/x/dom/parts/eve.d.ts +7 -0
  124. package/x/dom/parts/eve.js +16 -0
  125. package/x/dom/parts/eve.js.map +1 -0
  126. package/x/dom/{register.d.ts → parts/register.d.ts} +2 -6
  127. package/x/dom/parts/register.js.map +1 -0
  128. package/x/dom/types.d.ts +14 -2
  129. package/x/index.d.ts +7 -15
  130. package/x/index.html +6 -4
  131. package/x/index.html.js +4 -2
  132. package/x/index.html.js.map +1 -1
  133. package/x/index.js +7 -15
  134. package/x/index.js.map +1 -1
  135. package/x/loaders/index.barrel.d.ts +6 -0
  136. package/x/loaders/index.barrel.js +7 -0
  137. package/x/loaders/index.barrel.js.map +1 -0
  138. package/x/loaders/index.d.ts +1 -0
  139. package/x/loaders/index.js +2 -0
  140. package/x/loaders/index.js.map +1 -0
  141. package/x/loaders/make.d.ts +3 -0
  142. package/x/loaders/make.js +6 -0
  143. package/x/loaders/make.js.map +1 -0
  144. package/x/loaders/mock.d.ts +2 -0
  145. package/x/loaders/mock.js +8 -0
  146. package/x/loaders/mock.js.map +1 -0
  147. package/x/{ops/loaders → loaders}/parts/anims.d.ts +1 -1
  148. package/x/loaders/parts/anims.js.map +1 -0
  149. package/x/{ops/loaders → loaders}/parts/ascii-anim.d.ts +2 -2
  150. package/x/{ops/loaders → loaders}/parts/ascii-anim.js +2 -2
  151. package/x/loaders/parts/ascii-anim.js.map +1 -0
  152. package/x/loaders/parts/error-display.d.ts +1 -0
  153. package/x/{ops/loaders → loaders}/parts/error-display.js +2 -2
  154. package/x/loaders/parts/error-display.js.map +1 -0
  155. package/x/loaders/types.d.ts +3 -0
  156. package/x/loaders/types.js.map +1 -0
  157. package/x/loot/drag-and-drops.d.ts +30 -0
  158. package/x/loot/drag-and-drops.js +63 -0
  159. package/x/loot/drag-and-drops.js.map +1 -0
  160. package/x/loot/{drop.d.ts → drops.d.ts} +3 -5
  161. package/x/loot/drops.js +25 -0
  162. package/x/loot/drops.js.map +1 -0
  163. package/x/loot/helpers.d.ts +3 -3
  164. package/x/loot/helpers.js +3 -3
  165. package/x/loot/helpers.js.map +1 -1
  166. package/x/loot/index.barrel.d.ts +3 -0
  167. package/x/loot/index.barrel.js +4 -0
  168. package/x/loot/index.barrel.js.map +1 -0
  169. package/x/loot/index.d.ts +1 -3
  170. package/x/loot/index.js +1 -3
  171. package/x/loot/index.js.map +1 -1
  172. package/x/ops/index.d.ts +3 -0
  173. package/x/ops/index.js +4 -0
  174. package/x/ops/index.js.map +1 -0
  175. package/x/ops/op.d.ts +2 -2
  176. package/x/ops/op.js +3 -2
  177. package/x/ops/op.js.map +1 -1
  178. package/x/spa/index.barrel.d.ts +4 -0
  179. package/x/spa/index.barrel.js +3 -0
  180. package/x/spa/index.barrel.js.map +1 -0
  181. package/x/spa/index.d.ts +1 -0
  182. package/x/spa/index.js +2 -0
  183. package/x/spa/index.js.map +1 -0
  184. package/x/spa/plumbing/braces.d.ts +12 -0
  185. package/x/spa/plumbing/braces.js +55 -0
  186. package/x/spa/plumbing/braces.js.map +1 -0
  187. package/x/spa/plumbing/primitives.d.ts +22 -0
  188. package/x/spa/plumbing/primitives.js +65 -0
  189. package/x/spa/plumbing/primitives.js.map +1 -0
  190. package/x/spa/plumbing/router-core.d.ts +13 -0
  191. package/x/spa/plumbing/router-core.js +38 -0
  192. package/x/spa/plumbing/router-core.js.map +1 -0
  193. package/x/spa/plumbing/types.d.ts +35 -0
  194. package/x/spa/plumbing/types.js +2 -0
  195. package/x/spa/plumbing/types.js.map +1 -0
  196. package/x/spa/router.d.ts +16 -0
  197. package/x/spa/router.js +39 -0
  198. package/x/spa/router.js.map +1 -0
  199. package/x/spa/spa.test.d.ts +15 -0
  200. package/x/spa/spa.test.js +78 -0
  201. package/x/spa/spa.test.js.map +1 -0
  202. package/x/tests.test.js +4 -1
  203. package/x/tests.test.js.map +1 -1
  204. package/x/view/index.d.ts +4 -0
  205. package/x/view/index.js +5 -0
  206. package/x/view/index.js.map +1 -0
  207. package/x/view/types.d.ts +22 -0
  208. package/x/view/types.js +2 -0
  209. package/x/{views → view}/types.js.map +1 -1
  210. package/x/view/utils/make-component.d.ts +5 -0
  211. package/x/view/utils/make-component.js +17 -0
  212. package/x/view/utils/make-component.js.map +1 -0
  213. package/x/view/utils/make-view.d.ts +2 -0
  214. package/x/view/utils/make-view.js +24 -0
  215. package/x/view/utils/make-view.js.map +1 -0
  216. package/x/view/utils/parts/capsule.d.ts +13 -0
  217. package/x/view/utils/parts/capsule.js +49 -0
  218. package/x/view/utils/parts/capsule.js.map +1 -0
  219. package/x/view/utils/parts/chain.d.ts +11 -0
  220. package/x/view/utils/parts/chain.js +21 -0
  221. package/x/view/utils/parts/chain.js.map +1 -0
  222. package/x/view/utils/parts/context.d.ts +8 -0
  223. package/x/view/utils/parts/context.js +10 -0
  224. package/x/view/utils/parts/context.js.map +1 -0
  225. package/x/view/utils/parts/directive.d.ts +5 -0
  226. package/x/view/utils/parts/directive.js +18 -0
  227. package/x/view/utils/parts/directive.js.map +1 -0
  228. package/x/view/utils/parts/set-attrs.d.ts +3 -0
  229. package/x/view/utils/parts/set-attrs.js +21 -0
  230. package/x/view/utils/parts/set-attrs.js.map +1 -0
  231. package/x/view/utils/parts/sly-view.d.ts +5 -0
  232. package/x/view/utils/parts/sly-view.js +13 -0
  233. package/x/view/utils/parts/sly-view.js.map +1 -0
  234. package/x/view/view.d.ts +11 -0
  235. package/x/view/view.js +15 -0
  236. package/x/view/view.js.map +1 -0
  237. package/s/loot/drag-drop.ts +0 -76
  238. package/s/ops/loaders/make-loader.ts +0 -18
  239. package/s/views/attributes.ts +0 -89
  240. package/s/views/types.ts +0 -40
  241. package/s/views/utils/apply-attrs.ts +0 -33
  242. package/s/views/view.ts +0 -150
  243. package/x/dom/dashify.js.map +0 -1
  244. package/x/dom/register.js.map +0 -1
  245. package/x/loot/drag-drop.d.ts +0 -29
  246. package/x/loot/drag-drop.js +0 -54
  247. package/x/loot/drag-drop.js.map +0 -1
  248. package/x/loot/drop.js +0 -32
  249. package/x/loot/drop.js.map +0 -1
  250. package/x/ops/loaders/make-loader.d.ts +0 -5
  251. package/x/ops/loaders/make-loader.js +0 -7
  252. package/x/ops/loaders/make-loader.js.map +0 -1
  253. package/x/ops/loaders/parts/anims.js.map +0 -1
  254. package/x/ops/loaders/parts/ascii-anim.js.map +0 -1
  255. package/x/ops/loaders/parts/error-display.d.ts +0 -1
  256. package/x/ops/loaders/parts/error-display.js.map +0 -1
  257. package/x/views/attributes.d.ts +0 -10
  258. package/x/views/attributes.js +0 -46
  259. package/x/views/attributes.js.map +0 -1
  260. package/x/views/css-reset.js.map +0 -1
  261. package/x/views/types.d.ts +0 -31
  262. package/x/views/use.js.map +0 -1
  263. package/x/views/utils/apply-attrs.d.ts +0 -2
  264. package/x/views/utils/apply-attrs.js +0 -21
  265. package/x/views/utils/apply-attrs.js.map +0 -1
  266. package/x/views/utils/apply-styles.js.map +0 -1
  267. package/x/views/utils/mounts.js.map +0 -1
  268. package/x/views/view.d.ts +0 -9
  269. package/x/views/view.js +0 -116
  270. package/x/views/view.js.map +0 -1
  271. /package/s/{views → base}/css-reset.ts +0 -0
  272. /package/s/{views → base}/utils/apply-styles.ts +0 -0
  273. /package/s/{views → base}/utils/mounts.ts +0 -0
  274. /package/s/dom/{dashify.ts → parts/dashify.ts} +0 -0
  275. /package/x/{views → base}/css-reset.d.ts +0 -0
  276. /package/x/{views → base}/css-reset.js +0 -0
  277. /package/x/{views → base}/utils/apply-styles.d.ts +0 -0
  278. /package/x/{views → base}/utils/apply-styles.js +0 -0
  279. /package/x/{views → base}/utils/mounts.d.ts +0 -0
  280. /package/x/{views → base}/utils/mounts.js +0 -0
  281. /package/x/dom/{dashify.d.ts → parts/dashify.d.ts} +0 -0
  282. /package/x/dom/{dashify.js → parts/dashify.js} +0 -0
  283. /package/x/dom/{register.js → parts/register.js} +0 -0
  284. /package/x/{ops/loaders → loaders}/parts/anims.js +0 -0
  285. /package/x/{views → loaders}/types.js +0 -0
package/README.md CHANGED
@@ -4,99 +4,115 @@
4
4
  # 🦝 sly
5
5
  > *mischievous shadow views*
6
6
 
7
- [@e280](https://e280.org/)'s shiny, tasteful, incredible new [lit](https://lit.dev/)-based toolkit for frontend web developers.
8
- sly replaces its predecessor, [slate](https://github.com/benevolent-games/slate).
7
+ [@e280](https://e280.org/)'s shiny new [lit](https://lit.dev/)-based frontend webdev library. *(sly replaces its predecessor, [slate](https://github.com/benevolent-games/slate))*
9
8
 
10
- - 🍋 **views**hooks-based, shadow-dom'd, componentizable
11
- - 🪄 **dom**the "it's not jquery" multitool
12
- - 🫛 **ops**tools for async operations and loading spinners
13
- - 🧪 **testing page** https://sly.e280.org/
9
+ - 🍋 [**#views**](#views) — shadow-dom'd, hooks-based, componentizable
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
+ - 🧪 testing page — https://sly.e280.org/
14
17
 
15
18
 
16
19
 
17
20
  <br/><br/>
18
21
 
19
22
  ## 🦝 sly and friends
23
+ > `@e280/sly`
20
24
 
21
25
  ```sh
22
- npm install @e280/sly lit
26
+ npm install @e280/sly lit @e280/strata @e280/stz
23
27
  ```
24
28
 
25
29
  > [!NOTE]
26
- > - 🔥 [lit](https://lit.dev/) for html rendering
30
+ > - 🔥 [lit](https://lit.dev/), for html rendering
27
31
  > - ⛏️ [@e280/strata](https://github.com/e280/strata), for state management (signals, state trees)
28
- > - 🏂 [@e280/stz](https://github.com/e280/stz) is our ts standard library
29
- > - 🐢 [scute](https://github.com/e280/scute) is our buildy-bundly-buddy
32
+ > - 🏂 [@e280/stz](https://github.com/e280/stz), our ts standard library
33
+ > - 🐢 [@e280/scute](https://github.com/e280/scute), our buildy-bundly-buddy
34
+
35
+ > [!TIP]
36
+ > you can import everything in sly from `@e280/sly`,
37
+ > or from specific subpackages like `@e280/sly/view`, `@e280/sly/dom`, etc...
30
38
 
31
39
 
32
40
 
33
41
  <br/><br/>
42
+ <a id="views"></a>
34
43
 
35
- ## 🦝🍋 sly views
36
- > *views are the crown jewel of sly.. shadow-dom'd.. hooks-based.. "ergonomics"..*
44
+ ## 🍋🦝 sly views
45
+ > `@e280/sly/view`
46
+ > *the crown jewel of sly*
37
47
 
38
48
  ```ts
39
49
  view(use => () => html`<p>hello world</p>`)
40
50
  ```
41
51
 
42
- - views are not [web components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components), but they do have [shadow roots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) and support [slots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
43
- - any view can be registered as a web component, perfect for entrypoints or sharing widgets with html authors
44
- - views are typescript-native and comfy for webdevs building apps
45
- - views automatically rerender whenever any [strata-compatible](https://github.com/e280/strata) state changes
52
+ - 🪶 **no compile step** just god's honest javascript, via [lit](https://lit.dev/)-html tagged-template-literals
53
+ - 🥷 **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)
54
+ - 🪝 **hooks-based** declarative rendering with the [`use`](#use) family of ergonomic hooks
55
+ - **reactive** — they auto-rerender whenever any [strata](https://github.com/e280/strata)-compatible state changes
56
+ - 🧐 **not components, per se** — they're comfy typescript-native ui building blocks [(technically, lit directives)](https://lit.dev/docs/templates/custom-directives/)
57
+ - 🧩 **componentizable** — any view can be magically converted into a proper [web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)
46
58
 
47
59
  ### 🍋 view example
48
- - **import stuff**
49
- ```ts
50
- import {view, dom} from "@e280/sly"
51
- import {html, css} from "lit"
52
- ```
53
- - **declare a view**
60
+ ```ts
61
+ import {view, dom, BaseElement} from "@e280/sly"
62
+ import {html, css} from "lit"
63
+ ```
64
+ - **declare view**
54
65
  ```ts
55
66
  export const CounterView = view(use => (start: number) => {
56
- use.name("counter")
57
67
  use.styles(css`p {color: green}`)
58
68
 
59
69
  const $count = use.signal(start)
60
70
  const increment = () => $count.value++
61
71
 
62
72
  return html`
63
- <p>count ${$count.value}</p>
64
- <button @click="${increment}">+</button>
73
+ <button @click="${increment}">
74
+ ${$count.value}
75
+ </button>
65
76
  `
66
77
  })
67
78
  ```
68
- - each view renders into a `<sly-view view="counter">` host (where "counter" is the `use.name` you provided)
69
- - **inject a view into the dom**
79
+ - `$count` is a [strata signal](https://github.com/e280/strata#readme) *(we like those)*
80
+ - **inject view into dom**
70
81
  ```ts
71
82
  dom.in(".app").render(html`
72
83
  <h1>cool counter demo</h1>
73
84
  ${CounterView(1)}
74
85
  `)
75
86
  ```
76
- - 🤯 **register a view as a web component**
87
+ - 🤯 **register view as web component**
77
88
  ```ts
78
- dom.register({MyCounter: CounterView.component(1)})
79
- // <my-counter></my-counter>
89
+ dom.register({
90
+ MyCounter: CounterView
91
+ .component()
92
+ .props(() => [1]),
93
+ })
94
+ ```
95
+ ```html
96
+ <my-counter></my-counter>
80
97
  ```
81
98
 
82
- ### 🍋 view declaration settings
83
- - special settings for views at declaration-time
99
+ ### 🍋 view settings
100
+ - optional settings for views you should know about
84
101
  ```ts
85
102
  export const CoolView = view
86
103
  .settings({mode: "open", delegatesFocus: true})
87
- .declare(use => (greeting: string) => {
88
- return html`😎 ${greeting} <slot></slot>`
89
- })
104
+ .render(use => (greeting: string) => html`😎 ${greeting} <slot></slot>`)
90
105
  ```
91
106
  - all [attachShadow params](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters) (like `mode` and `delegatesFocus`) are valid `settings`
92
107
  - note the `<slot></slot>` we'll use in the next example lol
93
108
 
94
- ### 🍋 view injection options
95
- - options for views at the template injection site
109
+ ### 🍋 view chains
110
+ - views have this sick chaining syntax for supplying more stuff at the template injection site
96
111
  ```ts
97
112
  dom.in(".app").render(html`
98
113
  <h2>cool example</h2>
99
- ${CoolView.props("hello")
114
+ ${CoolView
115
+ .props("hello")
100
116
  .attr("class", "hero")
101
117
  .children(html`<em>spongebob</em>`)
102
118
  .render()}
@@ -104,30 +120,102 @@ view(use => () => html`<p>hello world</p>`)
104
120
  ```
105
121
  - `props` — provide props and start a view chain
106
122
  - `attr` — set html attributes on the `<sly-view>` host element
107
- - `children` — nested content in the host element, can be [slotted](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
123
+ - `children` — add nested [slottable](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots) content
108
124
  - `render` — end the view chain and render the lit directive
109
125
 
110
- ### 🍋 view web components
111
- - **build a component directly**
126
+ ### 🍋 view/component universality
127
+ - **you can start with a view,**
128
+ ```ts
129
+ export const GreeterView = view(use => (name: string) => {
130
+ return html`<p>hello ${name}</p>`
131
+ })
132
+ ```
133
+ - view usage
134
+ ```ts
135
+ GreeterView("pimsley")
136
+ ```
137
+ **then you can convert it to a component.**
112
138
  ```ts
113
- const MyComponent = view.component(use => html`<p>hello world</p>`)
139
+ export class GreeterComponent extends (
140
+ GreeterView
141
+ .component()
142
+ .props(component => [component.getAttribute("name") ?? "unknown"])
143
+ ) {}
114
144
  ```
115
- - notice that direct components don't take props (do `use.attrs` instead)
116
- - **convert any view into a web component**
145
+ - html usage
146
+ ```html
147
+ <greeter-component name="pimsley"></greeter-component>
148
+ ```
149
+ - **you can start with a component,**
117
150
  ```ts
118
- const MyCounter = CounterView.component(1)
151
+ export class GreeterComponent extends (
152
+ view(use => (name: string) => {
153
+ return html`<p>hello ${name}</p>`
154
+ })
155
+ .component()
156
+ .props(component => [component.getAttribute("name") ?? "unknown"])
157
+ ) {}
119
158
  ```
120
- - to convert a view to a component, you provide props
121
- - note that the component instance has a render method like `element.render(2)` which can take new props at runtime
159
+ - html usage
160
+ ```html
161
+ <greeter-component name="pimsley"></greeter-component>
162
+ ```
163
+ **and it already has `.view` ready for you.**
164
+ - view usage
165
+ ```ts
166
+ GreeterComponent.view("pimsley")
167
+ ```
168
+ - **understanding `.component(BaseElement)` and `.props(fn)`**
169
+ - `.props` takes a fn that is called every render, which returns the props given to the view
170
+ ```ts
171
+ .props(() => ["pimsley"])
172
+ ```
173
+ the props fn receives the component instance, so you can query html attributes or instance properties
174
+ ```ts
175
+ .props(component => [component.getAttribute("name") ?? "unknown"])
176
+ ```
177
+ - `.component` accepts a subclass of `BaseElement`, so you can define your own properties and methods for your component class
178
+ ```ts
179
+ const GreeterComponent = GreeterView
180
+
181
+ // declare your own custom class
182
+ .component(class extends BaseElement {
183
+ $name = signal("jim raynor")
184
+ updateName(name: string) {
185
+ this.$name.value = name
186
+ }
187
+ })
188
+
189
+ // props gets the right types on 'component'
190
+ .props(component => [component.$name.value])
191
+ ```
192
+ - `.component` provides the devs interacting with your component, with noice typings
193
+ ```ts
194
+ dom<GreeterComponent>("greeter-component").updateName("mortimer")
195
+ ```
196
+ - typescript class wizardry
197
+ - ❌ smol-brain approach exports class value, but NOT the typings
198
+ ```ts
199
+ export const GreeterComponent = (...)
200
+ ```
201
+ - ✅ giga-brain approach exports class value AND the typings
202
+ ```ts
203
+ export class GreeterComponent extends (...) {}
204
+ ```
122
205
  - **register web components to the dom**
123
206
  ```ts
124
- dom.register({MyComponent, MyCounter})
125
- // <my-component></my-component>
126
- // <my-counter></my-counter>
207
+ dom.register({GreeterComponent})
208
+ ```
209
+ - **oh and don't miss out on the insta-component shorthand**
210
+ ```ts
211
+ dom.register({
212
+ QuickComponent: view.component(use => html`⚡ incredi`),
213
+ })
127
214
  ```
128
- - `dom.register` automatically dashes the tag names (`MyComponent` becomes `<my-component>`)
129
215
 
130
- ### 🍋 view "use" hooks reference
216
+ <a id="use"></a>
217
+
218
+ ### 🍋 "use" hooks reference
131
219
  - 👮 **follow the hooks rules**
132
220
  > just like [react hooks](https://react.dev/warnings/invalid-hook-call-warning), the execution order of sly's `use` hooks actually matters..
133
221
  > 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..*
@@ -150,9 +238,9 @@ view(use => () => html`<p>hello world</p>`)
150
238
  // write the signal
151
239
  $count(2)
152
240
  ```
153
- - `derive` signals
241
+ - `derived` signals
154
242
  ```ts
155
- const $product = use.derive(() => $count() * $whatever())
243
+ const $product = use.derived(() => $count() * $whatever())
156
244
  ```
157
245
  - `lazy` signals
158
246
  ```ts
@@ -197,27 +285,31 @@ view(use => () => html`<p>hello world</p>`)
197
285
 
198
286
  v // 123
199
287
  ```
200
- - **use.attrs** — ergonomic typed html attribute access
201
- ```ts
202
- const attrs = use.attrs({
203
- name: String,
204
- count: Number,
205
- active: Boolean,
206
- })
207
- ```
208
- ```ts
209
- attrs.name // "chase"
210
- attrs.count // 123
211
- attrs.active // true
212
- ```
213
- ```ts
214
- attrs.name = "zenky"
215
- attrs.count = 124
216
- attrs.active = false // removes html attr
217
- ```
218
- ```ts
219
- attrs.name = undefined // removes the attr
220
- ```
288
+ - **use.attrs** — ergonomic typed html attribute access
289
+ - `use.attrs` is a bit different than `dom.attrs`, just to properly memoize .spec and .on for the render fn
290
+ - use.attrs.spec
291
+ ```ts
292
+ const attrs = use.attrs.spec({
293
+ name: String,
294
+ count: Number,
295
+ active: Boolean,
296
+ })
297
+ ```
298
+ ```ts
299
+ attrs.name // "chase"
300
+ attrs.count // 123
301
+ attrs.active // true
302
+ ```
303
+ - use.attrs.(strings/numbers/booleans)
304
+ ```ts
305
+ use.attrs.strings.name // "chase"
306
+ use.attrs.numbers.count // 123
307
+ use.attrs.booleans.active // true
308
+ ```
309
+ - use.attrs.on
310
+ ```ts
311
+ use.attrs.on(() => console.log("an attribute changed"))
312
+ ```
221
313
  - **use.render** — rerender the view (debounced)
222
314
  ```ts
223
315
  use.render()
@@ -245,7 +337,7 @@ view(use => () => html`<p>hello world</p>`)
245
337
  const op = use.op.promise(doAsyncWork())
246
338
  ```
247
339
 
248
- ### 🍋 view "use" recipes
340
+ ### 🍋 "use" recipes
249
341
  - make a ticker — mount, repeat, and nap
250
342
  ```ts
251
343
  import {repeat, nap} from "@e280/stz"
@@ -268,72 +360,202 @@ view(use => () => html`<p>hello world</p>`)
268
360
 
269
361
 
270
362
  <br/><br/>
363
+ <a id="base-element"></a>
364
+
365
+ ## 🪵🦝 sly base element
366
+ > `@e280/sly/base`
367
+ > *the classic experience*
368
+
369
+ ```ts
370
+ import {BaseElement, Use, dom} from "@e280/sly"
371
+ import {html, css} from "lit"
372
+ ```
373
+
374
+ `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.
375
+
376
+ 👮 a *BaseElement* is not a *View*, and cannot be converted into a *View*.
377
+
378
+ ### 🪵 let's clarify some sly terminology
379
+ - "Element"
380
+ - an html element; any subclass of the browser's HTMLElement
381
+ - all genuine ["web components"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) are elements
382
+ - "BaseElement"
383
+ - sly's own subclass of the browser-native HTMLElement
384
+ - is a true element and web component (can be registered to the dom)
385
+ - "View"
386
+ - sly's own magic concept that uses a lit-directive to render stuff
387
+ - NOT an element or web component (can NOT be registered to the dom)
388
+ - NOT related to BaseElement
389
+ - can be converted into a Component via `view.component().props(() => [])`
390
+ - "Component"
391
+ - a sly view that has been converted into an element
392
+ - is a true element and web component (can be registered to the dom)
393
+ - actually a subclass of BaseElement
394
+ - actually contains the view on `Component.view`
395
+
396
+ ### 🪵 base element setup
397
+ - **declare your element class**
398
+ ```ts
399
+ export class MyElement extends BaseElement {
400
+ static styles = css`span{color:orange}`
401
+
402
+ // custom property
403
+ $start = signal(10)
404
+
405
+ // custom attributes
406
+ attrs = dom.attrs(this).spec({
407
+ multiply: Number,
408
+ })
409
+
410
+ // custom methods
411
+ hello() {
412
+ return "world"
413
+ }
414
+
415
+ render(use: Use) {
416
+ const $count = use.signal(1)
417
+ const increment = () => $count.value++
418
+
419
+ const {$start} = this
420
+ const {multiply = 1} = this.attrs
421
+ const result = $start() + (multiply * $count())
422
+
423
+ return html`
424
+ <span>${result}</span>
425
+ <button @click="${increment}">+</button>
426
+ `
427
+ }
428
+ }
429
+ ```
430
+ - **register your element to the dom**
431
+ ```ts
432
+ dom.register({MyElement})
433
+ ```
434
+
435
+ ### 🪵 base element usage
436
+ - **place the element in your html body**
437
+ ```html
438
+ <body>
439
+ <my-element></my-element>
440
+ </body>
441
+ ```
442
+ - **now you can interact with it**
443
+ ```ts
444
+ const myElement = dom<MyElement>("my-element")
445
+
446
+ // js property
447
+ myElement.$start(100)
448
+
449
+ // html attributes
450
+ myElement.attrs.multiply = 2
451
+
452
+ // methods
453
+ myElement.hello()
454
+ // "world"
455
+ ```
456
+
457
+
458
+
459
+ <br/><br/>
460
+ <a id="dom"></a>
271
461
 
272
- ## 🦝🪄 sly dom
273
- > *the "it's not jquery!" multitool*
462
+ ## 🪄🦝 sly dom
463
+ > `@e280/sly/dom`
464
+ > *the "it's not jquery!" multitool*
274
465
 
275
466
  ```ts
276
467
  import {dom} from "@e280/sly"
277
468
  ```
278
469
 
279
470
  ### 🪄 dom queries
280
- - require an element
471
+ - `require` an element
281
472
  ```ts
282
473
  dom(".demo")
283
474
  // HTMLElement (or throws)
284
475
  ```
285
- - maybe get an element
476
+ - `maybe` get an element
286
477
  ```ts
287
478
  dom.maybe(".demo")
288
479
  // HTMLElement | undefined
289
480
  ```
290
- - select all elements
481
+ - `all` matching elements in an array
291
482
  ```ts
292
483
  dom.all(".demo ul li")
293
484
  // HTMLElement[]
294
485
  ```
295
- - within a specific container
486
+ - scoped to an element
296
487
  ```ts
297
- dom.in(element).require("li")
488
+ dom(element).require("li")
298
489
  // HTMLElement (or throws)
299
490
  ```
300
491
  ```ts
301
- dom.in(element).maybe("li")
492
+ dom(element).maybe("li")
302
493
  // HTMLElement | undefined
303
494
  ```
304
495
  ```ts
305
- dom.in(element).all("li")
496
+ dom(element).all("li")
306
497
  // HTMLElement[]
307
498
  ```
308
499
 
309
500
  ### 🪄 dom utilities
310
- - register web components
501
+ - `register` web components
311
502
  ```ts
312
503
  dom.register({MyComponent, AnotherCoolComponent})
313
504
  // <my-component>
314
505
  // <another-cool-component>
315
506
  ```
316
- - render content into an element
507
+ - `dom.register` automatically dashes the tag names (`MyComponent` becomes `<my-component>`)
508
+ - `render` content into an element
509
+ ```ts
510
+ dom(element).render(html`<p>hello world</p>`)
511
+ ```
512
+ ```ts
513
+ dom.in(".demo").render(html`<p>hello world</p>`)
514
+ ```
317
515
  ```ts
318
516
  dom.render(element, html`<p>hello world</p>`)
319
517
  ```
518
+ - `attrs` <a id="dom.attrs"></a> to setup a type-happy html attribute helper
320
519
  ```ts
321
- dom.in(element).render(html`<p>hello world</p>`)
520
+ const attrs = dom.attrs(element).spec({
521
+ name: String,
522
+ count: Number,
523
+ active: Boolean,
524
+ })
322
525
  ```
323
526
  ```ts
324
- dom.in(".demo").render(html`<p>hello world</p>`)
527
+ attrs.name // "chase"
528
+ attrs.count // 123
529
+ attrs.active // true
530
+ ```
531
+ ```ts
532
+ attrs.name = "zenky"
533
+ attrs.count = 124
534
+ attrs.active = false // removes html attr
535
+ ```
536
+ ```ts
537
+ attrs.name = undefined // removes the attr
538
+ attrs.count = undefined // removes the attr
539
+ ```
540
+ or if you wanna be more loosey-goosy, skip the spec
541
+ ```ts
542
+ dom.attrs(element).strings.name = "pimsley"
543
+ dom.attrs(element).numbers.count = 125
544
+ dom.attrs(element).booleans.active = true
325
545
  ```
326
546
 
327
547
 
328
548
 
329
549
  <br/><br/>
550
+ <a id="ops"></a>
330
551
 
331
- ## 🦝🫛 sly ops
332
- > *tools for async operations and loading spinners*
552
+ ## 🫛🦝 sly ops
553
+ > `@e280/sly/ops`
554
+ > *tools for async operations and loading spinners*
333
555
 
334
556
  ```ts
335
557
  import {nap} from "@e280/stz"
336
- import {Pod, podium, Op, makeLoader, anims} from "@e280/sly"
558
+ import {Pod, podium, Op, loaders} from "@e280/sly"
337
559
  ```
338
560
 
339
561
  ### 🫛 pods: loading/ready/error
@@ -383,7 +605,7 @@ import {Pod, podium, Op, makeLoader, anims} from "@e280/sly"
383
605
  ```
384
606
  - 🔥 create an op that calls and tracks an async fn
385
607
  ```ts
386
- const op = Op.fn(async() => {
608
+ const op = Op.load(async() => {
387
609
  await nap(4000)
388
610
  return 123
389
611
  })
@@ -432,14 +654,29 @@ import {Pod, podium, Op, makeLoader, anims} from "@e280/sly"
432
654
  - loading if any ops are in loading, otherwise
433
655
  - ready if all the ops are ready
434
656
 
435
- ### 🫛 loaders: animated loading spinners
436
- - create a `loader` using `makeLoader`
657
+
658
+
659
+ <br/><br/>
660
+ <a id="loaders"></a>
661
+
662
+ ## ⏳🦝 sly loaders
663
+ > `@e280/sly/loaders`
664
+ > *animated loading spinners for ops*
665
+
666
+ ```ts
667
+ import {loaders} from "@e280/sly"
668
+ ```
669
+
670
+ ### ⏳ make a loader, choose an anim
671
+ - create a loader fn
437
672
  ```ts
438
- const loader = makeLoader(anims.dots)
673
+ const loader = loaders.make(loaders.anims.dots)
439
674
  ```
440
675
  - see all the anims available on the testing page https://sly.e280.org/
441
676
  - ngl, i made too many.. *i was having fun, okay?*
442
- - use the loader to render your op
677
+
678
+ ### ⏳ render an op with it
679
+ - use your loader to render an op
443
680
  ```ts
444
681
  return html`
445
682
  <h2>cool stuff</h2>
@@ -456,8 +693,225 @@ import {Pod, podium, Op, makeLoader, anims} from "@e280/sly"
456
693
 
457
694
 
458
695
  <br/><br/>
696
+ <a id="spa"></a>
697
+
698
+ ## 💅🦝 sly spa
699
+ > `@e280/sly/spa`
700
+ > *hash router for single-page-apps*
701
+
702
+ ```ts
703
+ import {spa, html} from "@e280/sly"
704
+ ```
705
+
706
+ ### 💅 spa.Router basics
707
+ - **make a spa router**
708
+ ```ts
709
+ const router = new spa.Router({
710
+ routes: {
711
+ home: spa.route("#/", async() => html`home`),
712
+ settings: spa.route("#/settings", async() => html`settings`),
713
+ user: spa.route("#/user/{userId}", async({userId}) => html`user ${userId}`),
714
+ },
715
+ })
716
+ ```
717
+ - all route strings must start with `#/`
718
+ - use braces like `{userId}` to accept string params
719
+ - home-equivalent hashes like `""` and `"#"` are normalized to `"#/"`
720
+ - 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*
721
+ - you can provide `loader` option if you want to specify the loading spinner (defaults to `loaders.make()`)
722
+ - you can provide `notFound` option, if you want to specify what is shown on invalid routes (defaults to `() => null`)
723
+ - when `auto` is true (default), the router calls `.refresh()` and `.listen()` in the constructor.. set it to `false` if you want manual control
724
+ - you can set `auto` option false if you want to omit the default initial refresh and listen calls
725
+ - **render your current page**
726
+ ```ts
727
+ return html`
728
+ <div class="my-page">
729
+ ${router.render()}
730
+ </div>
731
+ `
732
+ ```
733
+ - returns lit content
734
+ - shows a loading spinner when pages are loading
735
+ - will display the notFound content for invalid routes (defaults to null)
736
+ - **perform navigations**
737
+ - go to settings page
738
+ ```ts
739
+ await router.nav.settings.go()
740
+ // goes to "#/settings"
741
+ ```
742
+ - go to user page
743
+ ```ts
744
+ await router.nav.user.go("123")
745
+ // goes to "#/user/123"
746
+ ```
747
+
748
+ ### 💅 spa.Router advanced
749
+ - **generate a route's hash string**
750
+ ```ts
751
+ const hash = router.nav.user.hash("123")
752
+ // "#/user/123"
753
+
754
+ html`<a href="${hash}">user 123</a>`
755
+ ```
756
+ - **check if a route is the currently-active one**
757
+ ```ts
758
+ const hash = router.nav.user.active
759
+ // true
760
+ ```
761
+ - **force-refresh the router**
762
+ ```ts
763
+ await router.refresh()
764
+ ```
765
+ - **force-navigate the router by hash**
766
+ ```ts
767
+ await router.refresh("#/user/123")
768
+ ```
769
+ - **get the current hash string (normalized)**
770
+ ```ts
771
+ router.hash
772
+ // "#/user/123"
773
+ ```
774
+ - **the `route(...)` helper fn enables the braces-params syntax**
775
+ - but, if you wanna do it differently, you *can* implement your own hash parser to do your own funky syntax
776
+ - **dispose the router when you're done with it**
777
+ ```ts
778
+ router.dispose()
779
+ // stop listening to hashchange events
780
+ ```
781
+
782
+
783
+
784
+ <br/><br/>
785
+ <a id="loot"></a>
786
+
787
+ ## 🪙🦝 loot
788
+ > `@e280/sly/loot`
789
+ > *drag-and-drop facilities*
790
+
791
+ ```ts
792
+ import {loot, view, dom} from "@e280/sly"
793
+ import {ev} from "@e280/stz"
794
+ ```
795
+
796
+ ### 🪙 `loot.Drops`
797
+ > *accept the user dropping stuff like files onto the page*
798
+ - **setup drops**
799
+ ```ts
800
+ const drops = new loot.Drops({
801
+ predicate: loot.hasFiles,
802
+ acceptDrop: event => {
803
+ const files = loot.files(event)
804
+ console.log("files dropped", files)
805
+ },
806
+ })
807
+ ```
808
+ - **attach event listeners to your dropzone,** one of these ways:
809
+ - **view example**
810
+ ```ts
811
+ view(() => () => html`
812
+ <div
813
+ ?data-indicator="${drops.$indicator()}"
814
+ @dragover="${drops.dragover}"
815
+ @dragleave="${drops.dragleave}"
816
+ @drop="${drops.drop}">
817
+ my dropzone
818
+ </div>
819
+ `)
820
+ ```
821
+ - **vanilla-js whole-page example**
822
+ ```ts
823
+ // attach listeners to the body
824
+ ev(document.body, {
825
+ dragover: drops.dragover,
826
+ dragleave: drops.dragleave,
827
+ drop: drops.drop,
828
+ })
829
+
830
+ // sly attribute handler for the body
831
+ const attrs = dom.attrs(document.body).spec({
832
+ "data-indicator": Boolean,
833
+ })
834
+
835
+ // sync the data-indicator attribute
836
+ drops.$indicator.on(bool => attrs["data-indicator"] = bool)
837
+ ```
838
+ - **flashy css indicator for the dropzone,** so the user knows your app is eager to accept the drop
839
+ ```css
840
+ [data-indicator] {
841
+ border: 0.5em dashed cyan;
842
+ }
843
+ ```
844
+
845
+ ### 🪙 `loot.DragAndDrops`
846
+ > *setup drag-and-drops between items within your page*
847
+ - **declare types for your draggy and droppy things**
848
+ ```ts
849
+ // money that can be picked up and dragged
850
+ type Money = {value: number}
851
+ // dnd will call this a "draggy"
852
+
853
+ // bag that money can be dropped into
854
+ type Bag = {id: number}
855
+ // dnd will call this a "droppy"
856
+ ```
857
+ - **make your dnd**
858
+ ```ts
859
+ const dnd = new loot.DragAndDrops<Money, Bag>({
860
+ acceptDrop: (event, money, bag) => {
861
+ console.log("drop!", {money, bag})
862
+ },
863
+ })
864
+ ```
865
+ - **attach dragzone listeners** (there can be many dragzones...)
866
+ ```ts
867
+ view(use => () => {
868
+ const money = use.once((): Money => ({value: 280}))
869
+ const dragzone = use.once(() => dnd.dragzone(() => money))
870
+
871
+ return html`
872
+ <div
873
+ draggable="${dragzone.draggable}"
874
+ @dragstart="${dragzone.dragstart}"
875
+ @dragend="${dragzone.dragend}">
876
+ money ${money.value}
877
+ </div>
878
+ `
879
+ })
880
+ ```
881
+ - **attach dropzone listeners** (there can be many dropzones...)
882
+ ```ts
883
+ view(use => () => {
884
+ const bag = use.once((): Bag => ({id: 1}))
885
+ const dropzone = use.once(() => dnd.dropzone(() => bag))
886
+ const indicator = !!(dnd.dragging && dnd.hovering === bag)
887
+
888
+ return html`
889
+ <div
890
+ ?data-indicator="${indicator}"
891
+ @dragenter="${dropzone.dragenter}"
892
+ @dragleave="${dropzone.dragleave}"
893
+ @dragover="${dropzone.dragover}"
894
+ @drop="${dropzone.drop}">
895
+ bag ${bag.id}
896
+ </div>
897
+ `
898
+ })
899
+ ```
900
+
901
+ ### 🪙 loot helpers
902
+ - **`loot.hasFiles(event)`** — return true if `DragEvent` contains any files (useful in `predicate`)
903
+ - **`loot.files(event)`** — returns an array of files in a drop's `DragEvent` (useful in `acceptDrop`)
904
+
905
+
906
+
907
+ <br/><br/>
908
+ <a id="e280"></a>
459
909
 
460
- ## 🦝🧑‍💻 sly is by e280
910
+ ## 🧑‍💻🦝 sly is by e280
461
911
  reward us with github stars
462
912
  build with us at https://e280.org/ but only if you're cool
463
913
 
914
+
915
+
916
+ <br/><br/>
917
+