@typed/ui 0.1.5 → 0.3.0

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 (50) hide show
  1. package/Anchor/package.json +6 -0
  2. package/Link/package.json +6 -0
  3. package/dist/cjs/Anchor.js +39 -48
  4. package/dist/cjs/Anchor.js.map +1 -1
  5. package/dist/cjs/Link.js +27 -17
  6. package/dist/cjs/Link.js.map +1 -1
  7. package/dist/cjs/index.js +22 -0
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/internal/addEventListener.js +19 -0
  10. package/dist/cjs/internal/addEventListener.js.map +1 -0
  11. package/dist/cjs/useClickAway.js +43 -0
  12. package/dist/cjs/useClickAway.js.map +1 -0
  13. package/dist/cjs/usePagination.js +68 -0
  14. package/dist/cjs/usePagination.js.map +1 -0
  15. package/dist/dts/Anchor.d.ts +8 -7
  16. package/dist/dts/Anchor.d.ts.map +1 -1
  17. package/dist/dts/Link.d.ts +2 -2
  18. package/dist/dts/Link.d.ts.map +1 -1
  19. package/dist/dts/index.d.ts +8 -0
  20. package/dist/dts/index.d.ts.map +1 -1
  21. package/dist/dts/internal/addEventListener.d.ts +6 -0
  22. package/dist/dts/internal/addEventListener.d.ts.map +1 -0
  23. package/dist/dts/internal/dom-properties.d.ts +36 -36
  24. package/dist/dts/internal/dom-properties.d.ts.map +1 -1
  25. package/dist/dts/useClickAway.d.ts +17 -0
  26. package/dist/dts/useClickAway.d.ts.map +1 -0
  27. package/dist/dts/usePagination.d.ts +41 -0
  28. package/dist/dts/usePagination.d.ts.map +1 -0
  29. package/dist/esm/Anchor.js +21 -45
  30. package/dist/esm/Anchor.js.map +1 -1
  31. package/dist/esm/Link.js +12 -11
  32. package/dist/esm/Link.js.map +1 -1
  33. package/dist/esm/index.js +29 -0
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/internal/addEventListener.js +11 -0
  36. package/dist/esm/internal/addEventListener.js.map +1 -0
  37. package/dist/esm/useClickAway.js +36 -0
  38. package/dist/esm/useClickAway.js.map +1 -0
  39. package/dist/esm/usePagination.js +59 -0
  40. package/dist/esm/usePagination.js.map +1 -0
  41. package/package.json +29 -13
  42. package/src/Anchor.ts +46 -54
  43. package/src/Link.ts +27 -18
  44. package/src/index.ts +32 -0
  45. package/src/internal/addEventListener.ts +22 -0
  46. package/src/internal/dom-properties.ts +36 -36
  47. package/src/useClickAway.ts +64 -0
  48. package/src/usePagination.ts +122 -0
  49. package/useClickAway/package.json +6 -0
  50. package/usePagination/package.json +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typed/ui",
3
- "version": "0.1.5",
3
+ "version": "0.3.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,18 +10,18 @@
10
10
  "sideEffects": [],
11
11
  "author": "Typed contributors",
12
12
  "dependencies": {
13
- "@effect/platform": "^0.31.1",
14
- "@effect/schema": "0.49.4",
15
- "csstype": "^3.1.2",
16
- "effect": "2.0.0-next.56",
17
- "@typed/context": "0.19.2",
18
- "@typed/dom": "8.19.2",
19
- "@typed/environment": "0.1.3",
20
- "@typed/fx": "1.18.4",
21
- "@typed/navigation": "0.6.4",
22
- "@typed/route": "1.0.4",
23
- "@typed/router": "0.19.5",
24
- "@typed/template": "0.1.4"
13
+ "@effect/platform": "^0.41.0",
14
+ "@effect/schema": "0.59.1",
15
+ "csstype": "^3.1.3",
16
+ "effect": "^2.0.3",
17
+ "@typed/context": "0.21.0",
18
+ "@typed/dom": "9.0.0",
19
+ "@typed/environment": "0.3.0",
20
+ "@typed/fx": "1.20.0",
21
+ "@typed/navigation": "0.8.0",
22
+ "@typed/route": "2.0.0",
23
+ "@typed/router": "0.21.0",
24
+ "@typed/template": "0.3.0"
25
25
  },
