@e280/sly 0.2.0-15 → 0.2.0-17
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.
- package/README.md +121 -12
- package/package.json +10 -4
- package/s/demo/views/loaders.ts +5 -5
- package/s/dom/dom.ts +3 -1
- package/s/dom/index.ts +4 -0
- package/s/dom/parts/el.ts +14 -0
- package/s/index.ts +6 -19
- package/s/loaders/index.barrel.ts +9 -0
- package/s/loaders/index.ts +3 -0
- package/s/{ops/loaders/make-loader.ts → loaders/make.ts} +3 -7
- package/s/loaders/mock.ts +11 -0
- package/s/{ops/loaders → loaders}/parts/anims.ts +1 -1
- package/s/{ops/loaders → loaders}/parts/ascii-anim.ts +4 -3
- package/s/{ops/loaders → loaders}/parts/error-display.ts +2 -2
- package/s/loaders/types.ts +6 -0
- package/s/loot/index.barrel.ts +5 -0
- package/s/loot/index.ts +1 -3
- package/s/ops/index.ts +5 -0
- package/s/spa/index.barrel.ts +6 -0
- package/s/spa/index.ts +3 -0
- package/s/spa/plumbing/braces.ts +76 -0
- package/s/spa/plumbing/primitives.ts +85 -0
- package/s/spa/plumbing/router-core.ts +49 -0
- package/s/spa/plumbing/types.ts +39 -0
- package/s/spa/router.ts +49 -0
- package/s/spa/spa.test.ts +91 -0
- package/s/tests.test.ts +4 -1
- package/s/ui/index.ts +9 -0
- package/s/ui/view/parts/capsule.ts +2 -2
- package/s/ui/view/parts/set-attrs.ts +33 -0
- package/x/demo/demo.bundle.min.js +18 -18
- package/x/demo/demo.bundle.min.js.map +4 -4
- package/x/demo/views/loaders.js +4 -4
- package/x/demo/views/loaders.js.map +1 -1
- package/x/dom/dom.d.ts +1 -0
- package/x/dom/dom.js +3 -1
- package/x/dom/dom.js.map +1 -1
- package/x/dom/index.d.ts +2 -0
- package/x/dom/index.js +3 -0
- package/x/dom/index.js.map +1 -0
- package/x/dom/parts/el.d.ts +2 -0
- package/x/dom/parts/el.js +7 -0
- package/x/dom/parts/el.js.map +1 -0
- package/x/index.d.ts +6 -16
- package/x/index.html +2 -2
- package/x/index.js +6 -16
- package/x/index.js.map +1 -1
- package/x/loaders/index.barrel.d.ts +6 -0
- package/x/loaders/index.barrel.js +7 -0
- package/x/loaders/index.barrel.js.map +1 -0
- package/x/loaders/index.d.ts +1 -0
- package/x/loaders/index.js +2 -0
- package/x/loaders/index.js.map +1 -0
- package/x/loaders/make.d.ts +3 -0
- package/x/loaders/make.js +6 -0
- package/x/loaders/make.js.map +1 -0
- package/x/loaders/mock.d.ts +2 -0
- package/x/loaders/mock.js +8 -0
- package/x/loaders/mock.js.map +1 -0
- package/x/{ops/loaders → loaders}/parts/anims.d.ts +1 -1
- package/x/loaders/parts/anims.js.map +1 -0
- package/x/{ops/loaders → loaders}/parts/ascii-anim.d.ts +2 -2
- package/x/{ops/loaders → loaders}/parts/ascii-anim.js +2 -2
- package/x/loaders/parts/ascii-anim.js.map +1 -0
- package/x/loaders/parts/error-display.d.ts +1 -0
- package/x/{ops/loaders → loaders}/parts/error-display.js +2 -2
- package/x/loaders/parts/error-display.js.map +1 -0
- package/x/loaders/types.d.ts +3 -0
- package/x/loaders/types.js +2 -0
- package/x/loaders/types.js.map +1 -0
- package/x/loot/index.barrel.d.ts +3 -0
- package/x/loot/index.barrel.js +4 -0
- package/x/loot/index.barrel.js.map +1 -0
- package/x/loot/index.d.ts +1 -3
- package/x/loot/index.js +1 -3
- package/x/loot/index.js.map +1 -1
- package/x/ops/index.d.ts +3 -0
- package/x/ops/index.js +4 -0
- package/x/ops/index.js.map +1 -0
- package/x/spa/index.barrel.d.ts +4 -0
- package/x/spa/index.barrel.js +3 -0
- package/x/spa/index.barrel.js.map +1 -0
- package/x/spa/index.d.ts +1 -0
- package/x/spa/index.js +2 -0
- package/x/spa/index.js.map +1 -0
- package/x/spa/plumbing/braces.d.ts +12 -0
- package/x/spa/plumbing/braces.js +55 -0
- package/x/spa/plumbing/braces.js.map +1 -0
- package/x/spa/plumbing/primitives.d.ts +22 -0
- package/x/spa/plumbing/primitives.js +65 -0
- package/x/spa/plumbing/primitives.js.map +1 -0
- package/x/spa/plumbing/router-core.d.ts +13 -0
- package/x/spa/plumbing/router-core.js +38 -0
- package/x/spa/plumbing/router-core.js.map +1 -0
- package/x/spa/plumbing/types.d.ts +34 -0
- package/x/spa/plumbing/types.js +2 -0
- package/x/spa/plumbing/types.js.map +1 -0
- package/x/spa/router.d.ts +16 -0
- package/x/spa/router.js +39 -0
- package/x/spa/router.js.map +1 -0
- package/x/spa/spa.test.d.ts +15 -0
- package/x/spa/spa.test.js +78 -0
- package/x/spa/spa.test.js.map +1 -0
- package/x/tests.test.js +4 -1
- package/x/tests.test.js.map +1 -1
- package/x/ui/index.d.ts +7 -0
- package/x/ui/index.js +8 -0
- package/x/ui/index.js.map +1 -0
- package/x/ui/view/parts/capsule.js +2 -2
- package/x/ui/view/parts/capsule.js.map +1 -1
- package/x/ui/view/parts/set-attrs.d.ts +3 -0
- package/x/ui/view/parts/set-attrs.js +21 -0
- package/x/ui/view/parts/set-attrs.js.map +1 -0
- package/s/ui/view/parts/apply-attrs.ts +0 -30
- package/x/ops/loaders/make-loader.d.ts +0 -5
- package/x/ops/loaders/make-loader.js +0 -7
- package/x/ops/loaders/make-loader.js.map +0 -1
- package/x/ops/loaders/parts/anims.js.map +0 -1
- package/x/ops/loaders/parts/ascii-anim.js.map +0 -1
- package/x/ops/loaders/parts/error-display.d.ts +0 -1
- package/x/ops/loaders/parts/error-display.js.map +0 -1
- package/x/ui/view/parts/apply-attrs.d.ts +0 -2
- package/x/ui/view/parts/apply-attrs.js +0 -19
- package/x/ui/view/parts/apply-attrs.js.map +0 -1
- /package/x/{ops/loaders → loaders}/parts/anims.js +0 -0
package/README.md
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
- 🍋 [**#views**](#views) — shadow-dom'd, hooks-based, componentizable
|
|
10
10
|
- 🪵 [**#base-element**](#base-element) — for a more classical experience
|
|
11
11
|
- 🪄 [**#dom**](#dom) — the "it's not jquery" multitool
|
|
12
|
-
- 🫛 [**#ops**](#ops) —
|
|
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
|
|
13
15
|
- 🪙 [**#loot**](#loot) — drag-and-drop facilities
|
|
14
16
|
- 🧪 testing page — https://sly.e280.org/
|
|
15
17
|
|
|
@@ -42,11 +44,11 @@ view(use => () => html`<p>hello world</p>`)
|
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
- 🪶 **no compile step** — just god's honest javascript, via [lit](https://lit.dev/)-html tagged-template-literals
|
|
45
|
-
- 🥷 **shadow dom'd** — each 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)
|
|
46
|
-
- 🪝 **hooks-based** — declarative rendering with the [`use
|
|
47
|
+
- 🥷 **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)
|
|
48
|
+
- 🪝 **hooks-based** — declarative rendering with the [`use`](#use) family of ergonomic hooks
|
|
47
49
|
- ⚡ **reactive** — they auto-rerender whenever any [strata](https://github.com/e280/strata)-compatible state changes
|
|
48
50
|
- 🧐 **not components, per se** — they're comfy typescript-native ui building blocks [(technically, lit directives)](https://lit.dev/docs/templates/custom-directives/)
|
|
49
|
-
- 🧩 **componentizable** — any view can be magically converted into a proper [web
|
|
51
|
+
- 🧩 **componentizable** — any view can be magically converted into a proper [web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)
|
|
50
52
|
|
|
51
53
|
### 🍋 view example
|
|
52
54
|
```ts
|
|
@@ -89,7 +91,7 @@ import {html, css} from "lit"
|
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
### 🍋 view settings
|
|
92
|
-
-
|
|
94
|
+
- optional settings for views you should know about
|
|
93
95
|
```ts
|
|
94
96
|
export const CoolView = view
|
|
95
97
|
.settings({mode: "open", delegatesFocus: true})
|
|
@@ -98,7 +100,7 @@ import {html, css} from "lit"
|
|
|
98
100
|
- all [attachShadow params](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters) (like `mode` and `delegatesFocus`) are valid `settings`
|
|
99
101
|
- note the `<slot></slot>` we'll use in the next example lol
|
|
100
102
|
|
|
101
|
-
### 🍋 view
|
|
103
|
+
### 🍋 view chains
|
|
102
104
|
- views have this sick chaining syntax for supplying more stuff at the template injection site
|
|
103
105
|
```ts
|
|
104
106
|
dom.in(".app").render(html`
|
|
@@ -351,7 +353,7 @@ import {BaseElement, Use, dom} from "@e280/sly"
|
|
|
351
353
|
import {html, css} from "lit"
|
|
352
354
|
```
|
|
353
355
|
|
|
354
|
-
`BaseElement` is more of an old-timey class-based "boomer" approach to making web components, but with a
|
|
356
|
+
`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.
|
|
355
357
|
|
|
356
358
|
👮 a *BaseElement* is not a *View*, and cannot be converted into a *View*.
|
|
357
359
|
|
|
@@ -533,7 +535,7 @@ import {dom} from "@e280/sly"
|
|
|
533
535
|
|
|
534
536
|
```ts
|
|
535
537
|
import {nap} from "@e280/stz"
|
|
536
|
-
import {Pod, podium, Op,
|
|
538
|
+
import {Pod, podium, Op, loaders} from "@e280/sly"
|
|
537
539
|
```
|
|
538
540
|
|
|
539
541
|
### 🫛 pods: loading/ready/error
|
|
@@ -632,14 +634,28 @@ import {Pod, podium, Op, makeLoader, anims} from "@e280/sly"
|
|
|
632
634
|
- loading if any ops are in loading, otherwise
|
|
633
635
|
- ready if all the ops are ready
|
|
634
636
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
<br/><br/>
|
|
640
|
+
<a id="loaders"></a>
|
|
641
|
+
|
|
642
|
+
## ⏳🦝 sly loaders
|
|
643
|
+
> *animated loading spinners for ops*
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
import {loaders} from "@e280/sly"
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### ⏳ make a loader, choose an anim
|
|
650
|
+
- create a loader fn
|
|
637
651
|
```ts
|
|
638
|
-
const loader =
|
|
652
|
+
const loader = loaders.make(loaders.anims.dots)
|
|
639
653
|
```
|
|
640
654
|
- see all the anims available on the testing page https://sly.e280.org/
|
|
641
655
|
- ngl, i made too many.. *i was having fun, okay?*
|
|
642
|
-
|
|
656
|
+
|
|
657
|
+
### ⏳ render an op with it
|
|
658
|
+
- use your loader to render an op
|
|
643
659
|
```ts
|
|
644
660
|
return html`
|
|
645
661
|
<h2>cool stuff</h2>
|
|
@@ -655,6 +671,99 @@ import {Pod, podium, Op, makeLoader, anims} from "@e280/sly"
|
|
|
655
671
|
|
|
656
672
|
|
|
657
673
|
|
|
674
|
+
<br/><br/>
|
|
675
|
+
<a id="spa"></a>
|
|
676
|
+
|
|
677
|
+
## 💅🦝 sly spa
|
|
678
|
+
> *hash router for single-page-apps*
|
|
679
|
+
|
|
680
|
+
```ts
|
|
681
|
+
import {spa, html} from "@e280/sly"
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### 💅 spa.Router basics
|
|
685
|
+
- **make a spa router**
|
|
686
|
+
```ts
|
|
687
|
+
const router = new spa.Router({
|
|
688
|
+
routes: {
|
|
689
|
+
home: spa.route("#/", async() => html`home`),
|
|
690
|
+
settings: spa.route("#/settings", async() => html`settings`),
|
|
691
|
+
user: spa.route("#/user/{userId}", async({userId}) => html`user ${userId}`),
|
|
692
|
+
},
|
|
693
|
+
})
|
|
694
|
+
```
|
|
695
|
+
- all route strings must start with `#/`
|
|
696
|
+
- use braces like `{userId}` to accept string params
|
|
697
|
+
- home-equivalent hashes like `""` and `"#"` are normalized to `"#/"`
|
|
698
|
+
- 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*
|
|
699
|
+
- you can provide `loader` option if you want to specify the loading spinner (defaults to `loaders.make()`)
|
|
700
|
+
- you can provide `notFound` option, if you want to specify what is shown on invalid routes (defaults to `() => null`)
|
|
701
|
+
- you can set `auto` option false if you want to omit the default initial refresh and listen calls
|
|
702
|
+
- **render your current page**
|
|
703
|
+
```ts
|
|
704
|
+
return html`
|
|
705
|
+
<div class="my-page">
|
|
706
|
+
${router.render()}
|
|
707
|
+
</div>
|
|
708
|
+
`
|
|
709
|
+
```
|
|
710
|
+
- returns lit content
|
|
711
|
+
- shows a loading spinner when pages are loading
|
|
712
|
+
- will display the notFound content for invalid routes (defaults to null)
|
|
713
|
+
- **perform navigations**
|
|
714
|
+
- go to settings page
|
|
715
|
+
```ts
|
|
716
|
+
await router.nav.settings.go()
|
|
717
|
+
// goes to "#/settings"
|
|
718
|
+
```
|
|
719
|
+
- go to user page
|
|
720
|
+
```ts
|
|
721
|
+
await router.nav.user.go("123")
|
|
722
|
+
// goes to "#/user/123"
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### 💅 spa.Router advanced
|
|
726
|
+
- **generate a route's hash string**
|
|
727
|
+
```ts
|
|
728
|
+
const hash = router.nav.user.hash("123")
|
|
729
|
+
// "#/user/123"
|
|
730
|
+
|
|
731
|
+
return html`
|
|
732
|
+
<a href="${hash}">user 123</a>
|
|
733
|
+
`
|
|
734
|
+
```
|
|
735
|
+
- **check if a route is the currently-active one**
|
|
736
|
+
```ts
|
|
737
|
+
const hash = router.nav.user.active
|
|
738
|
+
// true
|
|
739
|
+
|
|
740
|
+
return html`
|
|
741
|
+
<a href="${hash}">user 123</a>
|
|
742
|
+
`
|
|
743
|
+
```
|
|
744
|
+
- **force-refresh the router**
|
|
745
|
+
```ts
|
|
746
|
+
await router.refresh()
|
|
747
|
+
```
|
|
748
|
+
- **force-navigate the router by hash**
|
|
749
|
+
```ts
|
|
750
|
+
await router.refresh("#/user/123")
|
|
751
|
+
```
|
|
752
|
+
- **get the current hash string (normalized)**
|
|
753
|
+
```ts
|
|
754
|
+
router.hash
|
|
755
|
+
// "#/user/123"
|
|
756
|
+
```
|
|
757
|
+
- **the `route(...)` helper fn enables the braces-params syntax**
|
|
758
|
+
- but, if you wanna do it differently, you *can* implement your own hash parser to do your own funky syntax
|
|
759
|
+
- **dispose the router when you're done with it**
|
|
760
|
+
```ts
|
|
761
|
+
router.dispose()
|
|
762
|
+
// stop listening to hashchange events
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
|
|
658
767
|
<br/><br/>
|
|
659
768
|
<a id="loot"></a>
|
|
660
769
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e280/sly",
|
|
3
|
-
"version": "0.2.0-
|
|
3
|
+
"version": "0.2.0-17",
|
|
4
4
|
"description": "web shadow views",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./x/index.js",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./x/index.js"
|
|
9
|
+
".": "./x/index.js",
|
|
10
|
+
"./dom": "./x/dom/index.js",
|
|
11
|
+
"./loaders": "./x/loaders/index.js",
|
|
12
|
+
"./loot": "./x/loot/index.js",
|
|
13
|
+
"./ops": "./x/ops/index.js",
|
|
14
|
+
"./spa": "./x/spa/index.js",
|
|
15
|
+
"./ui": "./x/ui/index.js"
|
|
10
16
|
},
|
|
11
17
|
"files": [
|
|
12
18
|
"x",
|
|
@@ -16,8 +22,8 @@
|
|
|
16
22
|
"lit": "^3.3.1"
|
|
17
23
|
},
|
|
18
24
|
"dependencies": {
|
|
19
|
-
"@e280/strata": "^0.2.0-
|
|
20
|
-
"@e280/stz": "^0.2.
|
|
25
|
+
"@e280/strata": "^0.2.0-14",
|
|
26
|
+
"@e280/stz": "^0.2.6"
|
|
21
27
|
},
|
|
22
28
|
"devDependencies": {
|
|
23
29
|
"@e280/science": "^0.1.2",
|
package/s/demo/views/loaders.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import {css, html} from "lit"
|
|
3
3
|
import {Op} from "../../ops/op.js"
|
|
4
4
|
import {view} from "../../ui/view.js"
|
|
5
|
+
import {loaders} from "../../loaders/index.js"
|
|
5
6
|
import {cssReset} from "../../ui/base/css-reset.js"
|
|
6
|
-
import {anims, makeLoader} from "../../ops/loaders/make-loader.js"
|
|
7
7
|
|
|
8
8
|
export const LoadersView = view(use => () => {
|
|
9
9
|
use.name("loaders")
|
|
@@ -11,14 +11,14 @@ export const LoadersView = view(use => () => {
|
|
|
11
11
|
|
|
12
12
|
const op = use.once(() => Op.loading())
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
Object.entries(anims).map(([key, anim]) => ({
|
|
14
|
+
const library = use.once(() =>
|
|
15
|
+
Object.entries(loaders.anims).map(([key, anim]) => ({
|
|
16
16
|
key,
|
|
17
|
-
loader:
|
|
17
|
+
loader: loaders.make(anim)
|
|
18
18
|
}))
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
-
return
|
|
21
|
+
return library.map(({key, loader}) => html`
|
|
22
22
|
<div data-anim="${key}">
|
|
23
23
|
<span>${key}</span>
|
|
24
24
|
<span>${loader(op, () => null)}</span>
|
package/s/dom/dom.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
import {render} from "lit"
|
|
3
|
+
import {el} from "./parts/el.js"
|
|
3
4
|
import {AttrSpec} from "./types.js"
|
|
4
5
|
import {attrs} from "./attrs/attrs.js"
|
|
5
6
|
import {Content} from "../ui/types.js"
|
|
7
|
+
import {eve, EveSpec} from "./parts/eve.js"
|
|
6
8
|
import {register} from "./parts/register.js"
|
|
7
|
-
import { eve, EveSpec } from "./parts/eve.js"
|
|
8
9
|
|
|
9
10
|
export type Renderable = HTMLElement | ShadowRoot | DocumentFragment
|
|
10
11
|
export type Queryable = HTMLElement | ShadowRoot | Element | Document | DocumentFragment
|
|
@@ -70,6 +71,7 @@ dom.require = doc.require.bind(doc)
|
|
|
70
71
|
dom.maybe = doc.maybe.bind(doc)
|
|
71
72
|
dom.all = doc.all.bind(doc)
|
|
72
73
|
|
|
74
|
+
dom.el = el
|
|
73
75
|
dom.events = eve
|
|
74
76
|
dom.attrs = attrs
|
|
75
77
|
dom.register = register
|
package/s/dom/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
import {AttrValue} from "../../ui/types.js"
|
|
3
|
+
import {setAttrs} from "../../ui/view/parts/set-attrs.js"
|
|
4
|
+
|
|
5
|
+
export function el<E extends HTMLElement>(
|
|
6
|
+
tagName: string,
|
|
7
|
+
attrs: Record<string, AttrValue>,
|
|
8
|
+
) {
|
|
9
|
+
|
|
10
|
+
const element = document.createElement(tagName) as E
|
|
11
|
+
setAttrs(element, Object.entries(attrs))
|
|
12
|
+
return element
|
|
13
|
+
}
|
|
14
|
+
|
package/s/index.ts
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
export * from "./dom/
|
|
3
|
-
export * from "./
|
|
4
|
-
|
|
5
|
-
export * from "./ops/
|
|
6
|
-
export * from "./
|
|
7
|
-
export * from "./
|
|
8
|
-
export * from "./ops/op.js"
|
|
9
|
-
export * from "./ops/podium.js"
|
|
10
|
-
export * from "./ops/types.js"
|
|
11
|
-
|
|
12
|
-
export * as loot from "./loot/index.js"
|
|
13
|
-
|
|
14
|
-
export * from "./ui/base/css-reset.js"
|
|
15
|
-
export * from "./ui/base/use.js"
|
|
16
|
-
export * from "./ui/view/parts/chain.js"
|
|
17
|
-
export * from "./ui/view/parts/sly-view.js"
|
|
18
|
-
export * from "./ui/base-element.js"
|
|
19
|
-
export * from "./ui/types.js"
|
|
20
|
-
export * from "./ui/view.js"
|
|
2
|
+
export * from "./dom/index.js"
|
|
3
|
+
export * from "./loaders/index.js"
|
|
4
|
+
export * from "./loot/index.js"
|
|
5
|
+
export * from "./ops/index.js"
|
|
6
|
+
export * from "./spa/index.js"
|
|
7
|
+
export * from "./ui/index.js"
|
|
21
8
|
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
2
|
+
import {Loader} from "./types.js"
|
|
3
3
|
import {earth} from "./parts/anims.js"
|
|
4
|
-
import {Content} from "
|
|
4
|
+
import type {Content} from "../ui/types.js"
|
|
5
5
|
import {ErrorDisplay} from "./parts/error-display.js"
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
export type Loader = <V>(op: Op<V>, ready: (value: V) => Content) => Content
|
|
10
|
-
|
|
11
|
-
export function makeLoader(
|
|
7
|
+
export function make(
|
|
12
8
|
loading: () => Content = earth,
|
|
13
9
|
error: (error: any) => Content = (error: any) => ErrorDisplay(error),
|
|
14
10
|
): Loader {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import {css} from "lit"
|
|
3
3
|
import {nap, repeat} from "@e280/stz"
|
|
4
4
|
|
|
5
|
-
import {view} from "
|
|
6
|
-
import {Content} from "
|
|
7
|
-
import {cssReset} from "
|
|
5
|
+
import {view} from "../../ui/view.js"
|
|
6
|
+
import {Content} from "../../ui/types.js"
|
|
7
|
+
import {cssReset} from "../../ui/base/css-reset.js"
|
|
8
8
|
|
|
9
9
|
export function makeAsciiAnim(hz: number, frames: string[]): () => Content {
|
|
10
10
|
return () => AsciiAnim({hz, frames})
|
|
@@ -17,6 +17,7 @@ export const AsciiAnim = view(use => ({hz, frames}: {
|
|
|
17
17
|
|
|
18
18
|
use.name("loading")
|
|
19
19
|
use.styles(cssReset, style)
|
|
20
|
+
|
|
20
21
|
const frame = use.signal(0)
|
|
21
22
|
|
|
22
23
|
use.mount(() => repeat(async() => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import {css, html} from "lit"
|
|
3
|
-
import {view} from "
|
|
4
|
-
import {cssReset} from "
|
|
3
|
+
import {view} from "../../ui/view.js"
|
|
4
|
+
import {cssReset} from "../../ui/base/css-reset.js"
|
|
5
5
|
|
|
6
6
|
export const ErrorDisplay = view(use => (error: any) => {
|
|
7
7
|
use.name("error")
|
package/s/loot/index.ts
CHANGED
package/s/ops/index.ts
ADDED
package/s/spa/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
|
|
2
|
+
import {Hasher, Route} from "./types.js"
|
|
3
|
+
import type {Content} from "../../ui/types.js"
|
|
4
|
+
|
|
5
|
+
type ParamKeys<S extends string> =
|
|
6
|
+
S extends `${string}{${infer P}}${infer R}` ? (string & P) | ParamKeys<R> : never
|
|
7
|
+
|
|
8
|
+
type ParamsOf<S extends string> =
|
|
9
|
+
[ParamKeys<S>] extends [never] ? {} : { [K in ParamKeys<S>]: string }
|
|
10
|
+
|
|
11
|
+
type ParamsTuple<S extends string> =
|
|
12
|
+
keyof ParamsOf<S> extends never ? [] : [ParamsOf<S>]
|
|
13
|
+
|
|
14
|
+
export function hasher<S extends string>(spec: S): Hasher<ParamsTuple<S>> {
|
|
15
|
+
if (!spec.startsWith("#/"))
|
|
16
|
+
throw new Error(`hash route spec must start with "#/"`)
|
|
17
|
+
|
|
18
|
+
const specparts = spec.split("/")
|
|
19
|
+
const braceregex = /\{([^\}\/]+)\}/
|
|
20
|
+
|
|
21
|
+
function parse(hash: string): ParamsTuple<S> | null {
|
|
22
|
+
if (!hash.startsWith("#/"))
|
|
23
|
+
throw new Error(`hash must start with "#/"`)
|
|
24
|
+
|
|
25
|
+
const hashparts = hash.split("/")
|
|
26
|
+
const params: Record<string, string> = {}
|
|
27
|
+
|
|
28
|
+
if (hashparts.length !== specparts.length)
|
|
29
|
+
return null
|
|
30
|
+
|
|
31
|
+
for (const [index, specpart] of specparts.entries()) {
|
|
32
|
+
const hashpart = hashparts.at(index)
|
|
33
|
+
if (hashpart === undefined) return null
|
|
34
|
+
const bracematch = specpart.match(braceregex)
|
|
35
|
+
try {
|
|
36
|
+
if (bracematch) params[bracematch[1]] = decodeURIComponent(hashpart)
|
|
37
|
+
else if (hashpart !== specpart) return null
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (Object.keys(params).length === 0)
|
|
45
|
+
? ([] as ParamsTuple<S>)
|
|
46
|
+
: ([params as ParamsOf<S>] as ParamsTuple<S>)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function make(...[braces]: any[]): string {
|
|
50
|
+
const get = (param: string) => {
|
|
51
|
+
const p = param as any
|
|
52
|
+
if (p in braces) return braces[p]
|
|
53
|
+
else throw new Error(`missing param "${p}"`)
|
|
54
|
+
}
|
|
55
|
+
return specparts.map(specpart => {
|
|
56
|
+
const bracematch = specpart.match(braceregex)
|
|
57
|
+
return bracematch
|
|
58
|
+
? encodeURIComponent(get(bracematch[1]))
|
|
59
|
+
: specpart
|
|
60
|
+
}).join("/")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {parse, make}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function route<S extends string>(
|
|
67
|
+
spec: S,
|
|
68
|
+
fn: (...params: ParamsTuple<S>) => Promise<Content>,
|
|
69
|
+
): Route<ParamsTuple<S>> {
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
hasher: hasher(spec),
|
|
73
|
+
fn,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
import {ev, ob} from "@e280/stz"
|
|
3
|
+
import {Op} from "../../ops/op.js"
|
|
4
|
+
import {ResolvedRoute, Route, Routes} from "./types.js"
|
|
5
|
+
|
|
6
|
+
export function eraseWindowHash() {
|
|
7
|
+
const {pathname, search} = window.location
|
|
8
|
+
history.replaceState(null, "", pathname + search)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeHash(hash: string) {
|
|
12
|
+
const homeEquivalents = [/^$/, /^#$/, /^#\/$/]
|
|
13
|
+
return (homeEquivalents.some(regex => regex.test(hash)))
|
|
14
|
+
? "#/"
|
|
15
|
+
: hash
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class HashNormalizer {
|
|
19
|
+
constructor(public location: Location) {}
|
|
20
|
+
|
|
21
|
+
get hash() {
|
|
22
|
+
const hash = normalizeHash(this.location.hash)
|
|
23
|
+
if (hash === "#/") eraseWindowHash()
|
|
24
|
+
return hash
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set hash(hash: string) {
|
|
28
|
+
this.location.hash = hash
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class Navigable<R extends Routes = any, K extends keyof R = any> {
|
|
33
|
+
static all<R extends Routes>(
|
|
34
|
+
routes: R,
|
|
35
|
+
getRoute: () => Route<any> | null,
|
|
36
|
+
navigate: (hash: string) => Promise<ResolvedRoute<R>>,
|
|
37
|
+
): {[K in keyof R]: Navigable<R, K>} {
|
|
38
|
+
|
|
39
|
+
return ob(routes).map(route => new this(
|
|
40
|
+
route,
|
|
41
|
+
() => (getRoute() === route),
|
|
42
|
+
async(...params: any[]) => navigate(route.hasher.make(...params)),
|
|
43
|
+
))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
public route: Route<Parameters<R[K]["hasher"]["make"]>>,
|
|
48
|
+
private isActive: () => boolean,
|
|
49
|
+
public go: (...params: Parameters<R[K]["hasher"]["make"]>) => Promise<ResolvedRoute<R>>,
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
get active() {
|
|
53
|
+
return this.isActive()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
hash(...params: Parameters<R[K]["hasher"]["make"]>) {
|
|
57
|
+
return this.route.hasher.make(...params)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveRoute<R extends Routes>(
|
|
62
|
+
hash: string,
|
|
63
|
+
routes: R,
|
|
64
|
+
): ResolvedRoute<R> | null {
|
|
65
|
+
|
|
66
|
+
for (const key in routes) {
|
|
67
|
+
const route = routes[key]
|
|
68
|
+
const params = route.hasher.parse(hash)
|
|
69
|
+
if (params) {
|
|
70
|
+
return {
|
|
71
|
+
key,
|
|
72
|
+
route,
|
|
73
|
+
params,
|
|
74
|
+
op: Op.promise(route.fn(...params))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function onHashChange(fn: (event: HashChangeEvent) => void) {
|
|
83
|
+
return ev(window, {hashchange: fn})
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
import {signal} from "@e280/strata"
|
|
3
|
+
import type {Content} from "../../ui/types.js"
|
|
4
|
+
import {Navigable, normalizeHash, resolveRoute} from "./primitives.js"
|
|
5
|
+
import {Hashbearer, Navigables, ResolvedRoute, Routes} from "./types.js"
|
|
6
|
+
|
|
7
|
+
export class RouterCore<R extends Routes> {
|
|
8
|
+
readonly nav: Navigables<R>
|
|
9
|
+
readonly $resolved = signal<ResolvedRoute<R> | null>(null)
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
public readonly routes: R,
|
|
13
|
+
public readonly location: Hashbearer,
|
|
14
|
+
) {
|
|
15
|
+
|
|
16
|
+
this.nav = Navigable.all(
|
|
17
|
+
routes,
|
|
18
|
+
() => this.route,
|
|
19
|
+
async hash => {
|
|
20
|
+
this.location.hash = hash
|
|
21
|
+
const resolved = await this.refresh()
|
|
22
|
+
if (!resolved) throw new Error(`route failed "${hash}"`)
|
|
23
|
+
return resolved
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get hash() {
|
|
29
|
+
return normalizeHash(this.location.hash)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get content(): Content | null {
|
|
33
|
+
return this.$resolved.get()?.op.value ?? null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get route() {
|
|
37
|
+
return this.$resolved.get()?.route ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async refresh(hash?: string) {
|
|
41
|
+
if (hash !== undefined) this.location.hash = hash
|
|
42
|
+
hash = this.hash
|
|
43
|
+
const resolved = resolveRoute(hash, this.routes)
|
|
44
|
+
await this.$resolved.set(resolved)
|
|
45
|
+
await resolved?.op
|
|
46
|
+
return resolved
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|