@praxisjs/composables 0.1.5 → 1.0.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.
package/src/dom.ts CHANGED
@@ -1,93 +1,140 @@
1
- import { signal, computed, effect } from "@praxisjs/core/internal";
2
- import type { Computed } from "@praxisjs/shared";
1
+ import { Composable } from "@praxisjs/core";
2
+ import { signal, effect } from "@praxisjs/core/internal";
3
3
 
4
- export function createRef(): { current: HTMLElement | null } {
5
- return { current: null };
6
- }
4
+ export class WindowSize extends Composable {
5
+ declare width: number;
6
+ declare height: number;
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
9
+ private _handler = () => {};
10
+
11
+ setup() {
12
+ const width = signal(window.innerWidth);
13
+ const height = signal(window.innerHeight);
14
+ this._handler = () => {
15
+ width.set(window.innerWidth);
16
+ height.set(window.innerHeight);
17
+ };
18
+ window.addEventListener("resize", this._handler);
19
+ return { width, height };
20
+ }
7
21
 
8
- export function useElementSize(ref: { current: HTMLElement | null }) {
9
- const width = signal(0);
10
- const height = signal(0);
11
- const observer = new ResizeObserver(([entry]) => {
12
- width.set(entry.contentRect.width);
13
- height.set(entry.contentRect.height);
14
- });
15
- effect(() => {
16
- if (ref.current) {
17
- observer.observe(ref.current);
18
- width.set(ref.current.offsetWidth);
19
- height.set(ref.current.offsetHeight);
20
- }
21
- });
22
- return {
23
- width: computed(() => width()),
24
- height: computed(() => height()),
25
- stop: () => {
26
- observer.disconnect();
27
- },
28
- };
22
+ onUnmount() {
23
+ window.removeEventListener("resize", this._handler);
24
+ }
29
25
  }
30
26
 
31
- export function useWindowSize() {
32
- const width = signal(window.innerWidth);
33
- const height = signal(window.innerHeight);
34
- window.addEventListener("resize", () => {
35
- width.set(window.innerWidth);
36
- height.set(window.innerHeight);
37
- });
38
- return { width: computed(() => width()), height: computed(() => height()) };
27
+ export class ScrollPosition extends Composable {
28
+ declare x: number;
29
+ declare y: number;
30
+
31
+ private readonly _target: HTMLElement | Window;
32
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
33
+ private _handler = () => {};
34
+
35
+ constructor(target: HTMLElement | Window = window) {
36
+ super();
37
+ this._target = target;
38
+ }
39
+
40
+ setup() {
41
+ const x = signal(0);
42
+ const y = signal(0);
43
+ const t = this._target;
44
+ this._handler = () => {
45
+ x.set(t === window ? window.scrollX : (t as HTMLElement).scrollLeft);
46
+ y.set(t === window ? window.scrollY : (t as HTMLElement).scrollTop);
47
+ };
48
+ t.addEventListener("scroll", this._handler);
49
+ return { x, y };
50
+ }
51
+
52
+ onUnmount() {
53
+ this._target.removeEventListener("scroll", this._handler);
54
+ }
39
55
  }
40
56
 
41
- export function useScrollPosition(target: HTMLElement | Window = window) {
42
- const x = signal(0);
43
- const y = signal(0);
44
- target.addEventListener("scroll", () => {
45
- x.set(
46
- target === window ? window.scrollX : (target as HTMLElement).scrollLeft,
47
- );
48
- y.set(
49
- target === window ? window.scrollY : (target as HTMLElement).scrollTop,
50
- );
51
- });
52
- return { x: computed(() => x()), y: computed(() => y()) };
57
+ export class ElementSize extends Composable {
58
+ declare width: number;
59
+ declare height: number;
60
+
61
+ private _observer?: ResizeObserver;
62
+
63
+ constructor(private readonly ref: { current: HTMLElement | null }) {
64
+ super();
65
+ }
66
+
67
+ setup() {
68
+ const width = signal(0);
69
+ const height = signal(0);
70
+ this._observer = new ResizeObserver(([entry]) => {
71
+ width.set(entry.contentRect.width);
72
+ height.set(entry.contentRect.height);
73
+ });
74
+ effect(() => {
75
+ if (this.ref.current) {
76
+ this._observer?.observe(this.ref.current);
77
+ width.set(this.ref.current.offsetWidth);
78
+ height.set(this.ref.current.offsetHeight);
79
+ }
80
+ });
81
+ return { width, height };
82
+ }
83
+
84
+ onUnmount() {
85
+ this._observer?.disconnect();
86
+ }
53
87
  }
54
88
 
55
- export function useIntersection(
56
- ref: { current: HTMLElement | null },
57
- options?: IntersectionObserverInit,
58
- ): Computed<boolean> {
59
- const visible = signal(false);
60
- const observer = new IntersectionObserver(([entry]) => {
61
- visible.set(entry.isIntersecting);
62
- }, options);
63
- effect(() => {
64
- if (ref.current) observer.observe(ref.current);
65
- return () => {
66
- observer.disconnect();
67
- };
68
- });
69
- return computed(() => visible());
89
+ export class Intersection extends Composable {
90
+ declare visible: boolean;
91
+
92
+ private _observer?: IntersectionObserver;
93
+
94
+ constructor(
95
+ private readonly ref: { current: HTMLElement | null },
96
+ private readonly options?: IntersectionObserverInit,
97
+ ) {
98
+ super();
99
+ }
100
+
101
+ setup() {
102
+ const visible = signal(false);
103
+ this._observer = new IntersectionObserver(([entry]) => {
104
+ visible.set(entry.isIntersecting);
105
+ }, this.options);
106
+ effect(() => {
107
+ if (this.ref.current) this._observer?.observe(this.ref.current);
108
+ });
109
+ return { visible };
110
+ }
111
+
112
+ onUnmount() {
113
+ this._observer?.disconnect();
114
+ }
70
115
  }
71
116
 
72
- export function useFocus(ref: {
73
- current: HTMLElement | null;
74
- }): Computed<boolean> {
75
- const focused = signal(false);
76
- effect(() => {
77
- const el = ref.current;
78
- if (!el) return;
79
- const onFocus = () => {
80
- focused.set(true);
81
- };
82
- const onBlur = () => {
83
- focused.set(false);
84
- };
85
- el.addEventListener("focus", onFocus);
86
- el.addEventListener("blur", onBlur);
87
- return () => {
88
- el.removeEventListener("focus", onFocus);
89
- el.removeEventListener("blur", onBlur);
90
- };
91
- });
92
- return computed(() => focused());
117
+ export class Focus extends Composable {
118
+ declare focused: boolean;
119
+
120
+ constructor(private readonly ref: { current: HTMLElement | null }) {
121
+ super();
122
+ }
123
+
124
+ setup() {
125
+ const focused = signal(false);
126
+ effect(() => {
127
+ const el = this.ref.current;
128
+ if (!el) return;
129
+ const onFocus = () => { focused.set(true); };
130
+ const onBlur = () => { focused.set(false); };
131
+ el.addEventListener("focus", onFocus);
132
+ el.addEventListener("blur", onBlur);
133
+ return () => {
134
+ el.removeEventListener("focus", onFocus);
135
+ el.removeEventListener("blur", onBlur);
136
+ };
137
+ });
138
+ return { focused };
139
+ }
93
140
  }
package/src/index.ts CHANGED
@@ -1,22 +1,15 @@
1
1
  export {
2
- createRef,
3
- useElementSize,
4
- useWindowSize,
5
- useScrollPosition,
6
- useIntersection,
7
- useFocus,
2
+ WindowSize,
3
+ ScrollPosition,
4
+ ElementSize,
5
+ Intersection,
6
+ Focus,
8
7
  } from "./dom";
8
+ export { MediaQuery, ColorScheme, Mouse, KeyCombo, Idle } from "./browser";
9
9
  export {
10
- useMediaQuery,
11
- useColorScheme,
12
- useMouse,
13
- useKeyCombo,
14
- useIdle,
15
- } from "./browser";
16
- export {
17
- useClipboard,
18
- useGeolocation,
19
- useTimeAgo,
20
- usePagination,
10
+ Clipboard,
11
+ Geolocation,
12
+ TimeAgo,
13
+ Pagination,
14
+ type PaginationOptions,
21
15
  } from "./utilities";
22
- export type { PaginationOptions } from "./utilities";
package/src/utilities.ts CHANGED
@@ -1,123 +1,156 @@
1
+ import { Composable } from "@praxisjs/core";
1
2
  import { signal, computed } from "@praxisjs/core/internal";
2
3
  import type { Signal, Computed } from "@praxisjs/shared";
3
4
 
4
- export function useClipboard(resetDelay = 2000) {
5
- const copied = signal(false);
6
- const content = signal("");
7
- async function copy(text: string): Promise<void> {
8
- try {
9
- await navigator.clipboard.writeText(text);
10
- content.set(text);
11
- copied.set(true);
12
- setTimeout(() => {
13
- copied.set(false);
14
- }, resetDelay);
15
- } catch {
16
- console.warn("[useClipboard] Falha ao copiar");
17
- }
18
- }
19
- return {
20
- copy,
21
- copied: computed(() => copied()),
22
- content: computed(() => content()),
23
- };
5
+ export interface PaginationOptions {
6
+ total: number;
7
+ pageSize: number;
8
+ initial?: number;
24
9
  }
25
10
 
26
- export function useGeolocation(options?: PositionOptions) {
27
- const lat = signal<number | null>(null);
28
- const lng = signal<number | null>(null);
29
- const error = signal<GeolocationPositionError | null>(null);
30
- const loading = signal(true);
31
- navigator.geolocation.getCurrentPosition(
32
- (pos) => {
33
- lat.set(pos.coords.latitude);
34
- lng.set(pos.coords.longitude);
35
- loading.set(false);
36
- },
37
- (err) => {
38
- error.set(err);
39
- loading.set(false);
40
- },
41
- options,
42
- );
43
- return {
44
- lat: computed(() => lat()),
45
- lng: computed(() => lng()),
46
- error: computed(() => error()),
47
- loading: computed(() => loading()),
48
- };
11
+ export class Clipboard extends Composable {
12
+ declare copied: boolean;
13
+ declare content: string;
14
+ declare copy: (text: string) => Promise<void>;
15
+
16
+ constructor(private readonly resetDelay = 2000) {
17
+ super();
18
+ }
19
+
20
+ setup() {
21
+ const copied = signal(false);
22
+ const content = signal("");
23
+ const resetDelay = this.resetDelay;
24
+
25
+ const copy = async (text: string): Promise<void> => {
26
+ try {
27
+ await navigator.clipboard.writeText(text);
28
+ content.set(text);
29
+ copied.set(true);
30
+ setTimeout(() => { copied.set(false); }, resetDelay);
31
+ } catch {
32
+ console.warn("[Clipboard] Falha ao copiar");
33
+ }
34
+ };
35
+
36
+ return { copied, content, copy };
37
+ }
49
38
  }
50
39
 
51
- export function useTimeAgo(
52
- source:
53
- | Signal<Date | number>
54
- | Computed<Date | number>
55
- | (() => Date | number),
56
- locale = "pt-BR",
57
- ): Computed<string> {
58
- const read = typeof source === "function" ? source : source;
59
- const tick = signal(Date.now());
60
- setInterval(() => {
61
- tick.set(Date.now());
62
- }, 60_000);
63
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
64
-
65
- return computed(() => {
66
- void tick();
67
- const diff = new Date(read() as Date).getTime() - Date.now();
68
- const abs = Math.abs(diff);
69
- if (abs < 60_000) return rtf.format(Math.round(diff / 1000), "second");
70
- if (abs < 3_600_000) return rtf.format(Math.round(diff / 60_000), "minute");
71
- if (abs < 86_400_000)
72
- return rtf.format(Math.round(diff / 3_600_000), "hour");
73
- if (abs < 2_592_000_000)
74
- return rtf.format(Math.round(diff / 86_400_000), "day");
75
- return rtf.format(Math.round(diff / 2_592_000_000), "month");
76
- });
40
+ export class Geolocation extends Composable {
41
+ declare lat: number | null;
42
+ declare lng: number | null;
43
+ declare error: GeolocationPositionError | null;
44
+ declare loading: boolean;
45
+
46
+ constructor(private readonly options?: PositionOptions) {
47
+ super();
48
+ }
49
+
50
+ setup() {
51
+ const lat = signal<number | null>(null);
52
+ const lng = signal<number | null>(null);
53
+ const error = signal<GeolocationPositionError | null>(null);
54
+ const loading = signal(true);
55
+
56
+ navigator.geolocation.getCurrentPosition(
57
+ (pos) => {
58
+ lat.set(pos.coords.latitude);
59
+ lng.set(pos.coords.longitude);
60
+ loading.set(false);
61
+ },
62
+ (err) => {
63
+ error.set(err);
64
+ loading.set(false);
65
+ },
66
+ this.options,
67
+ );
68
+
69
+ return { lat, lng, error, loading };
70
+ }
77
71
  }
78
72
 
79
- export interface PaginationOptions {
80
- total: number;
81
- pageSize: number;
82
- initial?: number;
73
+ export class TimeAgo extends Composable {
74
+ declare value: string;
75
+
76
+ private _interval?: ReturnType<typeof setInterval>;
77
+
78
+ constructor(
79
+ private readonly source:
80
+ | Signal<Date | number>
81
+ | Computed<Date | number>
82
+ | (() => Date | number),
83
+ private readonly locale = "pt-BR",
84
+ ) {
85
+ super();
86
+ }
87
+
88
+ setup() {
89
+ const tick = signal(Date.now());
90
+ this._interval = setInterval(() => { tick.set(Date.now()); }, 60_000);
91
+
92
+ const rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: "auto" });
93
+ const read = this.source;
94
+
95
+ const value = computed(() => {
96
+ void tick();
97
+ const diff = new Date(read() as Date).getTime() - Date.now();
98
+ const abs = Math.abs(diff);
99
+ if (abs < 60_000) return rtf.format(Math.round(diff / 1000), "second");
100
+ if (abs < 3_600_000) return rtf.format(Math.round(diff / 60_000), "minute");
101
+ if (abs < 86_400_000) return rtf.format(Math.round(diff / 3_600_000), "hour");
102
+ if (abs < 2_592_000_000) return rtf.format(Math.round(diff / 86_400_000), "day");
103
+ return rtf.format(Math.round(diff / 2_592_000_000), "month");
104
+ });
105
+
106
+ return { value };
107
+ }
108
+
109
+ onUnmount() {
110
+ clearInterval(this._interval);
111
+ }
83
112
  }
84
113
 
85
- export function usePagination(
86
- options: PaginationOptions | (() => PaginationOptions),
87
- ) {
88
- const getOpts = typeof options === "function" ? options : () => options;
89
- const _page = signal(getOpts().initial ?? 1);
90
- const totalPages = computed(() =>
91
- Math.ceil(getOpts().total / getOpts().pageSize),
92
- );
93
- const page = computed(() => Math.min(_page(), totalPages()));
94
- const offset = computed(() => (page() - 1) * getOpts().pageSize);
95
- const hasNext = computed(() => page() < totalPages());
96
- const hasPrev = computed(() => page() > 1);
97
- return {
98
- page,
99
- totalPages,
100
- offset,
101
- hasNext,
102
- hasPrev,
103
- pageSize: computed(() => getOpts().pageSize),
104
- pages: computed(() =>
105
- Array.from({ length: totalPages() }, (_, i) => i + 1),
106
- ),
107
- next() {
108
- if (hasNext()) _page.update((p) => p + 1);
109
- },
110
- prev() {
111
- if (hasPrev()) _page.update((p) => p - 1);
112
- },
113
- goTo(p: number) {
114
- _page.set(Math.max(1, Math.min(p, totalPages())));
115
- },
116
- first() {
117
- _page.set(1);
118
- },
119
- last() {
120
- _page.set(totalPages());
121
- },
122
- };
114
+ export class Pagination extends Composable {
115
+ declare page: number;
116
+ declare totalPages: number;
117
+ declare offset: number;
118
+ declare hasNext: boolean;
119
+ declare hasPrev: boolean;
120
+ declare pageSize: number;
121
+ declare pages: number[];
122
+ declare next: () => void;
123
+ declare prev: () => void;
124
+ declare goTo: (p: number) => void;
125
+ declare first: () => void;
126
+ declare last: () => void;
127
+
128
+ constructor(private readonly options: PaginationOptions) {
129
+ super();
130
+ }
131
+
132
+ setup() {
133
+ const opts = this.options;
134
+ const _page = signal(opts.initial ?? 1);
135
+ const totalPages = computed(() => Math.ceil(opts.total / opts.pageSize));
136
+ const page = computed(() => Math.min(_page(), totalPages()));
137
+ const offset = computed(() => (page() - 1) * opts.pageSize);
138
+ const hasNext = computed(() => page() < totalPages());
139
+ const hasPrev = computed(() => page() > 1);
140
+
141
+ return {
142
+ page,
143
+ totalPages,
144
+ offset,
145
+ hasNext,
146
+ hasPrev,
147
+ pageSize: computed(() => opts.pageSize),
148
+ pages: computed(() => Array.from({ length: totalPages() }, (_, i) => i + 1)),
149
+ next: () => { if (hasNext()) _page.update((p) => p + 1); },
150
+ prev: () => { if (hasPrev()) _page.update((p) => p - 1); },
151
+ goTo: (p: number) => { _page.set(Math.max(1, Math.min(p, totalPages()))); },
152
+ first: () => { _page.set(1); },
153
+ last: () => { _page.set(totalPages()); },
154
+ };
155
+ }
123
156
  }