26
26
  "main": "./dist/cjs/index.js",
27
27
  "module": "./dist/esm/index.js",
@@ -42,6 +42,16 @@
42
42
  "types": "./dist/dts/Link.d.ts",
43
43
  "import": "./dist/esm/Link.js",
44
44
  "default": "./dist/cjs/Link.js"
45
+ },
46
+ "./useClickAway": {
47
+ "types": "./dist/dts/useClickAway.d.ts",
48
+ "import": "./dist/esm/useClickAway.js",
49
+ "default": "./dist/cjs/useClickAway.js"
50
+ },
51
+ "./usePagination": {
52
+ "types": "./dist/dts/usePagination.d.ts",
53
+ "import": "./dist/esm/usePagination.js",
54
+ "default": "./dist/cjs/usePagination.js"
45
55
  }
46
56
  },
47
57
  "typesVersions": {
@@ -51,6 +61,12 @@
51
61
  ],
52
62
  "Link": [
53
63
  "./dist/dts/Link.d.ts"
64
+ ],
65
+ "useClickAway": [
66
+ "./dist/dts/useClickAway.d.ts"
67
+ ],
68
+ "usePagination": [
69
+ "./dist/dts/usePagination.d.ts"
54
70
  ]
55
71
  }
56
72
  }
package/src/Anchor.ts CHANGED
@@ -9,11 +9,11 @@ import type { DefaultEventMap, ElementSource } from "@typed/template/ElementSour
9
9
  import * as EventHandler from "@typed/template/EventHandler"
10
10
  import type { Placeholder } from "@typed/template/Placeholder"
11
11
  import type { Renderable } from "@typed/template/Renderable"
12
- import type { TemplateFx } from "@typed/template/RenderTemplate"
13
- import { html } from "@typed/template/RenderTemplate"
12
+ import type { RenderEvent } from "@typed/template/RenderEvent"
13
+ import { html, type RenderTemplate } from "@typed/template/RenderTemplate"
14
14
  import type { Rendered } from "@typed/wire"
15
15
  import type { ReadonlyRecord, Scope } from "effect"
16
- import { Effect } from "effect"
16
+ import * as Effect from "effect/Effect"
17
17
  import { uncapitalize } from "effect/String"
18
18
  import type { HTMLAnchorElementProperties } from "./internal/dom-properties.js"
19
19
 
@@ -22,14 +22,14 @@ import type { HTMLAnchorElementProperties } from "./internal/dom-properties.js"
22
22
  */
23
23
  export type AnchorProps =
24
24
  & {
25
- readonly [K in keyof HTMLAnchorElementProperties]:
25
+ readonly [K in keyof HTMLAnchorElementProperties]?:
26
26
  | HTMLAnchorElementProperties[K]
27
27
  | Placeholder.Any<HTMLAnchorElementProperties[K]>
28
- | Directive.Directive<any, any>
28
+ | undefined
29
29
  }
30
30
  & {
31
- readonly ref?: (ref: ElementSource<HTMLAnchorElement>) => Effect.Effect<any, any, any>
32
- readonly data?: Placeholder.Any<ReadonlyRecord.ReadonlyRecord<any>>
31
+ readonly ref?: ((ref: ElementSource<HTMLAnchorElement>) => Effect.Effect<any, any, any>) | undefined
32
+ readonly data?: Placeholder.Any<ReadonlyRecord.ReadonlyRecord<any>> | undefined
33
33
  }
34
34
  & EventHandlerProps<HTMLAnchorElement>
35
35
 
@@ -58,60 +58,52 @@ export function Anchor<
58
58
  >(
59
59
  props: Props,
60
60
  ...children: Children
61
- ): TemplateFx<
62
- Placeholder.Context<Props[keyof Props] | ReturnOf<Props["ref"]> | Children[number]>,
61
+ ): Fx.Fx<
62
+ RenderTemplate | Scope.Scope | Placeholder.Context<Props[keyof Props] | ReturnOf<Props["ref"]> | Children[number]>,
63
63
  Placeholder.Error<Props[keyof Props] | ReturnOf<Props["ref"]> | Children[number]>,
