@e280/sly 0.2.0-16 → 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.
Files changed (109) hide show
  1. package/README.md +121 -12
  2. package/package.json +10 -4
  3. package/s/demo/views/loaders.ts +5 -5
  4. package/s/dom/dom.ts +2 -2
  5. package/s/dom/index.ts +4 -0
  6. package/s/index.ts +6 -19
  7. package/s/loaders/index.barrel.ts +9 -0
  8. package/s/loaders/index.ts +3 -0
  9. package/s/{ops/loaders/make-loader.ts → loaders/make.ts} +3 -7
  10. package/s/loaders/mock.ts +11 -0
  11. package/s/{ops/loaders → loaders}/parts/anims.ts +1 -1
  12. package/s/{ops/loaders → loaders}/parts/ascii-anim.ts +4 -3
  13. package/s/{ops/loaders → loaders}/parts/error-display.ts +2 -2
  14. package/s/loaders/types.ts +6 -0
  15. package/s/loot/index.barrel.ts +5 -0
  16. package/s/loot/index.ts +1 -3
  17. package/s/ops/index.ts +5 -0
  18. package/s/spa/index.barrel.ts +6 -0
  19. package/s/spa/index.ts +3 -0
  20. package/s/spa/plumbing/braces.ts +76 -0
  21. package/s/spa/plumbing/primitives.ts +85 -0
  22. package/s/spa/plumbing/router-core.ts +49 -0
  23. package/s/spa/plumbing/types.ts +39 -0
  24. package/s/spa/router.ts +49 -0
  25. package/s/spa/spa.test.ts +91 -0
  26. package/s/tests.test.ts +4 -1
  27. package/s/ui/index.ts +9 -0
  28. package/x/demo/demo.bundle.min.js +19 -19
  29. package/x/demo/demo.bundle.min.js.map +4 -4
  30. package/x/demo/views/loaders.js +4 -4
  31. package/x/demo/views/loaders.js.map +1 -1
  32. package/x/dom/dom.js +2 -2
  33. package/x/dom/dom.js.map +1 -1
  34. package/x/dom/index.d.ts +2 -0
  35. package/x/dom/index.js +3 -0
  36. package/x/dom/index.js.map +1 -0
  37. package/x/index.d.ts +6 -16
  38. package/x/index.html +2 -2
  39. package/x/index.js +6 -16
  40. package/x/index.js.map +1 -1
  41. package/x/loaders/index.barrel.d.ts +6 -0
  42. package/x/loaders/index.barrel.js +7 -0
  43. package/x/loaders/index.barrel.js.map +1 -0
  44. package/x/loaders/index.d.ts +1 -0
  45. package/x/loaders/index.js +2 -0
  46. package/x/loaders/index.js.map +1 -0
  47. package/x/loaders/make.d.ts +3 -0
  48. package/x/loaders/make.js +6 -0
  49. package/x/loaders/make.js.map +1 -0
  50. package/x/loaders/mock.d.ts +2 -0
  51. package/x/loaders/mock.js +8 -0
  52. package/x/loaders/mock.js.map +1 -0
  53. package/x/{ops/loaders → loaders}/parts/anims.d.ts +1 -1
  54. package/x/loaders/parts/anims.js.map +1 -0
  55. package/x/{ops/loaders → loaders}/parts/ascii-anim.d.ts +2 -2
  56. package/x/{ops/loaders → loaders}/parts/ascii-anim.js +2 -2
  57. package/x/loaders/parts/ascii-anim.js.map +1 -0
  58. package/x/loaders/parts/error-display.d.ts +1 -0
  59. package/x/{ops/loaders → loaders}/parts/error-display.js +2 -2
  60. package/x/loaders/parts/error-display.js.map +1 -0
  61. package/x/loaders/types.d.ts +3 -0
  62. package/x/loaders/types.js +2 -0
  63. package/x/loaders/types.js.map +1 -0
  64. package/x/loot/index.barrel.d.ts +3 -0
  65. package/x/loot/index.barrel.js +4 -0
  66. package/x/loot/index.barrel.js.map +1 -0
  67. package/x/loot/index.d.ts +1 -3
  68. package/x/loot/index.js +1 -3
  69. package/x/loot/index.js.map +1 -1
  70. package/x/ops/index.d.ts +3 -0
  71. package/x/ops/index.js +4 -0
  72. package/x/ops/index.js.map +1 -0
  73. package/x/spa/index.barrel.d.ts +4 -0
  74. package/x/spa/index.barrel.js +3 -0
  75. package/x/spa/index.barrel.js.map +1 -0
  76. package/x/spa/index.d.ts +1 -0
  77. package/x/spa/index.js +2 -0
  78. package/x/spa/index.js.map +1 -0
  79. package/x/spa/plumbing/braces.d.ts +12 -0
  80. package/x/spa/plumbing/braces.js +55 -0
  81. package/x/spa/plumbing/braces.js.map +1 -0
  82. package/x/spa/plumbing/primitives.d.ts +22 -0
  83. package/x/spa/plumbing/primitives.js +65 -0
  84. package/x/spa/plumbing/primitives.js.map +1 -0
  85. package/x/spa/plumbing/router-core.d.ts +13 -0
  86. package/x/spa/plumbing/router-core.js +38 -0
  87. package/x/spa/plumbing/router-core.js.map +1 -0
  88. package/x/spa/plumbing/types.d.ts +34 -0
  89. package/x/spa/plumbing/types.js +2 -0
  90. package/x/spa/plumbing/types.js.map +1 -0
  91. package/x/spa/router.d.ts +16 -0
  92. package/x/spa/router.js +39 -0
  93. package/x/spa/router.js.map +1 -0
  94. package/x/spa/spa.test.d.ts +15 -0
  95. package/x/spa/spa.test.js +78 -0
  96. package/x/spa/spa.test.js.map +1 -0
  97. package/x/tests.test.js +4 -1
  98. package/x/tests.test.js.map +1 -1
  99. package/x/ui/index.d.ts +7 -0
  100. package/x/ui/index.js +8 -0
  101. package/x/ui/index.js.map +1 -0
  102. package/x/ops/loaders/make-loader.d.ts +0 -5
  103. package/x/ops/loaders/make-loader.js +0 -7
  104. package/x/ops/loaders/make-loader.js.map +0 -1
  105. package/x/ops/loaders/parts/anims.js.map +0 -1
  106. package/x/ops/loaders/parts/ascii-anim.js.map +0 -1
  107. package/x/ops/loaders/parts/error-display.d.ts +0 -1
  108. package/x/ops/loaders/parts/error-display.js.map +0 -1
  109. /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) — tools for async operations and loading spinners
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.*`](#use) family of ergonomic hooks
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 components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)
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
- - lame settings for views you should know about
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 chain
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 zoomer twist — its `render` method gives you the same `use` hooks that views enjoy.
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, makeLoader, anims} from "@e280/sly"
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
- ### 🫛 loaders: animated loading spinners
636
- - create a `loader` using `makeLoader`
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 = makeLoader(anims.dots)
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
- - use the loader to render your op
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-16",
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-13",
20
- "@e280/stz": "^0.2.5"
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",
@@ -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 loaders = use.once(() =>
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: makeLoader(anim)
17
+ loader: loaders.make(anim)
18
18
  }))
19
19
  )
20
20
 
21
- return loaders.map(({key, loader}) => html`
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,11 +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
- import { el } from "./parts/el.js"
9
9
 
