mark-3 0.0.3 → 0.0.5

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 (68) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/components/index.js +1 -0
  3. package/components/scroll-aware-content.vue +162 -0
  4. package/composables/index.js +4 -1
  5. package/composables/use-intersection-observer.js +67 -0
  6. package/composables/use-responsiveness.js +61 -0
  7. package/composables/use-scroll-event.js +97 -0
  8. package/helpers/array/index.js +1 -0
  9. package/helpers/array/pick-random.js +24 -0
  10. package/helpers/array/tests/pick-random.test.js +39 -0
  11. package/helpers/date/index.js +1 -1
  12. package/helpers/date/{from-timestamp/index.test.js → tests/format-timestamp.test.js} +1 -1
  13. package/helpers/date/tests/format.test.js +97 -0
  14. package/helpers/date/{is-on-same-day/index.test.js → tests/is-on-same-day.test.js} +1 -1
  15. package/helpers/date/{is-on-same-year/index.test.js → tests/is-on-same-year.test.js} +1 -1
  16. package/helpers/number/clamp.js +17 -0
  17. package/helpers/number/index.js +2 -0
  18. package/helpers/number/random.js +24 -0
  19. package/helpers/number/tests/clamp.test.js +37 -0
  20. package/helpers/number/tests/random.test.js +51 -0
  21. package/helpers/string/{camel-to-dash/index.js → camel-to-dash.js} +2 -2
  22. package/helpers/string/encode-subscript.js +45 -0
  23. package/helpers/string/{escape-regex/index.js → escape-regex.js} +1 -1
  24. package/helpers/string/index.js +1 -0
  25. package/helpers/string/{camel-to-dash/index.test.js → tests/camel-to-dash.test.js} +1 -1
  26. package/helpers/string/tests/encode-subscript.test.js +41 -0
  27. package/helpers/string/{escape-regex/index.test.js → tests/escape-regex.test.js} +1 -1
  28. package/helpers/string/{trim/index.test.js → tests/trim.test.js} +1 -1
  29. package/helpers/string/{trim/index.js → trim.js} +3 -3
  30. package/helpers/time/debounce.js +29 -0
  31. package/helpers/time/index.js +1 -0
  32. package/helpers/time/tests/debounce.test.js +58 -0
  33. package/helpers/types/index.js +3 -0
  34. package/helpers/types/{is-empty/index.js → is-empty.js} +1 -1
  35. package/helpers/types/is-map.js +12 -0
  36. package/helpers/types/is-numeric.js +22 -0
  37. package/helpers/types/{is-plain-object/index.js → is-plain-object.js} +8 -3
  38. package/helpers/types/is-set.js +12 -0
  39. package/helpers/types/{get-type-name/index.test.js → tests/get-type-name.test.js} +2 -2
  40. package/helpers/types/{is-array/index.test.js → tests/is-array.test.js} +1 -1
  41. package/helpers/types/{is-empty/index.test.js → tests/is-empty.test.js} +1 -1
  42. package/helpers/types/tests/is-map.test.js +41 -0
  43. package/helpers/types/tests/is-numeric.test.js +51 -0
  44. package/helpers/types/{is-plain-object/index.test.js → tests/is-plain-object.test.js} +1 -1
  45. package/helpers/types/tests/is-set.test.js +41 -0
  46. package/helpers/types/tests/is-string.test.js +46 -0
  47. package/package.json +1 -1
  48. package/helpers/date/format/README.md +0 -33
  49. package/helpers/date/format/index.test.js +0 -97
  50. package/helpers/date/from-timestamp/README.md +0 -14
  51. package/helpers/date/is-on-same-day/README.md +0 -12
  52. package/helpers/date/is-on-same-year/README.md +0 -13
  53. package/helpers/string/camel-to-dash/README.md +0 -24
  54. package/helpers/string/escape-regex/README.md +0 -12
  55. package/helpers/string/trim/README.md +0 -66
  56. package/helpers/types/get-type-name/README.md +0 -11
  57. package/helpers/types/is-array/README.md +0 -11
  58. package/helpers/types/is-empty/README.md +0 -35
  59. package/helpers/types/is-plain-object/README.md +0 -17
  60. package/helpers/types/is-string/README.md +0 -12
  61. package/helpers/types/is-string/index.test.js +0 -46
  62. /package/helpers/date/{from-timestamp/index.js → format-timestamp.js} +0 -0
  63. /package/helpers/date/{format/index.js → format.js} +0 -0
  64. /package/helpers/date/{is-on-same-day/index.js → is-on-same-day.js} +0 -0
  65. /package/helpers/date/{is-on-same-year/index.js → is-on-same-year.js} +0 -0
  66. /package/helpers/types/{get-type-name/index.js → get-type-name.js} +0 -0
  67. /package/helpers/types/{is-array/index.js → is-array.js} +0 -0
  68. /package/helpers/types/{is-string/index.js → is-string.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.0.5](https://github.com/ismailceylan/mark-3/compare/v0.0.4...v0.0.5) (2025-08-03)
6
+
7
+
8
+ ### Features
9
+
10
+ * **component:** add new scroll-aware-content component ([5cb2645](https://github.com/ismailceylan/mark-3/commit/5cb2645a7f2ed778adc5f30a6e684b89b4ac360a))
11
+ * **composables:** add new use-intersection-observer composable ([5d71054](https://github.com/ismailceylan/mark-3/commit/5d710544a3067f29c8b5c4ce1db66c1ab3549a06))
12
+ * **composables:** add new use-scroll-event composable ([00e87cc](https://github.com/ismailceylan/mark-3/commit/00e87cc5bab981f53c4c61a7f3858ad4d0c45a9c))
13
+ * **helpers:** add new array/pick-random method ([1679cfa](https://github.com/ismailceylan/mark-3/commit/1679cfa821219fe85aef19afc17ed1b831d08ffd))
14
+ * **helpers:** add new number/clamp method ([3694de5](https://github.com/ismailceylan/mark-3/commit/3694de551c05ad8cab78e139f35225f92d7e50b1))
15
+ * **helpers:** add new number/random method ([bf718b3](https://github.com/ismailceylan/mark-3/commit/bf718b3ffca5147fb7985bde8d5c22e2763b3bfc))
16
+ * **helpers:** add new string/encode-subscript method ([3dce61a](https://github.com/ismailceylan/mark-3/commit/3dce61a8c9d927126ddb94118a33d0663bba5269))
17
+ * **helpers:** add new time/debounce helper method ([e9c4232](https://github.com/ismailceylan/mark-3/commit/e9c423233f7b946715f8624432af40f96aba9e90))
18
+ * **helpers:** add new types/is-numeric helper method ([ab84773](https://github.com/ismailceylan/mark-3/commit/ab847736ae695fce62e42155c3653a216f6e321b))
19
+
20
+ ### [0.0.4](https://github.com/ismailceylan/mark-3/compare/v0.0.3...v0.0.4) (2025-08-02)
21
+
22
+
23
+ ### Features
24
+
25
+ * **composables:** add new use-responsiveness composable ([3b6ebe4](https://github.com/ismailceylan/mark-3/commit/3b6ebe4f0c50668453071cf4146b3dc459a25f24))
26
+ * **helpers:** add new types/is-map method ([fdcda02](https://github.com/ismailceylan/mark-3/commit/fdcda0239aab54951df1c708dee7e74989801c79))
27
+ * **helpers:** add new types/is-set method ([618abbe](https://github.com/ismailceylan/mark-3/commit/618abbeb6f068774dfa4c6158302fd7ac5f7fccc))
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * **helpers:** ensures that the default argument value is used for chars on string/trim method ([645f41e](https://github.com/ismailceylan/mark-3/commit/645f41e883b8777a16e4773ed520411e9953a45b))
33
+
5
34
  ### [0.0.3](https://github.com/ismailceylan/mark-3/compare/v0.0.2...v0.0.3) (2025-08-02)
6
35
 
7
36
 
@@ -0,0 +1 @@
1
+ export { default as ScrollAwareContent } from "./scroll-aware-content.vue";
@@ -0,0 +1,162 @@
1
+ <template>
2
+ <!-- mark-3/components/scroll-aware-content -->
3
+ <section ref="rootEl">
4
+ <slot
5
+ v-bind="
6
+ {
7
+ observe, biggestScrollTop, biggestScrollLeft,
8
+ latestScrollTop, latestScrollLeft
9
+ }"
10
+ />
11
+ </section>
12
+ </template>
13
+
14
+ <script setup>
15
+ import { ref, onMounted, onUnmounted } from "vue";
16
+ import { debounce } from "../helpers/time";
17
+ import { useIntersectionObserver, useScrollEvent } from "../composables";
18
+
19
+ const rootEl = ref( null );
20
+ const targetEl = ref( null );
21
+ const emit = defineEmits([ "scroll", "metrics" ]);
22
+
23
+ let biggestScrollTop = 0;
24
+ let latestScrollTop = 0;
25
+ let biggestScrollLeft = 0;
26
+ let latestScrollLeft = 0;
27
+
28
+ defineExpose({ scrollTo, reset });
29
+
30
+ const { factor, scrollable, delay, intersect } = defineProps(
31
+ {
32
+ scrollable: [ String, HTMLElement, Document ],
33
+ intersect: Function,
34
+ delay:
35
+ {
36
+ type: Number,
37
+ default: 50
38
+ },
39
+ factor:
40
+ {
41
+ type: Number,
42
+ default: 1.2
43
+ }
44
+ });
45
+
46
+ const onScrollThrottled = debounce( onScroll, delay );
47
+ const { observe } = useIntersectionObserver( targetEl,
48
+ {
49
+ intersect,
50
+ rootMargin: "-0px",
51
+ threshold: [ 0, 1, .6 ]
52
+ });
53
+
54
+ onMounted(() =>
55
+ {
56
+ targetEl.value = (() =>
57
+ {
58
+ if( typeof scrollable == "string" )
59
+ {
60
+ if( scrollable == "html" )
61
+ {
62
+ return document;
63
+ }
64
+
65
+ if( scrollable == "body" )
66
+ {
67
+ return document.body;
68
+ }
69
+
70
+ if( scrollable[ 0 ] == "#" )
71
+ {
72
+ return document.querySelector( scrollable );
73
+ }
74
+
75
+ return rootEl.value.querySelector( scrollable );
76
+ }
77
+ else
78
+ {
79
+ return scrollable || rootEl.value;
80
+ }
81
+ })();
82
+
83
+ targetEl.value.scrollTop = latestScrollTop;
84
+ targetEl.value.scrollLeft = latestScrollLeft;
85
+
86
+ targetEl.value.addEventListener( "scroll", onScrollThrottled,
87
+ {
88
+ passive: true
89
+ });
90
+
91
+ });
92
+
93
+ onUnmounted(() =>
94
+ targetEl.value.removeEventListener( "scroll", onScrollThrottled )
95
+ );
96
+
97
+ function scrollTo({ top = 0, left = 0 })
98
+ {
99
+ targetEl.value.scrollTo({ top, left });
100
+ latestScrollTop = biggestScrollTop = top;
101
+ latestScrollLeft = biggestScrollLeft = left;
102
+ }
103
+
104
+ function reset()
105
+ {
106
+ biggestScrollTop = 0;
107
+ latestScrollTop = 0;
108
+ biggestScrollLeft = 0;
109
+ latestScrollLeft = 0;
110
+ }
111
+
112
+ function onScroll( evt )
113
+ {
114
+ const metrics = useScrollEvent( evt,
115
+ {
116
+ latestScrollTop, latestScrollLeft,
117
+ biggestScrollLeft, biggestScrollTop
118
+ });
119
+
120
+ const { direction, scrollable, visible, maxScrolled, scrolled } = metrics;
121
+
122
+ // if user scroll to one way and scroll back to starting point
123
+ // immediately, direction variable will be null which that means
124
+ // there was no scrolling at all
125
+ if( ! direction )
126
+ {
127
+ return;
128
+ }
129
+
130
+ if( direction == "vertical" )
131
+ {
132
+ latestScrollTop = scrolled;
133
+
134
+ if( scrolled > biggestScrollTop )
135
+ {
136
+ biggestScrollTop = scrolled;
137
+ }
138
+ }
139
+ else if( direction == "horizontal" )
140
+ {
141
+ latestScrollLeft = scrolled;
142
+
143
+ if( scrolled > biggestScrollLeft )
144
+ {
145
+ biggestScrollLeft = scrolled;
146
+ }
147
+ }
148
+
149
+ emit( "metrics",
150
+ {
151
+ biggestScrollLeft, biggestScrollTop,
152
+ latestScrollLeft, latestScrollTop,
153
+ ...metrics
154
+ });
155
+
156
+ if( scrollable - visible - maxScrolled < visible * factor )
157
+ {
158
+ emit( "scroll", metrics );
159
+ }
160
+ }
161
+
162
+ </script>
@@ -1,2 +1,5 @@
1
- export { default as useEventListener } from "./use-event-listener.js";
2
1
  export { default as useMediaQuery } from "./use-media-query.js";
2
+ export { default as useScrollEvent } from "./use-scroll-event.js";
3
+ export { default as useEventListener } from "./use-event-listener.js";
4
+ export { default as useResponsiveness } from "./use-responsiveness.js";
5
+ export { default as useIntersectionObserver } from "./use-intersection-observer.js";
@@ -0,0 +1,67 @@
1
+ import { unref, onMounted } from "vue";
2
+
3
+ /**
4
+ * A composition function that creates an IntersectionObserver instance and
5
+ * provides a method to observe the provided elements.
6
+ *
7
+ * @param {Ref<Element>|Element} maybeRefSourceElement - The source element
8
+ * that the observer will observe for intersection.
9
+ * @param {Object} [options] - The options to pass to the IntersectionObserver
10
+ * constructor.
11
+ * @returns {{ observe: (el: Element) => void, observer: IntersectionObserver }}
12
+ * A object with two properties. The `observe` property is a function that
13
+ * takes an element to observe and the `observer` property is the created
14
+ * IntersectionObserver instance.
15
+ */
16
+ export default function useIntersectionObserver( maybeRefSourceElement, options = {})
17
+ {
18
+ let observer;
19
+
20
+ onMounted( init );
21
+
22
+ function init()
23
+ {
24
+ options.source = unref( maybeRefSourceElement );
25
+ observer = new IntersectionObserver( handleObservations, options );
26
+ }
27
+
28
+ function handleObservations( entries )
29
+ {
30
+ requestAnimationFrame(() =>
31
+ {
32
+ for( const { target: { item }, isIntersecting } of entries )
33
+ {
34
+ if( options.intersect )
35
+ {
36
+ options.intersect( item, isIntersecting );
37
+ }
38
+ else
39
+ {
40
+ item.isIntersecting = isIntersecting;
41
+ }
42
+ }
43
+ });
44
+ }
45
+
46
+ function observe( item )
47
+ {
48
+ return el =>
49
+ {
50
+ if( ! el )
51
+ {
52
+ return;
53
+ }
54
+
55
+ el.item = item;
56
+
57
+ if( ! observer )
58
+ {
59
+ init();
60
+ }
61
+
62
+ observer && observer.observe( el );
63
+ }
64
+ }
65
+
66
+ return { observe, observer }
67
+ }
@@ -0,0 +1,61 @@
1
+ import { reactive, toRefs } from "vue";
2
+ import { camelToDash } from "../helpers/string";
3
+ import { useMediaQuery } from ".";
4
+
5
+ /**
6
+ * Reactive media query states for multiple breakpoints at once using
7
+ * useMediaQuery composable under the hood and returns an object of
8
+ * reactive media query states.
9
+ *
10
+ * The object keys are the same as the keys of the rules object.
11
+ *
12
+ * If the media queries you pass to the useResponsiveness composable
13
+ * are min- prefixed, the relevant props of returned object will be
14
+ * true if the screen is at least or more in that breakpoint. But if
15
+ * the queries are min- and max- prefixed, the relevant props of the
16
+ * reactive object that returned will be true if the screen is exactly
17
+ * in that breakpoint.
18
+ *
19
+ * You can pass any valid media query to the useResponsiveness composable
20
+ * like color schemes, display modes, and more.
21
+ *
22
+ * @typedef {import('vue').Ref} Ref
23
+ * @typedef {import('vue').Reactive} Reactive
24
+ * @param {Object} rules Object of media queries
25
+ * @returns {Reactive<{}>|Ref<boolean>} Object of reactive media query states
26
+ * @example
27
+ * const breakpoints = useResponsiveness({
28
+ * mobile: "(max-width: 767px)",
29
+ * tablet: "(min-width: 768px) and (max-width: 1023px)",
30
+ * desktop: "(min-width: 1024px)",
31
+ * });
32
+ * // { mobile: false, tablet: false, desktop: true}
33
+ * // every property tells the screen is exactly in that breakpoint
34
+ * @example
35
+ * const breakpoints = useResponsiveness({
36
+ * mobile: "(min-width: 300px)",
37
+ * tablet: "(min-width: 768px)",
38
+ * desktop: "(min-width: 1024px)",
39
+ * });
40
+ * // { mobile: true, tablet: true, desktop: false}
41
+ * // every property tells the screen is at least or more in that breakpoint
42
+ */
43
+ export default function useResponsiveness( rules = {})
44
+ {
45
+ const breakpoints = reactive({});
46
+ const keys = Object.keys( rules );
47
+
48
+ for( const name in rules )
49
+ {
50
+ useMediaQuery( rules[ name ], isActive =>
51
+ breakpoints[ camelToDash( name )] = isActive
52
+ );
53
+ }
54
+
55
+ if( keys.length === 1 )
56
+ {
57
+ return toRefs( breakpoints )[ keys[ 0 ]];
58
+ }
59
+
60
+ return breakpoints;
61
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @typedef ScrollMetrics
3
+ * @type {object}
4
+ * @property {number} maxScrolled the furthest point reached by scrolling
5
+ * @property {"vertical"|"horizontal"} direction scroll direction
6
+ * @property {number} seen seen pixels
7
+ * @property {number} unseen unseen pixels
8
+ * @property {number} visible the area covered by the scrollable content
9
+ * on the screen
10
+ * @property {number} scrolled currently scrolled pixels
11
+ * @property {number} scrollable total scrollable pixels
12
+ * @property {"up"|"down"} vertical whether the vertical direction is
13
+ * towards the up or the down
14
+ * @property {"left"|"right"} horizontal whether the horizontal direction
15
+ * is towards the left or the right
16
+ */
17
+ /**
18
+ * This function takes a scroll event and calculates the scrolling
19
+ * direction, along with metrics for visible, scrollable, and
20
+ * unseen areas.
21
+ *
22
+ * The direction is determined as vertical or horizontal, and returned
23
+ * along with relevant metrics.
24
+ *
25
+ * @param {Event} evt scroll event object
26
+ * @param {object} metrics
27
+ * @property {number} metrics.latestScrollTop latest vertically scrolled pixel
28
+ * @property {number} metrics.latestScrollLeft latest horizontally scrolled pixel
29
+ * @property {number} metrics.biggestScrollLeft biggest horizontally scrolled pixel
30
+ * @property {number} metrics.biggestScrollTop biggest vertically scrolled pixel
31
+ * @return {ScrollMetrics}
32
+ */
33
+ export default function useScrollEvent(
34
+ { target },
35
+ { latestScrollTop = 0, latestScrollLeft = 0, biggestScrollLeft = 0, biggestScrollTop = 0 } = {}
36
+ )
37
+ {
38
+ target = target === document
39
+ ? document.scrollingElement
40
+ : target;
41
+
42
+ const { clientHeight, clientWidth, scrollTop, scrollLeft, scrollHeight, scrollWidth } = target;
43
+ const vertical = calcDirection( "down", null, "up", [ scrollTop, latestScrollTop ]);
44
+ const horizontal = calcDirection( "right", null, "left", [ scrollLeft, latestScrollLeft ]);
45
+ const direction = ( vertical && "vertical" ) || ( horizontal && "horizontal" );
46
+ let visible = 0, scrollable = 0, scrolled = 0;
47
+ let seen = 0, unseen = 0, maxScrolled = 0;
48
+
49
+ if( direction == "vertical" )
50
+ {
51
+ visible = clientHeight;
52
+ scrollable = scrollHeight;
53
+ scrolled = scrollTop;
54
+ maxScrolled = biggestScrollTop;
55
+ }
56
+ else if( direction == "horizontal" )
57
+ {
58
+ visible = clientWidth;
59
+ scrollable = scrollWidth;
60
+ scrolled = scrollLeft;
61
+ maxScrolled = biggestScrollLeft;
62
+ }
63
+
64
+ seen = scrolled + visible;
65
+ unseen = scrollable - seen;
66
+ maxScrolled = Math.max( maxScrolled, scrolled );
67
+
68
+ function calcDirection( a, middle, b, [ v1, v2 ])
69
+ {
70
+ if( v1 === v2 )
71
+ {
72
+ return middle;
73
+ }
74
+
75
+ if( v2 > v1 )
76
+ {
77
+ return b;
78
+ }
79
+
80
+ if( v2 < v1 )
81
+ {
82
+ return a;
83
+ }
84
+ }
85
+
86
+ return {
87
+ maxScrolled,
88
+ direction,
89
+ seen,
90
+ unseen,
91
+ visible,
92
+ scrolled,
93
+ scrollable,
94
+ vertical,
95
+ horizontal
96
+ }
97
+ }
@@ -0,0 +1 @@
1
+ export { default as pickRandom } from "./pick-random";
@@ -0,0 +1,24 @@
1
+ import { random } from "../number";
2
+ import { isArray } from "../types";
3
+
4
+ /**
5
+ * Picks a random element from the given array.
6
+ *
7
+ * @template T
8
+ * @param {T[]} arr - The array to pick a random element from.
9
+ * @returns {T|undefined} - The random element from the array, or undefined if the array is empty.
10
+ */
11
+ export default function pickRandom( arr )
12
+ {
13
+ if( ! isArray( arr ))
14
+ {
15
+ return undefined;
16
+ }
17
+
18
+ if( arr.length === 0 )
19
+ {
20
+ return undefined;
21
+ }
22
+
23
+ return arr[ random( 0, arr.length - 1 )];
24
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { pickRandom } from "..";
3
+
4
+ // örnek test verisi
5
+ const sampleArray = [10, 20, 30, 40, 50];
6
+
7
+ describe("pickRandom", () =>
8
+ {
9
+ it("should return undefined if input is not an array", () =>
10
+ {
11
+ expect(pickRandom(null)).toBeUndefined();
12
+ expect(pickRandom({})).toBeUndefined();
13
+ expect(pickRandom("not array")).toBeUndefined();
14
+ expect(pickRandom(123)).toBeUndefined();
15
+ });
16
+
17
+ it("should return undefined if array is empty", () =>
18
+ {
19
+ expect(pickRandom([])).toBeUndefined();
20
+ });
21
+
22
+ it("should return an element from the array", () =>
23
+ {
24
+ const value = pickRandom(sampleArray);
25
+ expect(sampleArray).toContain(value);
26
+ });
27
+
28
+ it("should return different results across multiple calls", () =>
29
+ {
30
+ const results = new Set();
31
+ for (let i = 0; i < 20; i++)
32
+ {
33
+ const val = pickRandom(sampleArray);
34
+ results.add(val);
35
+ }
36
+ // Rastgelelik garantisi yok ama çeşitlilik kontrolü yapılabilir
37
+ expect(results.size).toBeGreaterThan(1);
38
+ });
39
+ });
@@ -1,4 +1,4 @@
1
1
  export { default as format } from "./format";
2
2
  export { default as isOnSameDay } from "./is-on-same-day";
3
- export { default as fromTimestamp } from "./from-timestamp";
3
+ export { default as fromTimestamp } from "./format-timestamp";
4
4
  export { default as isOnSameYear } from "./is-on-same-year";
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import fromTimestamp from "./index";
2
+ import { fromTimestamp } from "..";
3
3
 
4
4
  describe( "fromTimestamp", () =>
5
5
  {
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { format } from "..";
3
+
4
+ describe( "format", () =>
5
+ {
6
+ const date = new Date( "2025-12-21T22:10:05" );
7
+
8
+ it( "formats full year (Y)", () =>
9
+ {
10
+ expect( format( date, "Y" )).toBe( "2025" );
11
+ });
12
+
13
+ it( "formats short year (y)", () =>
14
+ {
15
+ expect( format( date, "y" )).toBe( "25" );
16
+ });
17
+
18
+ it( "formats zero-padded month (m)", () =>
19
+ {
20
+ expect( format( date, "m" )).toBe( "12" );
21
+ });
22
+
23
+ it( "formats non-padded month (n)", () =>
24
+ {
25
+ expect( format( date, "n" )).toBe( "12" );
26
+ });
27
+
28
+ it( "formats short month name (M)", () =>
29
+ {
30
+ expect( format( date, "M" )).toBe( "Dec" );
31
+ });
32
+
33
+ it( "formats full month name (MM)", () =>
34
+ {
35
+ expect( format( date, "MM" )).toBe( "December" );
36
+ });
37
+
38
+ it( "formats day padded (d)", () =>
39
+ {
40
+ expect( format( date, "d" )).toBe( "21" );
41
+ });
42
+
43
+ it( "formats day non-padded (j)", () =>
44
+ {
45
+ expect( format( date, "j" )).toBe( "21" );
46
+ });
47
+
48
+ it( "formats zero-padded hour (H)", () =>
49
+ {
50
+ expect( format( date, "H" )).toBe( "22" );
51
+ });
52
+
53
+ it( "formats non-padded hour (G)", () =>
54
+ {
55
+ expect( format( date, "G" )).toBe( "22" );
56
+ });
57
+
58
+ it( "formats minutes (i)", () =>
59
+ {
60
+ expect( format( date, "i" )).toBe( "10" );
61
+ });
62
+
63
+ it( "formats seconds (s)", () =>
64
+ {
65
+ expect( format( date, "s" )).toBe( "05" );
66
+ });
67
+
68
+ it( "formats a custom pattern", () =>
69
+ {
70
+ expect( format( date, "d.m.Y H:i" )).toBe( "21.12.2025 22:10" );
71
+ });
72
+
73
+ it( "supports localized short month (M) in TR", () =>
74
+ {
75
+ expect( format( date, "M", "tr" )).toBe( "Ara" ); // Aralık
76
+ });
77
+
78
+ it( "supports localized long month (MM) in TR", () =>
79
+ {
80
+ expect( format( date, "MM", "tr" )).toBe( "Aralık" );
81
+ });
82
+
83
+ it( "returns empty string for empty pattern", () =>
84
+ {
85
+ expect( format( date, "" )).toBe( "" );
86
+ });
87
+
88
+ it( "leaves unknown tokens as-is", () =>
89
+ {
90
+ expect( format( date, "[foo] Y" )).toBe( "[foo] 2025" );
91
+ });
92
+
93
+ it( "mixes plain text and tokens", () =>
94
+ {
95
+ expect( format( date, "d MM Y, H:i" )).toBe( "21 December 2025, 22:10" );
96
+ });
97
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import isOnSameDay from "./index";
2
+ import { isOnSameDay } from "..";
3
3
 
4
4
  describe( "isOnSameDay", () =>
5
5
  {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import isOnSameYear from "./index";
2
+ import { isOnSameYear } from "..";
3
3
 
4
4
  describe( "isOnSameYear", () =>
5
5
  {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Clamps a value within a range.
3
+ *
4
+ * @param {number} value value to be clamped
5
+ * @param {number} min minimum value
6
+ * @param {number} max maximum value
7
+ * @return {number} clamped value
8
+ */
9
+ export default function clamp( value, min, max )
10
+ {
11
+ if( min > max )
12
+ {
13
+ [ min, max ] = [ max, min ];
14
+ }
15
+
16
+ return Math.max( min, Math.min( max, value ));
17
+ }
@@ -0,0 +1,2 @@
1
+ export { default as clamp } from "./clamp";
2
+ export { default as random } from "./random";
@@ -0,0 +1,24 @@
1
+ import { clamp } from ".";
2
+
3
+ /**
4
+ * Produce a random number between two values. If no value is provided,
5
+ * a random number is produced.
6
+ *
7
+ * @param {number} min minimum value
8
+ * @param {number} max maximum value
9
+ * @param {boolean} shouldFloat should the result be a float
10
+ * @return {number}
11
+ */
12
+ export default function random( min = 0, max = 1, shouldFloat = false )
13
+ {
14
+ if( min > max )
15
+ {
16
+ [ min, max ] = [ max, min ];
17
+ }
18
+
19
+ const rnd = clamp( Math.random() * max, min, max );
20
+
21
+ return shouldFloat
22
+ ? parseFloat( rnd )
23
+ : Math.floor( rnd );
24
+ }