64
- HTMLAnchorElement
64
+ RenderEvent
65
65
  > {
66
- const ref = Directive.ref(({ value: ref }) =>
66
+ const {
67
+ data,
68
+ hash,
69
+ host,
70
+ hostname,
71
+ href,
72
+ hreflang,
73
+ pathname,
74
+ port,
75
+ protocol,
76
+ ref,
77
+ scrollLeft,
78
+ scrollTop,
79
+ search,
80
+ ...rest
81
+ } = props
82
+
83
+ const refDirective = Directive.ref(({ value }) =>
67
84
  Effect.gen(function*(_) {
68
- yield* _(addEventListeners(props, ref))
85
+ yield* _(addEventListeners(props, value))
69
86
 
70
- if (props.ref) {
71
- yield* _(props.ref(ref as any))
87
+ if (ref) {
88
+ yield* _(ref(value as any))
72
89
  }
73
90
  })
74
91
  )
75
92
  return html`<a
76
- ref="${ref}"
77
- .data="${props.data}"
78
- ?hidden="${props.hidden}"
79
- ?hidefocus="${props.hideFocus}"
80
- ?spellcheck="${props.spellcheck}"
81
- .scrollLeft="${props.scrollLeft}"
82
- .scrollTop="${props.scrollTop}"
83
- accesskey="${props.accessKey}"
84
- charset="${props.charset}"
85
- class="${props.className}"
86
- contenteditable="${props.contentEditable}"
87
- coords="${props.coords}"
88
- dir="${props.dir}"
89
- download="${props.download}"
90
- draggable="${props.draggable}"
91
- .hash="${props.hash}"
92
- .host="${props.host}"
93
- .hostname="${props.hostname}"
94
- .href="${props.href}"
95
- hreflang="${props.hreflang}"
96
- id="${props.id}"
97
- id="${props.id}"
98
- lang="${props.lang}"
99
- Methods="${props.Methods}"
100
- name="${props.name}"
101
- .pathname="${props.pathname}"
102
- .port="${props.port}"
103
- .protocol="${props.protocol}"
104
- rel="${props.rel}"
105
- rev="${props.rev}"
106
- .search="${props.search}"
107
- shape="${props.shape}"
108
- slot="${props.slot}"
109
- tabindex="${props.tabIndex}"
110
- target="${props.target}"
111
- text="${props.text}"
112
- title="${props.title}"
113
- type="${props.type}"
114
- urn="${props.urn}"
93
+ ref="${refDirective}"
94
+ .props="${rest}"
95
+ .data="${data}"
96
+ .scrollLeft="${scrollLeft}"
97
+ .scrollTop="${scrollTop}"
98
+ .hash="${hash}"
99
+ .host="${host}"
100
+ .hostname="${hostname}"
101
+ .href="${href}"
102
+ .hreflang="${hreflang}"
103
+ .pathname="${pathname}"
104
+ .port="${port}"
105
+ .protocol="${protocol}"
106
+ .search="${search}"
115
107
  >${children}</a>` as any
116
108
  }
117
109
 
@@ -123,7 +115,7 @@ export function addEventListeners<Props extends EventHandlerProps<any>, T extend
123
115
  ref: ElementSource<T>
124
116
  ): Effect.Effect<Scope.Scope | GetEventHandlersContext<Props>, never, void> {
125
117
  return Fx.forkScoped(
126
- Fx.merge(
118
+ Fx.mergeAll(
127
119
  getEventHandlers(props).map(([type, handler]: any) => addEventListener(ref, type, handler))
128
120
  )
129
121
  ) as any
package/src/Link.ts CHANGED
@@ -12,7 +12,8 @@ import { Placeholder } from "@typed/template/Placeholder"
12
12
  import type { Renderable } from "@typed/template/Renderable"
13
13
  import type { RenderEvent } from "@typed/template/RenderEvent"
14
14
  import type { RenderTemplate } from "@typed/template/RenderTemplate"
15
- import { Effect, type Scope } from "effect"
15
+ import * as Effect from "effect/Effect"
16
+ import type * as Scope from "effect/Scope"
16
17
  import { Anchor, type AnchorProps, getEventHandler } from "./Anchor.js"
17
18
 
18
19
  /**
@@ -30,36 +31,36 @@ export type LinkProps = Omit<AnchorProps, keyof URL> & {
30
31
  * @since 1.0.0
31
32
  */
32
33
  export function Link<Props extends LinkProps, Children extends ReadonlyArray<Renderable<any, any>> = readonly []>(
33
- props: Props,
34
+ { onClick, ref, relative, replace, state, to, ...props }: Props,
34
35
  ...children: Children
35
36
  ): Fx.Fx<
36
37
  | Navigation.Navigation
37
38
  | CurrentRoute
38
39
  | RenderTemplate
39
40
  | Scope.Scope
40
- | Location
41
- | Placeholder.Context<Props[keyof Props] | Children[number]>,
42
- Placeholder.Error<Props[keyof Props] | Children[number]>,
41
+ | Placeholder.Context<Props[keyof Props] | Children[number]>
42
+ | Fx.Context<Props[keyof Props] | Children[number]>,
43
+ Placeholder.Error<Props[keyof Props] | Children[number]> | Fx.Error<Props[keyof Props] | Children[number]>,
43
44
  RenderEvent
44
45
  > {
45
46
  return Fx.gen(function*(_) {
46
- const onClickHandler = getEventHandler(props.onClick)
47
- const to = yield* _(
48
- Placeholder.asRef<Placeholder.Context<Props["to"]>, Placeholder.Error<Props["to"]>, string>(props.to)
47
+ const onClickHandler = getEventHandler(onClick)
48
+ const toRef = yield* _(
49
+ Placeholder.asRef<Placeholder.Context<Props["to"]>, Placeholder.Error<Props["to"]>, string>(to)
49
50
  )
50
- const relative = yield* _(
51
+ const relativeRef = yield* _(
51
52
  Placeholder.asRef<Placeholder.Context<Props["relative"]>, Placeholder.Error<Props["relative"]>, boolean>(
52
- props.relative ?? true
53
+ relative ?? true
53
54
  )
54
55
  )
55
- const replace = yield* _(
56
+ const replaceRef = yield* _(
56
57
  Placeholder.asRef<Placeholder.Context<Props["replace"]>, Placeholder.Error<Props["replace"]>, boolean>(
57
- props.replace ?? false
58
+ replace ?? false
58
59
  )
59
60
  )
60
- const state = yield* _(
61
+ const stateRef = yield* _(
61
62
  Placeholder.asRef<Placeholder.Context<Props["state"]>, Placeholder.Error<Props["state"]>, unknown>(
62
- props.state as Placeholder.Any<unknown>
63
+ state as Placeholder.Any<unknown>
63
64
  )
64
65
  )
65
66
  const reloadDocument = yield* _(
@@ -72,10 +73,13 @@ export function Link<Props extends LinkProps, Children extends ReadonlyArray<Ren
72
73
  )
73
74
  )
74
75
 
75
- const href = RefSubject.tuple(relative, to).mapEffect(([rel, to]) => rel ? makeHref(to) : Effect.succeed(to))
76
+ const href = RefSubject.mapEffect(
77
+ RefSubject.tuple([relativeRef, toRef]),
78
+ ([rel, to]) => rel ? makeHref(to) : Effect.succeed(to)
79
+ )
76
80
 
77
81
  const navigate = Effect.gen(function*(_) {
78
- const current = yield* _(Effect.all({ replace, state, reloadDocument }))
82
+ const current = yield* _(Effect.all({ replace: replaceRef, state: stateRef, reloadDocument }))
79
83
  const url = yield* _(href)
80
84
 
81
85
  yield* _(Navigation.navigate(url, {
@@ -88,7 +92,7 @@ export function Link<Props extends LinkProps, Children extends ReadonlyArray<Ren
88
92
  }
89
93
  })
90
94
 
91
- const onClick = EventHandler.preventDefault(
95
+ const onClickEventHandler = EventHandler.preventDefault(
92
96
  (ev: EventWithCurrentTarget<HTMLAnchorElement, MouseEvent>) =>
93
97
  Effect.gen(function*(_) {
94
98
  if (onClickHandler) {
@@ -99,6 +103,11 @@ export function Link<Props extends LinkProps, Children extends ReadonlyArray<Ren
99
103
  onClickHandler?.options
100
104
  )
101
105
 
102
- return Anchor({ ...props, href, state, onClick }, ...children)
106
+ const allProps = { ...props, ref, href, state: stateRef, onClick: onClickEventHandler }
107
+
108
+ return Anchor(
109
+ allProps as any as AnchorProps,
110
+ ...children
111
+ )
103
112
  })
104
113
  }
package/src/index.ts CHANGED
@@ -2,6 +2,28 @@
2
2
  * @since 1.0.0
3
3
  */
4
4
 
5
+ // TODO:
6
+ // Form + Fields/FieldGroups
7
+ // Masking
8
+ // Accordion
9
+ // Button
10
+ // Select
11
+ // MultiSelect
12
+ // Drag and drop
13
+ // Popovers
14
+ // Portals
15
+ // Menu
16
+ // Trees
17
+ // Modals
18
+ // Search/Autocomplete
19
+ // Tables
20
+ // Editable Tables
21
+ // File uploads
22
+ // Focus management
23
+ // Toasts
24
+ // Virtualization
25
+ // Optimistic UI helpers??
26
+
5
27
  /**
6
28
  * @since 1.0.0
7
29
  */
@@ -10,3 +32,13 @@ export * from "./Anchor.js"
10
32
  * @since 1.0.0
11
33
  */
12
34
  export * from "./Link.js"
35
+
36
+ /**
37
+ * @since 1.0.0
38
+ */
39
+ export * from "./useClickAway.js"
40
+
41
+ /**
42
+ * @since 1.0.0
43
+ */
44
+ export * from "./usePagination.js"
@@ -0,0 +1,22 @@
1
+ import type { EventWithCurrentTarget } from "@typed/dom/EventTarget"
2
+ import * as Fx from "@typed/fx/Fx"
3
+ import type { DefaultEventMap } from "@typed/template/ElementSource"
4
+ import type { Scope } from "effect"
5
+ import * as Effect from "effect/Effect"
6
+
7
+ export function addEventListeners<T extends EventTarget, Events extends ReadonlyArray<keyof DefaultEventMap<T>>>(
8
+ target: T,
9
+ ...events: Events
10
+ ) {
11
+ return Fx.withEmitter<never, EventWithCurrentTarget<T, DefaultEventMap<T>[Events[number]]>, Scope.Scope>(
12
+ (emitter) => {
13
+ events.forEach((event) => target.addEventListener(event as string, emitter.succeed as any))
14
+
15
+ return Effect.addFinalizer(() =>
16
+ Effect.sync(() => {
17
+ events.forEach((event) => target.addEventListener(event as string, emitter.succeed as any))
18
+ })
19
+ )
20
+ }
21
+ )
22
+ }
@@ -1,50 +1,50 @@
1
1
  export type ElementProperties = {
2
- className?: string
3
- id?: string
4
- scrollLeft?: number
5
- scrollTop?: number
6
- slot?: string
2
+ className?: string | undefined
3
+ id?: string | undefined
4
+ scrollLeft?: number | undefined
5
+ scrollTop?: number | undefined
6
+ slot?: string | undefined
7
7
  }
8
8
 
9
9
  export type HTMLElementProperties =
10
10
  & ElementProperties
11
11
  & {
12
- accessKey?: string
13
- contentEditable?: string
14
- dir?: string
15
- draggable?: boolean
16
- hidden?: boolean
17
- hideFocus?: boolean
18
- lang?: string
19
- spellcheck?: boolean
20
- tabIndex?: boolean
21
- title?: string
12
+ accessKey?: string | undefined
13
+ contentEditable?: string | undefined
14
+ dir?: string | undefined
15
+ draggable?: boolean | undefined
16
+ hidden?: boolean | undefined
17
+ hideFocus?: boolean | undefined
18
+ lang?: string | undefined
19
+ spellcheck?: boolean | undefined
20
+ tabIndex?: boolean | undefined
21
+ title?: string | undefined
22
22
  }
23
23
 
24
24
  export type HTMLAnchorElementProperties =
25
25
  & HTMLElementProperties
26
26
  & {
27
- Methods?: string
28
- charset?: string
29
- coords?: string
30
- download?: string
31
- hash?: string
32
- host?: string
33
- hostname?: string
34
- href?: string
35
- hreflang?: string
36
- name?: string
37
- pathname?: string
38
- port?: string
39
- protocol?: string
40
- rel?: string
41
- rev?: string
42
- search?: string
43
- shape?: string
44
- target?: string
45
- text?: string
46
- type?: string
47
- urn?: string
27
+ Methods?: string | undefined
28
+ charset?: string | undefined
29
+ coords?: string | undefined
30
+ download?: string | undefined
31
+ hash?: string | undefined
32
+ host?: string | undefined
33
+ hostname?: string | undefined
34
+ href?: string | undefined
35
+ hreflang?: string | undefined
36
+ name?: string | undefined
37
+ pathname?: string | undefined
38
+ port?: string | undefined
39
+ protocol?: string | undefined
40
+ rel?: string | undefined
41
+ rev?: string | undefined
42
+ search?: string | undefined
43
+ shape?: string | undefined
44
+ target?: string | undefined
45
+ text?: string | undefined
46
+ type?: string | undefined
47
+ urn?: string | undefined
48
48
  }
49
49
 
50
50
  export type HTMLAppletElementProperties =
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+
5
+ import { Document } from "@typed/dom/Document"
6
+ import type { EventWithCurrentTarget } from "@typed/dom/EventTarget"
7
+ import * as Fx from "@typed/fx/Fx"
8
+ import type * as ElementRef from "@typed/template/ElementRef"
9
+ import { getElements } from "@typed/template/ElementSource"
10
+ import type { Rendered } from "@typed/wire"
11
+ import type { Effect, Fiber, Scope } from "effect"
12
+ import * as Option from "effect/Option"
13
+ import { addEventListeners } from "./internal/addEventListener"
14
+
15
+ /**
16
+ * @since 1.0.0
17
+ */
18
+ export function useClickAway<Refs extends ReadonlyArray<ElementRef.ElementRef<any>>, R2>(
19
+ refs: Refs,
20
+ f: (event: EventWithCurrentTarget<Document, MouseEvent | TouchEvent>) => Effect.Effect<R2, never, unknown>
21
+ ): Effect.Effect<Document | Scope.Scope | R2, never, Fiber.RuntimeFiber<never, void>> {
22
+ return Fx.forkScoped(onClickAway(refs, f))
23
+ }
24
+
25
+ /**
26
+ * @since 1.0.0
27
+ */
28
+ export function onClickAway<Refs extends ReadonlyArray<ElementRef.ElementRef<any>>, R2, E2, B>(
29
+ refs: Refs,
30
+ f: (event: EventWithCurrentTarget<Document, MouseEvent | TouchEvent>) => Effect.Effect<R2, E2, B>
31
+ ): Fx.Fx<Document | R2 | Scope.Scope, E2, B> {
32
+ return Fx.fromFxEffect(Document.with((document) => {
33
+ const events = addEventListeners(document, "click", "touchend", "contextmenu")
34
+ const elements = Fx.map(
35
+ Fx.tuple(refs as ReadonlyArray<Fx.Fx<Scope.Scope, never, Rendered>>),
36
+ (els) => els.flatMap(getElements)
37
+ )
38
+
39
+ return Fx.mapEffect(
40
+ Fx.compact(Fx.snapshot(events, elements, containsRefs)),
41
+ f
42
+ )
43
+ }))
44
+ }
45
+
46
+ const containsRefs = (
47
+ event: EventWithCurrentTarget<Document, MouseEvent | TouchEvent>,
48
+ refs: ReadonlyArray<Element>
49
+ ): Option.Option<EventWithCurrentTarget<Document, MouseEvent | TouchEvent>> => {
50
+ const target = event.target
51
+
52
+ if (
53
+ target === null ||
54
+ refs.some(
55
+ (c) =>
56
+ c === target ||
57
+ c.contains(target as Element)
58
+ )
59
+ ) {
60
+ return Option.none()
61
+ } else {
62
+ return Option.some(event)
63
+ }
64
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+
5
+ import * as RefSubject from "@typed/fx/RefSubject"
6
+ import type { Scope } from "effect"
7
+ import * as Effect from "effect/Effect"
8
+
9
+ /**
10
+ * @since 1.0.0
11
+ */
12
+ export type PaginationOptions = {
13
+ readonly initialPage?: number // 0
14
+ readonly initialPageSize?: number // 10
15
+ }
16
+
17
+ /**
18
+ * @since 1.0.0
19
+ */
20
+ export interface Pagination<E, A> {
21
+ readonly page: RefSubject.Computed<never, never, number>
22
+ readonly pageSize: RefSubject.Computed<never, never, number>
23
+ readonly canGoBack: RefSubject.Computed<never, never, boolean>
24
+ readonly canGoForward: RefSubject.Computed<never, E, boolean>
25
+ readonly paginated: RefSubject.Computed<never, E, ReadonlyArray<A>>
26
+ readonly viewing: RefSubject.Computed<never, E, Viewing>
27
+
28
+ readonly goBack: Effect.Effect<never, never, number>
29
+ readonly goForward: Effect.Effect<never, E, number>
30
+ readonly goToStart: Effect.Effect<never, never, number>
31
+ readonly goToEnd: Effect.Effect<never, E, number>
32
+ }
33
+
34
+ /**
35
+ * @since 1.0.0
36
+ */
37
+ export interface Viewing {
38
+ readonly from: number
39
+ readonly to: number
40
+ readonly total: number
41
+ }
42
+
43
+ /**
44
+ * @since 1.0.0
45
+ */
46
+ export function usePagination<R, E, A>(
47
+ items: RefSubject.Computed<R, E, ReadonlyArray<A>>,
48
+ options: PaginationOptions = {}
49
+ ): Effect.Effect<R | Scope.Scope, never, Pagination<E, A>> {
50
+ return Effect.gen(function*(_) {
51
+ const ctx = yield* _(Effect.context<R>())
52
+ const page: RefSubject.RefSubject<never, never, number> = yield* _(RefSubject.of(options.initialPage ?? 0))
53
+ const pageSize: RefSubject.RefSubject<never, never, number> = yield* _(RefSubject.of(options.initialPageSize ?? 10))
54
+ const canGoBack: RefSubject.Computed<never, never, boolean> = RefSubject.map(page, (x) => x > 0)
55
+ const combined: RefSubject.Computed<never, E, readonly [number, number, ReadonlyArray<A>]> = RefSubject.provide(
56
+ RefSubject.tuple([page, pageSize, items] as const),
57
+ ctx
58
+ )
59
+ const canGoForward: RefSubject.Computed<never, E, boolean> = RefSubject.map(
60
+ combined,
61
+ ([page, pageSize, results]) => page < Math.ceil(results.length / pageSize - 1)
62
+ )
63
+
64
+ const getTotalPages: Effect.Effect<never, E, number> = Effect.provide(
65
+ Effect.gen(function*($) {
66
+ const currentPageSize = yield* $(pageSize)
67
+ const results = yield* $(items)
68
+
69
+ return Math.ceil(results.length / currentPageSize - 1)
70
+ }),
71
+ ctx
72
+ )
73
+ const goBack: Effect.Effect<never, never, number> = RefSubject.update(page, (x) => Math.max(x - 1, 0))
74
+ const goForward: Effect.Effect<never, E, number> = RefSubject.updateEffect(
75
+ page,
76
+ (currentPage) =>
77
+ Effect.gen(function*($) {
78
+ const totalPages = yield* $(getTotalPages)
79
+ const nextPage = Math.min(currentPage + 1, totalPages)
80
+
81
+ return nextPage
82
+ })
83
+ )
84
+ const goToStart: Effect.Effect<never, never, number> = RefSubject.set(page, 0)
85
+ const goToEnd: Effect.Effect<never, E, number> = RefSubject.updateEffect(page, () => getTotalPages)
86
+
87
+ const paginated: RefSubject.Computed<never, E, ReadonlyArray<A>> = RefSubject.map(
88
+ combined,
89
+ ([page, pageSize, results]) => {
90
+ const start = page * pageSize
91
+ const end = start + pageSize
92
+
93
+ return results.slice(start, end)
94
+ }
95
+ )
96
+
97
+ const viewing: RefSubject.Computed<never, E, Viewing> = RefSubject.map(combined, ([page, pageSize, results]) => {
98
+ const start = page * pageSize
99
+ const end = start + pageSize
100
+ const total = results.length
101
+
102
+ return {
103
+ from: start + 1,
104
+ to: Math.min(end, total),
105
+ total
106
+ }
107
+ })
108
+
109
+ return {
110
+ page,
111
+ pageSize,
112
+ canGoBack,
113
+ canGoForward,
114
+ goBack,
115
+ goForward,
116
+ goToStart,
117
+ goToEnd,
118
+ paginated,
119
+ viewing
120
+ } as const
121
+ })
122
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "main": "../dist/cjs/useClickAway.js",
3
+ "module": "../dist/esm/useClickAway.js",
4
+ "types": "../dist/dts/useClickAway.d.ts",
5
+ "sideEffects": []
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "main": "../dist/cjs/usePagination.js",
3
+ "module": "../dist/esm/usePagination.js",
4
+ "types": "../dist/dts/usePagination.d.ts",
5
+ "sideEffects": []
6
+ }