10
10
  export type Renderable = HTMLElement | ShadowRoot | DocumentFragment
11
11
  export type Queryable = HTMLElement | ShadowRoot | Element | Document | DocumentFragment
package/s/dom/index.ts ADDED
@@ -0,0 +1,4 @@
1
+
2
+ export * from "./types.js"
3
+ export * from "./dom.js"
4
+
package/s/index.ts CHANGED
@@ -1,21 +1,8 @@
1
1
 
2
- export * from "./dom/types.js"
3
- export * from "./dom/dom.js"
4
-
5
- export * from "./ops/loaders/make-loader.js"
6
- export * from "./ops/loaders/parts/ascii-anim.js"
7
- export * from "./ops/loaders/parts/error-display.js"
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
 
@@ -0,0 +1,9 @@
1
+
2
+ export * as anims from "./parts/anims.js"
3
+ export * from "./parts/ascii-anim.js"
4
+ export * from "./parts/error-display.js"
5
+ export * from "./make.js"
6
+ export * from "./mock.js"
7
+ export * from "./types.js"
8
+
9
+
@@ -0,0 +1,3 @@
1
+
2
+ export * as loaders from "./index.barrel.js"
3
+
@@ -1,14 +1,10 @@
1
1
 
2
- import {Op} from "../op.js"
2
+ import {Loader} from "./types.js"
3
3
  import {earth} from "./parts/anims.js"
4
- import {Content} from "../../ui/types.js"
4
+ import type {Content} from "../ui/types.js"
5
5
  import {ErrorDisplay} from "./parts/error-display.js"
6
6
 
7
- export * as anims from "./parts/anims.js"
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 {
@@ -0,0 +1,11 @@
1
+
2
+ import {Loader} from "./types.js"
3
+
4
+ export function mock(): Loader {
5
+ return (op, ready) => op.select({
6
+ ready,
7
+ loading: () => `[loading]`,
8
+ error: () => `[error]`,
9
+ })
10
+ }
11
+
@@ -1,6 +1,6 @@
1
1
 
2
2
  import {makeAsciiAnim} from "./ascii-anim.js"
3
- import {Content} from "../../../ui/types.js"
3
+ import type {Content} from "../../ui/types.js"
4
4
 
5
5
  const fast = 20
6
6
  const mid = 10
@@ -2,9 +2,9 @@
2
2
  import {css} from "lit"
3
3
  import {nap, repeat} from "@e280/stz"
4
4
 
5
- import {view} from "../../../ui/view.js"
6
- import {Content} from "../../../ui/types.js"
7
- import {cssReset} from "../../../ui/base/css-reset.js"
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 "../../../ui/view.js"
4
- import {cssReset} from "../../../ui/base/css-reset.js"
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")
@@ -0,0 +1,6 @@
1
+
2
+ import type {Op} from "../ops/op.js"
3
+ import type {Content} from "../ui/types.js"
4
+
5
+ export type Loader = <V>(op: Op<V>, ready: (value: V) => Content) => Content
6
+
@@ -0,0 +1,5 @@
1
+
2
+ export * from "./drag-and-drops.js"
3
+ export * from "./drops.js"
4
+ export * from "./helpers.js"
5
+
package/s/loot/index.ts CHANGED
@@ -1,5 +1,3 @@
1
1
 
2
- export * from "./drag-and-drops.js"
3
- export * from "./drops.js"
4
- export * from "./helpers.js"
2
+ export * as loot from "./index.barrel.js"
5
3
 
package/s/ops/index.ts ADDED
@@ -0,0 +1,5 @@
1
+
2
+ export * from "./op.js"
3
+ export * from "./podium.js"
4
+ export * from "./types.js"
5
+
@@ -0,0 +1,6 @@
1
+
2
+ export {route} from "./plumbing/braces.js"
3
+ export type {Navigable} from "./plumbing/primitives.js"
4
+ export {RouterOptions, Hasher, Route, Routes, Navigables} from "./plumbing/types.js"
5
+ export {Router} from "./router.js"
6
+
package/s/spa/index.ts ADDED
@@ -0,0 +1,3 @@
1
+
2
+ export * as spa from "./index.barrel.js"
3
+
@@ -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
+