@studiocubics/utils 0.0.1
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/.turbo/turbo-build.log +17 -0
- package/CHANGELOG.md +7 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +347 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/src/API/_index.ts +1 -0
- package/src/API/apiRes.ts +74 -0
- package/src/Calculations/_index.ts +1 -0
- package/src/Calculations/calculateSafePosition.ts +137 -0
- package/src/Code/_index.ts +1 -0
- package/src/Code/delay.ts +5 -0
- package/src/Dates/_index.ts +2 -0
- package/src/Dates/formateDate.ts +140 -0
- package/src/Dates/relativeTime.ts +64 -0
- package/src/Forms/_index.ts +1 -0
- package/src/Forms/initialiseForm.ts +14 -0
- package/src/Numbers/_index.ts +1 -0
- package/src/Numbers/remap.ts +28 -0
- package/src/React/_index.ts +1 -0
- package/src/React/mergeRefs.ts +16 -0
- package/src/Strings/_index.ts +3 -0
- package/src/Strings/cn.ts +3 -0
- package/src/Strings/cssSafeString.ts +13 -0
- package/src/Strings/stringCases.ts +50 -0
- package/src/index.ts +8 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +9 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
> @studiocubics/utils@0.0.1 build /home/runner/work/StudioCubics/StudioCubics/packages/utils
|
|
3
|
+
> tsup
|
|
4
|
+
|
|
5
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
+
[34mCLI[39m tsup v8.5.1
|
|
8
|
+
[34mCLI[39m Using tsup config: /home/runner/work/StudioCubics/StudioCubics/packages/utils/tsup.config.ts
|
|
9
|
+
[34mCLI[39m Target: es2022
|
|
10
|
+
[34mCLI[39m Cleaning output folder
|
|
11
|
+
[34mCJS[39m Build start
|
|
12
|
+
[32mCJS[39m [1mdist/index.js [22m[32m9.75 KB[39m
|
|
13
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m19.24 KB[39m
|
|
14
|
+
[32mCJS[39m ⚡️ Build success in 343ms
|
|
15
|
+
[34mDTS[39m Build start
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 2850ms
|
|
17
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m5.81 KB[39m
|
package/CHANGELOG.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Ref } from 'react';
|
|
2
|
+
|
|
3
|
+
type ActionResponse<K extends string = string, E extends K[] = K[]> = {
|
|
4
|
+
success: boolean;
|
|
5
|
+
payload?: unknown;
|
|
6
|
+
error?: string;
|
|
7
|
+
fieldErrors?: Record<K, E>;
|
|
8
|
+
};
|
|
9
|
+
declare const apiRes: {
|
|
10
|
+
actionFail: <K extends string = string>(error: ActionResponse<K>["error"], fieldErrors?: Record<string, string[]>) => ActionResponse<K>;
|
|
11
|
+
actionSuccess: <K extends string = string>(payload?: unknown) => ActionResponse<K>;
|
|
12
|
+
/**
|
|
13
|
+
* "Failed to fetch data!"
|
|
14
|
+
*/
|
|
15
|
+
apiError: (message?: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* "You are not authenticated to access this resource."
|
|
18
|
+
*/
|
|
19
|
+
unauthorised: string;
|
|
20
|
+
/**
|
|
21
|
+
* "You do not have the necessary permissions to access this resource."
|
|
22
|
+
*/
|
|
23
|
+
forbidden: string;
|
|
24
|
+
/**
|
|
25
|
+
* "${item} is of wrong type, expected type: ${expectedType}"
|
|
26
|
+
*/
|
|
27
|
+
wrongType: (item: string, expectedType?: string) => string;
|
|
28
|
+
/**
|
|
29
|
+
* "An error occured trying to read the formdata in these fields:
|
|
30
|
+
* ${fields}."
|
|
31
|
+
*/
|
|
32
|
+
formdataError: (fields: string | string[]) => string;
|
|
33
|
+
/**
|
|
34
|
+
* "Missing parameter: ${param}."
|
|
35
|
+
*/
|
|
36
|
+
missingParams: (param: string | string[]) => string;
|
|
37
|
+
/**
|
|
38
|
+
* "${item} not found!"
|
|
39
|
+
*/
|
|
40
|
+
notFound: (item: string) => string;
|
|
41
|
+
/**
|
|
42
|
+
* "${item} already exists!"
|
|
43
|
+
*/
|
|
44
|
+
alreadyExists: (item: string) => string;
|
|
45
|
+
/**
|
|
46
|
+
* "${contentName} size exceeded the maximum of ${maxSize}"
|
|
47
|
+
*/
|
|
48
|
+
contentTooLarge: (contentName: string, maxSize?: string) => string;
|
|
49
|
+
/**
|
|
50
|
+
* An API Error has occurred:
|
|
51
|
+
* Check here => ${location}
|
|
52
|
+
* More Info: ${errorMessage}
|
|
53
|
+
*/
|
|
54
|
+
generalError: (location: string, errorMessage?: string) => string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
interface SafePositionOptions {
|
|
58
|
+
anchorOrigin?: string;
|
|
59
|
+
transformOrigin?: string;
|
|
60
|
+
margin?: number;
|
|
61
|
+
}
|
|
62
|
+
interface Position {
|
|
63
|
+
x: number;
|
|
64
|
+
y: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Calculates safe position of an element within viewport relative to an anchor element
|
|
68
|
+
* Works like Material UI Popover positioning:
|
|
69
|
+
* - anchorOrigin: point on the anchor element (e.g., "top center" = top-center of anchor)
|
|
70
|
+
* - transformOrigin: point on the positioned element that attaches to anchor (e.g., "bottom center" = bottom-center of popup attaches to anchor point)
|
|
71
|
+
*
|
|
72
|
+
* Example: anchorOrigin "top center" + transformOrigin "bottom center" positions popup above and centered
|
|
73
|
+
*
|
|
74
|
+
* @param element - The element to position
|
|
75
|
+
* @param anchorElement - The anchor element to position relative to
|
|
76
|
+
* @param options - Configuration options
|
|
77
|
+
* @returns Safe x, y coordinates
|
|
78
|
+
*/
|
|
79
|
+
declare function calculateSafePosition(element: HTMLElement | null, anchorElement: HTMLElement | null, options?: SafePositionOptions): Position;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Delays the program loop for the given amount of milliseconds
|
|
83
|
+
*/
|
|
84
|
+
declare const delay: (ms: number) => Promise<unknown>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns a human-readable relative time string vs Date.now().
|
|
88
|
+
* Accepts Date | number (ms) | date-string.
|
|
89
|
+
* Fast, allocation-light, no Intl.
|
|
90
|
+
*/
|
|
91
|
+
declare function relativeTime(input: Date | number | string): string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Formats a date according to the provided format string.
|
|
95
|
+
*
|
|
96
|
+
* @param dateInput - The date to format (as Date object, timestamp, or date string)
|
|
97
|
+
* @param format - The format string using tokens (default: "day D MMM, YYYY")
|
|
98
|
+
*
|
|
99
|
+
* @returns The formatted date string
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* formatDate(new Date(), "Day, DD MMMM YYYY")
|
|
103
|
+
* // => "Thursday, 02 January 2026"
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* formatDate(new Date(), "DD/MM/YYYY HH:mm:ss")
|
|
107
|
+
* // => "02/01/2026 14:30:45"
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* formatDate(new Date(), "h:mm A")
|
|
111
|
+
* // => "2:30 PM"
|
|
112
|
+
*
|
|
113
|
+
* Available tokens:
|
|
114
|
+
* - Day: Full day name (e.g., "Monday")
|
|
115
|
+
* - day: Short day name (e.g., "Mon")
|
|
116
|
+
* - DD: Day of month, zero-padded (e.g., "05")
|
|
117
|
+
* - D: Day of month (e.g., "5")
|
|
118
|
+
* - MMMM: Full month name (e.g., "January")
|
|
119
|
+
* - MMM: Short month name (e.g., "Jan")
|
|
120
|
+
* - MM: Month number, zero-padded (e.g., "01")
|
|
121
|
+
* - YYYY: Full year (e.g., "2026")
|
|
122
|
+
* - YY: Two-digit year (e.g., "26")
|
|
123
|
+
* - HH: Hours (24-hour), zero-padded (e.g., "14")
|
|
124
|
+
* - H: Hours (24-hour) (e.g., "14")
|
|
125
|
+
* - hh: Hours (12-hour), zero-padded (e.g., "02")
|
|
126
|
+
* - h: Hours (12-hour) (e.g., "2")
|
|
127
|
+
* - mm: Minutes, zero-padded (e.g., "05")
|
|
128
|
+
* - m: Minutes (e.g., "5")
|
|
129
|
+
* - ss: Seconds, zero-padded (e.g., "09")
|
|
130
|
+
* - s: Seconds (e.g., "9")
|
|
131
|
+
* - A: AM/PM uppercase (e.g., "PM")
|
|
132
|
+
* - a: am/pm lowercase (e.g., "pm")
|
|
133
|
+
*/
|
|
134
|
+
declare function formatDate(dateInput: number | string | Date, format?: string): string;
|
|
135
|
+
|
|
136
|
+
declare function initialiseForm<T extends string = string>(...fieldNames: T[]): ActionResponse<T, T[]>;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* This function returns the rounded corresponding value in the target range using linear interpolation.
|
|
140
|
+
*
|
|
141
|
+
* @param value - The input value to remap.
|
|
142
|
+
* @param from - The source range as a tuple [min, max].
|
|
143
|
+
* @param to - The target range as a tuple [min, max].
|
|
144
|
+
* @returns The remapped value in the target range.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* remap(3, [1, 10], [1, 5]); // 2
|
|
148
|
+
*/
|
|
149
|
+
declare function remap(value: number, from: [number, number], to: [number, number], roundTo?: "floor" | "ceil"): number;
|
|
150
|
+
|
|
151
|
+
declare function mergeRefs<T>(...refs: (Ref<T> | undefined)[]): (instance: T | null) => void;
|
|
152
|
+
|
|
153
|
+
declare function cn(...classNames: (string | undefined)[]): string;
|
|
154
|
+
|
|
155
|
+
declare function toCapitalCase(str: string | undefined): string;
|
|
156
|
+
declare function toCamelCase(str: string | undefined): string;
|
|
157
|
+
declare function toKebabCase(str: string | undefined): string;
|
|
158
|
+
declare function toSnakeCase(str: string | undefined): string;
|
|
159
|
+
declare function toConstantCase(str: string | undefined): string;
|
|
160
|
+
declare function toCapitalised(str: string | undefined): string;
|
|
161
|
+
|
|
162
|
+
declare function cssSafeString(str: string): string;
|
|
163
|
+
|
|
164
|
+
export { type ActionResponse, type Position, type SafePositionOptions, apiRes, calculateSafePosition, cn, cssSafeString, delay, formatDate, initialiseForm, mergeRefs, relativeTime, remap, toCamelCase, toCapitalCase, toCapitalised, toConstantCase, toKebabCase, toSnakeCase };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/API/apiRes.ts
|
|
2
|
+
var apiRes = {
|
|
3
|
+
actionFail: (error, fieldErrors) => ({
|
|
4
|
+
success: false,
|
|
5
|
+
error,
|
|
6
|
+
fieldErrors
|
|
7
|
+
}),
|
|
8
|
+
actionSuccess: (payload) => ({
|
|
9
|
+
success: true,
|
|
10
|
+
payload
|
|
11
|
+
}),
|
|
12
|
+
/**
|
|
13
|
+
* "Failed to fetch data!"
|
|
14
|
+
*/
|
|
15
|
+
apiError: (message) => message ? message : "Failed to fetch data!",
|
|
16
|
+
/**
|
|
17
|
+
* "You are not authenticated to access this resource."
|
|
18
|
+
*/
|
|
19
|
+
unauthorised: "You are not authenticated to access this resource.",
|
|
20
|
+
/**
|
|
21
|
+
* "You do not have the necessary permissions to access this resource."
|
|
22
|
+
*/
|
|
23
|
+
forbidden: "You do not have the necessary permissions to access this resource.",
|
|
24
|
+
/**
|
|
25
|
+
* "${item} is of wrong type, expected type: ${expectedType}"
|
|
26
|
+
*/
|
|
27
|
+
wrongType: (item, expectedType) => `${item} is of wrong type${expectedType ? `, expected type: ${expectedType}` : ""}.`,
|
|
28
|
+
/**
|
|
29
|
+
* "An error occured trying to read the formdata in these fields:
|
|
30
|
+
* ${fields}."
|
|
31
|
+
*/
|
|
32
|
+
formdataError: (fields) => `An error occured trying to read the formdata in these fields:
|
|
33
|
+
${JSON.stringify(fields)}.`,
|
|
34
|
+
/**
|
|
35
|
+
* "Missing parameter: ${param}."
|
|
36
|
+
*/
|
|
37
|
+
missingParams: (param) => `Missing parameter: ${JSON.stringify(param)}.`,
|
|
38
|
+
/**
|
|
39
|
+
* "${item} not found!"
|
|
40
|
+
*/
|
|
41
|
+
notFound: (item) => `${item} not found!`,
|
|
42
|
+
/**
|
|
43
|
+
* "${item} already exists!"
|
|
44
|
+
*/
|
|
45
|
+
alreadyExists: (item) => `${item} already exists!`,
|
|
46
|
+
/**
|
|
47
|
+
* "${contentName} size exceeded the maximum of ${maxSize}"
|
|
48
|
+
*/
|
|
49
|
+
contentTooLarge: (contentName, maxSize) => `${contentName} size exceeded${maxSize ? ` the maximum of ${maxSize}` : ""}.`,
|
|
50
|
+
/**
|
|
51
|
+
* An API Error has occurred:
|
|
52
|
+
* Check here => ${location}
|
|
53
|
+
* More Info: ${errorMessage}
|
|
54
|
+
*/
|
|
55
|
+
generalError: (location, errorMessage = "") => `An API Error has occurred:
|
|
56
|
+
Check here => ${location}
|
|
57
|
+
More Info: ${errorMessage}`
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/Calculations/calculateSafePosition.ts
|
|
61
|
+
function calculateSafePosition(element, anchorElement, options = {}) {
|
|
62
|
+
const {
|
|
63
|
+
anchorOrigin = "top left",
|
|
64
|
+
transformOrigin = "top left",
|
|
65
|
+
margin = 0
|
|
66
|
+
} = options;
|
|
67
|
+
if (!element || !anchorElement) return { x: 0, y: 0 };
|
|
68
|
+
const parseOrigin = (origin) => {
|
|
69
|
+
const parts = origin.toLowerCase().trim().split(/\s+/);
|
|
70
|
+
let vertical = "top";
|
|
71
|
+
let horizontal = "left";
|
|
72
|
+
for (const part of parts) {
|
|
73
|
+
if (["top", "bottom"].includes(part)) vertical = part;
|
|
74
|
+
else if (["left", "right"].includes(part))
|
|
75
|
+
horizontal = part;
|
|
76
|
+
else if (part === "center") {
|
|
77
|
+
if (parts.length === 1) {
|
|
78
|
+
vertical = "center";
|
|
79
|
+
horizontal = "center";
|
|
80
|
+
} else if (["top", "bottom"].includes(parts[0])) horizontal = "center";
|
|
81
|
+
else vertical = "center";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { vertical, horizontal };
|
|
85
|
+
};
|
|
86
|
+
const getAnchorOffset = (size, origin) => {
|
|
87
|
+
if (origin === "center") return size / 2;
|
|
88
|
+
if (origin === "bottom" || origin === "right") return size;
|
|
89
|
+
return 0;
|
|
90
|
+
};
|
|
91
|
+
const getTransformOffset = (size, origin) => {
|
|
92
|
+
if (origin === "center") return size / 2;
|
|
93
|
+
if (origin === "bottom" || origin === "right") return size;
|
|
94
|
+
return 0;
|
|
95
|
+
};
|
|
96
|
+
const elementRect = element.getBoundingClientRect();
|
|
97
|
+
const anchorRect = anchorElement.getBoundingClientRect();
|
|
98
|
+
const viewportWidth = window.innerWidth;
|
|
99
|
+
const viewportHeight = window.innerHeight;
|
|
100
|
+
const anchor = parseOrigin(anchorOrigin);
|
|
101
|
+
const transform = parseOrigin(transformOrigin);
|
|
102
|
+
const anchorX = anchorRect.left + getAnchorOffset(anchorRect.width, anchor.horizontal);
|
|
103
|
+
const anchorY = anchorRect.top + getAnchorOffset(anchorRect.height, anchor.vertical);
|
|
104
|
+
const elementOriginX = getTransformOffset(
|
|
105
|
+
elementRect.width,
|
|
106
|
+
transform.horizontal
|
|
107
|
+
);
|
|
108
|
+
const elementOriginY = getTransformOffset(
|
|
109
|
+
elementRect.height,
|
|
110
|
+
transform.vertical
|
|
111
|
+
);
|
|
112
|
+
let x = anchorX - elementOriginX;
|
|
113
|
+
let y = anchorY - elementOriginY;
|
|
114
|
+
if (x + elementRect.width > viewportWidth - margin) {
|
|
115
|
+
x = viewportWidth - elementRect.width - margin;
|
|
116
|
+
}
|
|
117
|
+
if (x < margin) {
|
|
118
|
+
x = margin;
|
|
119
|
+
}
|
|
120
|
+
if (y + elementRect.height > viewportHeight - margin) {
|
|
121
|
+
y = viewportHeight - elementRect.height - margin;
|
|
122
|
+
}
|
|
123
|
+
if (y < margin) {
|
|
124
|
+
y = margin;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
x: Math.max(margin, x),
|
|
128
|
+
y: Math.max(margin, y)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/Code/delay.ts
|
|
133
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
134
|
+
|
|
135
|
+
// src/Dates/relativeTime.ts
|
|
136
|
+
function relativeTime(input) {
|
|
137
|
+
const t = input instanceof Date ? input.getTime() : typeof input === "number" ? input : Date.parse(input);
|
|
138
|
+
if (!Number.isFinite(t)) return "invalid date";
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
const diffMs = t - now;
|
|
141
|
+
const absMs = Math.abs(diffMs);
|
|
142
|
+
const isFuture = diffMs > 0;
|
|
143
|
+
const MIN = 6e4;
|
|
144
|
+
const HOUR = 36e5;
|
|
145
|
+
const DAY = 864e5;
|
|
146
|
+
const WEEK = 6048e5;
|
|
147
|
+
const YEAR = 31536e6;
|
|
148
|
+
if (absMs > 5 * YEAR) {
|
|
149
|
+
return isFuture ? "a long time from now" : "a long time ago";
|
|
150
|
+
}
|
|
151
|
+
if (absMs < MIN) {
|
|
152
|
+
return "just now";
|
|
153
|
+
}
|
|
154
|
+
let value;
|
|
155
|
+
let unit;
|
|
156
|
+
if (absMs < HOUR) {
|
|
157
|
+
value = Math.round(absMs / MIN);
|
|
158
|
+
unit = "minute";
|
|
159
|
+
} else if (absMs < DAY) {
|
|
160
|
+
value = Math.round(absMs / HOUR);
|
|
161
|
+
unit = "hour";
|
|
162
|
+
} else if (absMs < WEEK) {
|
|
163
|
+
value = Math.round(absMs / DAY);
|
|
164
|
+
unit = "day";
|
|
165
|
+
} else if (absMs < YEAR) {
|
|
166
|
+
value = Math.round(absMs / WEEK);
|
|
167
|
+
unit = "week";
|
|
168
|
+
} else {
|
|
169
|
+
value = Math.round(absMs / YEAR);
|
|
170
|
+
unit = "year";
|
|
171
|
+
}
|
|
172
|
+
if (value !== 1) unit += "s";
|
|
173
|
+
return isFuture ? `in ${value} ${unit}` : `${value} ${unit} ago`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/Dates/formateDate.ts
|
|
177
|
+
function formatDate(dateInput, format = "HH:mm:ss day D MMM, YYYY") {
|
|
178
|
+
const date = new Date(dateInput);
|
|
179
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
180
|
+
const daysFull = [
|
|
181
|
+
"Sunday",
|
|
182
|
+
"Monday",
|
|
183
|
+
"Tuesday",
|
|
184
|
+
"Wednesday",
|
|
185
|
+
"Thursday",
|
|
186
|
+
"Friday",
|
|
187
|
+
"Saturday"
|
|
188
|
+
];
|
|
189
|
+
const daysShort = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
190
|
+
const monthsFull = [
|
|
191
|
+
"January",
|
|
192
|
+
"February",
|
|
193
|
+
"March",
|
|
194
|
+
"April",
|
|
195
|
+
"May",
|
|
196
|
+
"June",
|
|
197
|
+
"July",
|
|
198
|
+
"August",
|
|
199
|
+
"September",
|
|
200
|
+
"October",
|
|
201
|
+
"November",
|
|
202
|
+
"December"
|
|
203
|
+
];
|
|
204
|
+
const monthsShort = monthsFull.map((m) => m.slice(0, 3));
|
|
205
|
+
const dayIndex = date.getDay();
|
|
206
|
+
const dateNum = date.getDate();
|
|
207
|
+
const monthIndex = date.getMonth();
|
|
208
|
+
const year = date.getFullYear();
|
|
209
|
+
const hours24 = date.getHours();
|
|
210
|
+
const minutes = date.getMinutes();
|
|
211
|
+
const seconds = date.getSeconds();
|
|
212
|
+
const hours12 = hours24 % 12 || 12;
|
|
213
|
+
const isPM = hours24 >= 12;
|
|
214
|
+
const yearStr = String(year);
|
|
215
|
+
const replacements = {
|
|
216
|
+
Day: daysFull[dayIndex],
|
|
217
|
+
day: daysShort[dayIndex],
|
|
218
|
+
DD: pad(dateNum),
|
|
219
|
+
D: String(dateNum),
|
|
220
|
+
MMMM: monthsFull[monthIndex],
|
|
221
|
+
MMM: monthsShort[monthIndex],
|
|
222
|
+
MM: pad(monthIndex + 1),
|
|
223
|
+
YYYY: yearStr,
|
|
224
|
+
YY: yearStr.slice(-2),
|
|
225
|
+
HH: pad(hours24),
|
|
226
|
+
H: String(hours24),
|
|
227
|
+
hh: pad(hours12),
|
|
228
|
+
h: String(hours12),
|
|
229
|
+
mm: pad(minutes),
|
|
230
|
+
m: String(minutes),
|
|
231
|
+
ss: pad(seconds),
|
|
232
|
+
s: String(seconds),
|
|
233
|
+
A: isPM ? "PM" : "AM",
|
|
234
|
+
a: isPM ? "pm" : "am"
|
|
235
|
+
};
|
|
236
|
+
const tokens = Object.keys(replacements).sort(
|
|
237
|
+
(a, b) => b.length - a.length
|
|
238
|
+
);
|
|
239
|
+
const pattern = new RegExp(tokens.join("|"), "g");
|
|
240
|
+
return format.replace(pattern, (match) => replacements[match]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/Forms/initialiseForm.ts
|
|
244
|
+
function initialiseForm(...fieldNames) {
|
|
245
|
+
let fieldErrors = {};
|
|
246
|
+
for (const fieldName of fieldNames) {
|
|
247
|
+
fieldErrors[fieldName] = [];
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
fieldErrors
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/Numbers/remap.ts
|
|
256
|
+
function remap(value, from, to, roundTo = "floor") {
|
|
257
|
+
const [fromMin, fromMax] = from;
|
|
258
|
+
const [toMin, toMax] = to;
|
|
259
|
+
if (roundTo == "ceil")
|
|
260
|
+
return Math.ceil(
|
|
261
|
+
toMin + (value - fromMin) * (toMax - toMin) / (fromMax - fromMin)
|
|
262
|
+
);
|
|
263
|
+
return Math.floor(
|
|
264
|
+
toMin + (value - fromMin) * (toMax - toMin) / (fromMax - fromMin)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/React/mergeRefs.ts
|
|
269
|
+
function mergeRefs(...refs) {
|
|
270
|
+
return (instance) => {
|
|
271
|
+
for (const ref of refs) {
|
|
272
|
+
if (!ref) continue;
|
|
273
|
+
if (typeof ref === "function") {
|
|
274
|
+
ref(instance);
|
|
275
|
+
} else {
|
|
276
|
+
ref.current = instance;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/Strings/cn.ts
|
|
283
|
+
function cn(...classNames) {
|
|
284
|
+
return classNames.filter((c) => c).join(" ");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/Strings/stringCases.ts
|
|
288
|
+
function toWords(str) {
|
|
289
|
+
return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_\-]+/g, " ").trim().split(/\s+/).filter(Boolean).map((w) => w.toLowerCase());
|
|
290
|
+
}
|
|
291
|
+
function toCapitalCase(str) {
|
|
292
|
+
if (!str) return "";
|
|
293
|
+
const words = toWords(str);
|
|
294
|
+
return words.map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
295
|
+
}
|
|
296
|
+
function toCamelCase(str) {
|
|
297
|
+
if (!str) return "";
|
|
298
|
+
const words = toWords(str);
|
|
299
|
+
return words.map((w, i) => i === 0 ? w : w[0].toUpperCase() + w.slice(1)).join("");
|
|
300
|
+
}
|
|
301
|
+
function toKebabCase(str) {
|
|
302
|
+
if (!str) return "";
|
|
303
|
+
return toWords(str).join("-");
|
|
304
|
+
}
|
|
305
|
+
function toSnakeCase(str) {
|
|
306
|
+
if (!str) return "";
|
|
307
|
+
return toWords(str).join("_");
|
|
308
|
+
}
|
|
309
|
+
function toConstantCase(str) {
|
|
310
|
+
if (!str) return "";
|
|
311
|
+
return toWords(str).join("_").toUpperCase();
|
|
312
|
+
}
|
|
313
|
+
function toCapitalised(str) {
|
|
314
|
+
if (!str) return "";
|
|
315
|
+
const words = toWords(str);
|
|
316
|
+
if (words.length === 0) return "";
|
|
317
|
+
return words.map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/Strings/cssSafeString.ts
|
|
321
|
+
function cssSafeString(str) {
|
|
322
|
+
const encoded = encodeURIComponent(str).toLowerCase().replace(/\.|%[0-9a-z]{2}/gi, "");
|
|
323
|
+
const firstChar = encoded.charAt(0);
|
|
324
|
+
if (firstChar.match(/^[0-9-]/)) {
|
|
325
|
+
return "_" + encoded;
|
|
326
|
+
}
|
|
327
|
+
return encoded;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
exports.apiRes = apiRes; exports.calculateSafePosition = calculateSafePosition; exports.cn = cn; exports.cssSafeString = cssSafeString; exports.delay = delay; exports.formatDate = formatDate; exports.initialiseForm = initialiseForm; exports.mergeRefs = mergeRefs; exports.relativeTime = relativeTime; exports.remap = remap; exports.toCamelCase = toCamelCase; exports.toCapitalCase = toCapitalCase; exports.toCapitalised = toCapitalised; exports.toConstantCase = toConstantCase; exports.toKebabCase = toKebabCase; exports.toSnakeCase = toSnakeCase;
|
|
347
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/StudioCubics/StudioCubics/packages/utils/dist/index.js","../src/API/apiRes.ts","../src/Calculations/calculateSafePosition.ts","../src/Code/delay.ts","../src/Dates/relativeTime.ts","../src/Dates/formateDate.ts","../src/Forms/initialiseForm.ts","../src/Numbers/remap.ts","../src/React/mergeRefs.ts","../src/Strings/cn.ts","../src/Strings/stringCases.ts","../src/Strings/cssSafeString.ts"],"names":[],"mappings":"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","file":"/home/runner/work/StudioCubics/StudioCubics/packages/utils/dist/index.js","sourcesContent":[null,"export type ActionResponse<K extends string = string, E extends K[] = K[]> = {\n success: boolean;\n payload?: unknown;\n error?: string;\n fieldErrors?: Record<K, E>;\n};\n\nexport const apiRes = {\n actionFail: <K extends string = string>(\n error: ActionResponse<K>[\"error\"],\n fieldErrors?: Record<string, string[]>\n ): ActionResponse<K> => ({\n success: false,\n error,\n fieldErrors: fieldErrors as ActionResponse<K>[\"fieldErrors\"],\n }),\n actionSuccess: <K extends string = string>(\n payload?: unknown\n ): ActionResponse<K> => ({\n success: true,\n payload,\n }),\n /**\n * \"Failed to fetch data!\"\n */\n apiError: (message?: string) => (message ? message : \"Failed to fetch data!\"),\n /**\n * \"You are not authenticated to access this resource.\"\n */\n unauthorised: \"You are not authenticated to access this resource.\",\n /**\n * \"You do not have the necessary permissions to access this resource.\"\n */\n forbidden:\n \"You do not have the necessary permissions to access this resource.\",\n /**\n * \"${item} is of wrong type, expected type: ${expectedType}\"\n */\n wrongType: (item: string, expectedType?: string) =>\n `${item} is of wrong type${expectedType ? `, expected type: ${expectedType}` : \"\"}.`,\n /**\n * \"An error occured trying to read the formdata in these fields:\n * ${fields}.\"\n */\n formdataError: (fields: string | string[]) =>\n `An error occured trying to read the formdata in these fields:\\n${JSON.stringify(fields)}.`,\n /**\n * \"Missing parameter: ${param}.\"\n */\n missingParams: (param: string | string[]) =>\n `Missing parameter: ${JSON.stringify(param)}.`,\n /**\n * \"${item} not found!\"\n */\n notFound: (item: string) => `${item} not found!`,\n /**\n * \"${item} already exists!\"\n */\n alreadyExists: (item: string) => `${item} already exists!`,\n /**\n * \"${contentName} size exceeded the maximum of ${maxSize}\"\n */\n contentTooLarge: (contentName: string, maxSize?: string) =>\n `${contentName} size exceeded${\n maxSize ? ` the maximum of ${maxSize}` : \"\"\n }.`,\n /**\n * An API Error has occurred:\n * Check here => ${location}\n * More Info: ${errorMessage}\n */\n generalError: (location: string, errorMessage: string = \"\") =>\n `An API Error has occurred:\\nCheck here => ${location}\\nMore Info: ${errorMessage}`,\n};\n","type VerticalOrigin = \"top\" | \"center\" | \"bottom\";\ntype HorizontalOrigin = \"left\" | \"center\" | \"right\";\n\ninterface ParsedOrigin {\n vertical: VerticalOrigin;\n horizontal: HorizontalOrigin;\n}\n\nexport interface SafePositionOptions {\n anchorOrigin?: string;\n transformOrigin?: string;\n margin?: number;\n}\n\nexport interface Position {\n x: number;\n y: number;\n}\n\n/**\n * Calculates safe position of an element within viewport relative to an anchor element\n * Works like Material UI Popover positioning:\n * - anchorOrigin: point on the anchor element (e.g., \"top center\" = top-center of anchor)\n * - transformOrigin: point on the positioned element that attaches to anchor (e.g., \"bottom center\" = bottom-center of popup attaches to anchor point)\n *\n * Example: anchorOrigin \"top center\" + transformOrigin \"bottom center\" positions popup above and centered\n *\n * @param element - The element to position\n * @param anchorElement - The anchor element to position relative to\n * @param options - Configuration options\n * @returns Safe x, y coordinates\n */\nexport function calculateSafePosition(\n element: HTMLElement | null,\n anchorElement: HTMLElement | null,\n options: SafePositionOptions = {}\n): Position {\n const {\n anchorOrigin = \"top left\",\n transformOrigin = \"top left\",\n margin = 0,\n } = options;\n\n if (!element || !anchorElement) return { x: 0, y: 0 };\n\n const parseOrigin = (origin: string): ParsedOrigin => {\n const parts = origin.toLowerCase().trim().split(/\\s+/);\n let vertical: VerticalOrigin = \"top\";\n let horizontal: HorizontalOrigin = \"left\";\n\n for (const part of parts) {\n if ([\"top\", \"bottom\"].includes(part)) vertical = part as VerticalOrigin;\n else if ([\"left\", \"right\"].includes(part))\n horizontal = part as HorizontalOrigin;\n else if (part === \"center\") {\n // Assign center depending on position in string\n if (parts.length === 1) {\n vertical = \"center\";\n horizontal = \"center\";\n } else if ([\"top\", \"bottom\"].includes(parts[0])) horizontal = \"center\";\n else vertical = \"center\";\n }\n }\n\n return { vertical, horizontal };\n };\n\n const getAnchorOffset = (\n size: number,\n origin: VerticalOrigin | HorizontalOrigin\n ): number => {\n if (origin === \"center\") return size / 2;\n if (origin === \"bottom\" || origin === \"right\") return size;\n return 0; // top or left\n };\n\n const getTransformOffset = (\n size: number,\n origin: VerticalOrigin | HorizontalOrigin\n ): number => {\n if (origin === \"center\") return size / 2;\n if (origin === \"bottom\" || origin === \"right\") return size;\n return 0; // top or left\n };\n\n const elementRect = element.getBoundingClientRect();\n const anchorRect = anchorElement.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n\n const anchor = parseOrigin(anchorOrigin);\n const transform = parseOrigin(transformOrigin);\n\n // Calculate anchor point on the anchor element\n const anchorX =\n anchorRect.left + getAnchorOffset(anchorRect.width, anchor.horizontal);\n const anchorY =\n anchorRect.top + getAnchorOffset(anchorRect.height, anchor.vertical);\n\n // Calculate where on the element we're attaching (transform origin point)\n const elementOriginX = getTransformOffset(\n elementRect.width,\n transform.horizontal\n );\n const elementOriginY = getTransformOffset(\n elementRect.height,\n transform.vertical\n );\n\n // Position the element so its transform origin aligns with the anchor point\n let x = anchorX - elementOriginX;\n let y = anchorY - elementOriginY;\n\n // Check and adjust for viewport boundaries with margin\n // Right boundary\n if (x + elementRect.width > viewportWidth - margin) {\n x = viewportWidth - elementRect.width - margin;\n }\n // Left boundary\n if (x < margin) {\n x = margin;\n }\n\n // Bottom boundary\n if (y + elementRect.height > viewportHeight - margin) {\n y = viewportHeight - elementRect.height - margin;\n }\n // Top boundary\n if (y < margin) {\n y = margin;\n }\n\n return {\n x: Math.max(margin, x),\n y: Math.max(margin, y),\n };\n}\n","/**\n * Delays the program loop for the given amount of milliseconds\n */\nexport const delay = (ms: number) =>\n new Promise((resolve) => setTimeout(resolve, ms));\n","/**\n * Returns a human-readable relative time string vs Date.now().\n * Accepts Date | number (ms) | date-string.\n * Fast, allocation-light, no Intl.\n */\nexport function relativeTime(\n input: Date | number | string\n): string {\n const t: number =\n input instanceof Date\n ? input.getTime()\n : typeof input === \"number\"\n ? input\n : Date.parse(input);\n\n if (!Number.isFinite(t)) return \"invalid date\";\n\n const now = Date.now();\n const diffMs = t - now;\n const absMs = Math.abs(diffMs);\n const isFuture = diffMs > 0;\n\n // time constants (ms)\n const MIN = 60_000;\n const HOUR = 3_600_000;\n const DAY = 86_400_000;\n const WEEK = 604_800_000;\n const YEAR = 31_536_000_000;\n\n // very far away → coarse wording\n if (absMs > 5 * YEAR) {\n return isFuture ? \"a long time from now\" : \"a long time ago\";\n }\n\n if (absMs < MIN) {\n return \"just now\";\n }\n\n let value: number;\n let unit: \"minute\" | \"hour\" | \"day\" | \"week\" | \"year\";\n\n if (absMs < HOUR) {\n value = Math.round(absMs / MIN);\n unit = \"minute\";\n } else if (absMs < DAY) {\n value = Math.round(absMs / HOUR);\n unit = \"hour\";\n } else if (absMs < WEEK) {\n value = Math.round(absMs / DAY);\n unit = \"day\";\n } else if (absMs < YEAR) {\n value = Math.round(absMs / WEEK);\n unit = \"week\";\n } else {\n value = Math.round(absMs / YEAR);\n unit = \"year\";\n }\n\n if (value !== 1) unit += \"s\";\n\n return isFuture\n ? `in ${value} ${unit}`\n : `${value} ${unit} ago`;\n}\n","type DateToken =\n | \"Day\"\n | \"day\"\n | \"DD\"\n | \"D\"\n | \"MMMM\"\n | \"MMM\"\n | \"MM\"\n | \"YYYY\"\n | \"YY\"\n | \"HH\"\n | \"H\"\n | \"hh\"\n | \"h\"\n | \"mm\"\n | \"m\"\n | \"ss\"\n | \"s\"\n | \"A\"\n | \"a\";\n\n/**\n * Formats a date according to the provided format string.\n *\n * @param dateInput - The date to format (as Date object, timestamp, or date string)\n * @param format - The format string using tokens (default: \"day D MMM, YYYY\")\n *\n * @returns The formatted date string\n *\n * @example\n * formatDate(new Date(), \"Day, DD MMMM YYYY\")\n * // => \"Thursday, 02 January 2026\"\n *\n * @example\n * formatDate(new Date(), \"DD/MM/YYYY HH:mm:ss\")\n * // => \"02/01/2026 14:30:45\"\n *\n * @example\n * formatDate(new Date(), \"h:mm A\")\n * // => \"2:30 PM\"\n *\n * Available tokens:\n * - Day: Full day name (e.g., \"Monday\")\n * - day: Short day name (e.g., \"Mon\")\n * - DD: Day of month, zero-padded (e.g., \"05\")\n * - D: Day of month (e.g., \"5\")\n * - MMMM: Full month name (e.g., \"January\")\n * - MMM: Short month name (e.g., \"Jan\")\n * - MM: Month number, zero-padded (e.g., \"01\")\n * - YYYY: Full year (e.g., \"2026\")\n * - YY: Two-digit year (e.g., \"26\")\n * - HH: Hours (24-hour), zero-padded (e.g., \"14\")\n * - H: Hours (24-hour) (e.g., \"14\")\n * - hh: Hours (12-hour), zero-padded (e.g., \"02\")\n * - h: Hours (12-hour) (e.g., \"2\")\n * - mm: Minutes, zero-padded (e.g., \"05\")\n * - m: Minutes (e.g., \"5\")\n * - ss: Seconds, zero-padded (e.g., \"09\")\n * - s: Seconds (e.g., \"9\")\n * - A: AM/PM uppercase (e.g., \"PM\")\n * - a: am/pm lowercase (e.g., \"pm\")\n */\nexport function formatDate(\n dateInput: number | string | Date,\n format: string = \"HH:mm:ss day D MMM, YYYY\"\n) {\n const date = new Date(dateInput);\n\n const pad = (n: number) => String(n).padStart(2, \"0\");\n\n const daysFull = [\n \"Sunday\",\n \"Monday\",\n \"Tuesday\",\n \"Wednesday\",\n \"Thursday\",\n \"Friday\",\n \"Saturday\",\n ];\n const daysShort = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\n const monthsFull = [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n ];\n const monthsShort = monthsFull.map((m) => m.slice(0, 3));\n\n // Extract all values once\n const dayIndex = date.getDay();\n const dateNum = date.getDate();\n const monthIndex = date.getMonth();\n const year = date.getFullYear();\n const hours24 = date.getHours();\n const minutes = date.getMinutes();\n const seconds = date.getSeconds();\n\n const hours12 = hours24 % 12 || 12;\n const isPM = hours24 >= 12;\n const yearStr = String(year);\n\n const replacements: Record<DateToken, string> = {\n Day: daysFull[dayIndex],\n day: daysShort[dayIndex],\n DD: pad(dateNum),\n D: String(dateNum),\n MMMM: monthsFull[monthIndex],\n MMM: monthsShort[monthIndex],\n MM: pad(monthIndex + 1),\n YYYY: yearStr,\n YY: yearStr.slice(-2),\n HH: pad(hours24),\n H: String(hours24),\n hh: pad(hours12),\n h: String(hours12),\n mm: pad(minutes),\n m: String(minutes),\n ss: pad(seconds),\n s: String(seconds),\n A: isPM ? \"PM\" : \"AM\",\n a: isPM ? \"pm\" : \"am\",\n };\n\n // Sort tokens by length (longest first) to avoid partial replacements\n const tokens = (Object.keys(replacements) as DateToken[]).sort(\n (a, b) => b.length - a.length\n );\n\n // Use a single regex replacement to avoid overlapping matches\n const pattern = new RegExp(tokens.join(\"|\"), \"g\");\n return format.replace(pattern, (match) => replacements[match as DateToken]);\n}\n","import type { ActionResponse } from \"../API/apiRes\";\n\nexport function initialiseForm<T extends string = string>(\n ...fieldNames: T[]\n): ActionResponse<T, T[]> {\n let fieldErrors = {} as Record<T, T[]>;\n for (const fieldName of fieldNames) {\n fieldErrors[fieldName] = [] as T[];\n }\n return {\n success: false,\n fieldErrors,\n };\n}\n","/**\n * This function returns the rounded corresponding value in the target range using linear interpolation.\n *\n * @param value - The input value to remap.\n * @param from - The source range as a tuple [min, max].\n * @param to - The target range as a tuple [min, max].\n * @returns The remapped value in the target range.\n *\n * @example\n * remap(3, [1, 10], [1, 5]); // 2\n */\nexport function remap(\n value: number,\n from: [number, number],\n to: [number, number],\n roundTo: \"floor\" | \"ceil\" = \"floor\",\n): number {\n const [fromMin, fromMax] = from;\n const [toMin, toMax] = to;\n\n if (roundTo == \"ceil\")\n return Math.ceil(\n toMin + ((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin),\n );\n return Math.floor(\n toMin + ((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin),\n );\n}\n","import type { RefObject, Ref } from \"react\";\n\nexport function mergeRefs<T>(\n ...refs: (Ref<T> | undefined)[]\n): (instance: T | null) => void {\n return (instance) => {\n for (const ref of refs) {\n if (!ref) continue;\n if (typeof ref === \"function\") {\n ref(instance);\n } else {\n (ref as RefObject<T | null>).current = instance;\n }\n }\n };\n}\n","export function cn(...classNames: (string | undefined)[]) {\n return classNames.filter((c) => c).join(\" \");\n}\n","function toWords(str: string): string[] {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\") // camelCase → camel Case\n .replace(/[_\\-]+/g, \" \") // snake_case, kebab-case → spaces\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((w) => w.toLowerCase());\n}\n\nexport function toCapitalCase(str: string | undefined): string {\n if (!str) return \"\";\n const words = toWords(str);\n return words.map((w) => w[0].toUpperCase() + w.slice(1)).join(\"\");\n}\n\nexport function toCamelCase(str: string | undefined): string {\n if (!str) return \"\";\n\n const words = toWords(str);\n return words\n .map((w, i) => (i === 0 ? w : w[0].toUpperCase() + w.slice(1)))\n .join(\"\");\n}\n\nexport function toKebabCase(str: string | undefined): string {\n if (!str) return \"\";\n\n return toWords(str).join(\"-\");\n}\n\nexport function toSnakeCase(str: string | undefined): string {\n if (!str) return \"\";\n\n return toWords(str).join(\"_\");\n}\n\nexport function toConstantCase(str: string | undefined): string {\n if (!str) return \"\";\n\n return toWords(str).join(\"_\").toUpperCase();\n}\n\nexport function toCapitalised(str: string | undefined): string {\n if (!str) return \"\";\n\n const words = toWords(str);\n if (words.length === 0) return \"\";\n return words.map((w) => w[0].toUpperCase() + w.slice(1)).join(\" \");\n}\n","export function cssSafeString(str: string) {\n const encoded = encodeURIComponent(str)\n .toLowerCase()\n .replace(/\\.|%[0-9a-z]{2}/gi, \"\");\n\n const firstChar = encoded.charAt(0);\n if (firstChar.match(/^[0-9-]/)) {\n // Check if the first character is a number or hyphen\n return \"_\" + encoded;\n }\n\n return encoded;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@studiocubics/utils",
|
|
3
|
+
"description": "Package containing utility functions by Studio Cubics",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"private": false,
|
|
8
|
+
"version": "0.0.1",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": "19",
|
|
16
|
+
"@types/react": "19"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"tsup": "^8.5.1"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"@studiocubics",
|
|
23
|
+
"cubics",
|
|
24
|
+
"studio",
|
|
25
|
+
"utils",
|
|
26
|
+
"functions",
|
|
27
|
+
"utilities"
|
|
28
|
+
],
|
|
29
|
+
"author": {
|
|
30
|
+
"name": "Studio Cubics",
|
|
31
|
+
"email": "studiocubics7@gmail.com",
|
|
32
|
+
"url": "https://studio-cubics.vercel.app"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"clean": "rimraf dist node_modules"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./apiRes";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type ActionResponse<K extends string = string, E extends K[] = K[]> = {
|
|
2
|
+
success: boolean;
|
|
3
|
+
payload?: unknown;
|
|
4
|
+
error?: string;
|
|
5
|
+
fieldErrors?: Record<K, E>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const apiRes = {
|
|
9
|
+
actionFail: <K extends string = string>(
|
|
10
|
+
error: ActionResponse<K>["error"],
|
|
11
|
+
fieldErrors?: Record<string, string[]>
|
|
12
|
+
): ActionResponse<K> => ({
|
|
13
|
+
success: false,
|
|
14
|
+
error,
|
|
15
|
+
fieldErrors: fieldErrors as ActionResponse<K>["fieldErrors"],
|
|
16
|
+
}),
|
|
17
|
+
actionSuccess: <K extends string = string>(
|
|
18
|
+
payload?: unknown
|
|
19
|
+
): ActionResponse<K> => ({
|
|
20
|
+
success: true,
|
|
21
|
+
payload,
|
|
22
|
+
}),
|
|
23
|
+
/**
|
|
24
|
+
* "Failed to fetch data!"
|
|
25
|
+
*/
|
|
26
|
+
apiError: (message?: string) => (message ? message : "Failed to fetch data!"),
|
|
27
|
+
/**
|
|
28
|
+
* "You are not authenticated to access this resource."
|
|
29
|
+
*/
|
|
30
|
+
unauthorised: "You are not authenticated to access this resource.",
|
|
31
|
+
/**
|
|
32
|
+
* "You do not have the necessary permissions to access this resource."
|
|
33
|
+
*/
|
|
34
|
+
forbidden:
|
|
35
|
+
"You do not have the necessary permissions to access this resource.",
|
|
36
|
+
/**
|
|
37
|
+
* "${item} is of wrong type, expected type: ${expectedType}"
|
|
38
|
+
*/
|
|
39
|
+
wrongType: (item: string, expectedType?: string) =>
|
|
40
|
+
`${item} is of wrong type${expectedType ? `, expected type: ${expectedType}` : ""}.`,
|
|
41
|
+
/**
|
|
42
|
+
* "An error occured trying to read the formdata in these fields:
|
|
43
|
+
* ${fields}."
|
|
44
|
+
*/
|
|
45
|
+
formdataError: (fields: string | string[]) =>
|
|
46
|
+
`An error occured trying to read the formdata in these fields:\n${JSON.stringify(fields)}.`,
|
|
47
|
+
/**
|
|
48
|
+
* "Missing parameter: ${param}."
|
|
49
|
+
*/
|
|
50
|
+
missingParams: (param: string | string[]) =>
|
|
51
|
+
`Missing parameter: ${JSON.stringify(param)}.`,
|
|
52
|
+
/**
|
|
53
|
+
* "${item} not found!"
|
|
54
|
+
*/
|
|
55
|
+
notFound: (item: string) => `${item} not found!`,
|
|
56
|
+
/**
|
|
57
|
+
* "${item} already exists!"
|
|
58
|
+
*/
|
|
59
|
+
alreadyExists: (item: string) => `${item} already exists!`,
|
|
60
|
+
/**
|
|
61
|
+
* "${contentName} size exceeded the maximum of ${maxSize}"
|
|
62
|
+
*/
|
|
63
|
+
contentTooLarge: (contentName: string, maxSize?: string) =>
|
|
64
|
+
`${contentName} size exceeded${
|
|
65
|
+
maxSize ? ` the maximum of ${maxSize}` : ""
|
|
66
|
+
}.`,
|
|
67
|
+
/**
|
|
68
|
+
* An API Error has occurred:
|
|
69
|
+
* Check here => ${location}
|
|
70
|
+
* More Info: ${errorMessage}
|
|
71
|
+
*/
|
|
72
|
+
generalError: (location: string, errorMessage: string = "") =>
|
|
73
|
+
`An API Error has occurred:\nCheck here => ${location}\nMore Info: ${errorMessage}`,
|
|
74
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./calculateSafePosition";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
type VerticalOrigin = "top" | "center" | "bottom";
|
|
2
|
+
type HorizontalOrigin = "left" | "center" | "right";
|
|
3
|
+
|
|
4
|
+
interface ParsedOrigin {
|
|
5
|
+
vertical: VerticalOrigin;
|
|
6
|
+
horizontal: HorizontalOrigin;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SafePositionOptions {
|
|
10
|
+
anchorOrigin?: string;
|
|
11
|
+
transformOrigin?: string;
|
|
12
|
+
margin?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Position {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Calculates safe position of an element within viewport relative to an anchor element
|
|
22
|
+
* Works like Material UI Popover positioning:
|
|
23
|
+
* - anchorOrigin: point on the anchor element (e.g., "top center" = top-center of anchor)
|
|
24
|
+
* - transformOrigin: point on the positioned element that attaches to anchor (e.g., "bottom center" = bottom-center of popup attaches to anchor point)
|
|
25
|
+
*
|
|
26
|
+
* Example: anchorOrigin "top center" + transformOrigin "bottom center" positions popup above and centered
|
|
27
|
+
*
|
|
28
|
+
* @param element - The element to position
|
|
29
|
+
* @param anchorElement - The anchor element to position relative to
|
|
30
|
+
* @param options - Configuration options
|
|
31
|
+
* @returns Safe x, y coordinates
|
|
32
|
+
*/
|
|
33
|
+
export function calculateSafePosition(
|
|
34
|
+
element: HTMLElement | null,
|
|
35
|
+
anchorElement: HTMLElement | null,
|
|
36
|
+
options: SafePositionOptions = {}
|
|
37
|
+
): Position {
|
|
38
|
+
const {
|
|
39
|
+
anchorOrigin = "top left",
|
|
40
|
+
transformOrigin = "top left",
|
|
41
|
+
margin = 0,
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
if (!element || !anchorElement) return { x: 0, y: 0 };
|
|
45
|
+
|
|
46
|
+
const parseOrigin = (origin: string): ParsedOrigin => {
|
|
47
|
+
const parts = origin.toLowerCase().trim().split(/\s+/);
|
|
48
|
+
let vertical: VerticalOrigin = "top";
|
|
49
|
+
let horizontal: HorizontalOrigin = "left";
|
|
50
|
+
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
if (["top", "bottom"].includes(part)) vertical = part as VerticalOrigin;
|
|
53
|
+
else if (["left", "right"].includes(part))
|
|
54
|
+
horizontal = part as HorizontalOrigin;
|
|
55
|
+
else if (part === "center") {
|
|
56
|
+
// Assign center depending on position in string
|
|
57
|
+
if (parts.length === 1) {
|
|
58
|
+
vertical = "center";
|
|
59
|
+
horizontal = "center";
|
|
60
|
+
} else if (["top", "bottom"].includes(parts[0])) horizontal = "center";
|
|
61
|
+
else vertical = "center";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { vertical, horizontal };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const getAnchorOffset = (
|
|
69
|
+
size: number,
|
|
70
|
+
origin: VerticalOrigin | HorizontalOrigin
|
|
71
|
+
): number => {
|
|
72
|
+
if (origin === "center") return size / 2;
|
|
73
|
+
if (origin === "bottom" || origin === "right") return size;
|
|
74
|
+
return 0; // top or left
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getTransformOffset = (
|
|
78
|
+
size: number,
|
|
79
|
+
origin: VerticalOrigin | HorizontalOrigin
|
|
80
|
+
): number => {
|
|
81
|
+
if (origin === "center") return size / 2;
|
|
82
|
+
if (origin === "bottom" || origin === "right") return size;
|
|
83
|
+
return 0; // top or left
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const elementRect = element.getBoundingClientRect();
|
|
87
|
+
const anchorRect = anchorElement.getBoundingClientRect();
|
|
88
|
+
const viewportWidth = window.innerWidth;
|
|
89
|
+
const viewportHeight = window.innerHeight;
|
|
90
|
+
|
|
91
|
+
const anchor = parseOrigin(anchorOrigin);
|
|
92
|
+
const transform = parseOrigin(transformOrigin);
|
|
93
|
+
|
|
94
|
+
// Calculate anchor point on the anchor element
|
|
95
|
+
const anchorX =
|
|
96
|
+
anchorRect.left + getAnchorOffset(anchorRect.width, anchor.horizontal);
|
|
97
|
+
const anchorY =
|
|
98
|
+
anchorRect.top + getAnchorOffset(anchorRect.height, anchor.vertical);
|
|
99
|
+
|
|
100
|
+
// Calculate where on the element we're attaching (transform origin point)
|
|
101
|
+
const elementOriginX = getTransformOffset(
|
|
102
|
+
elementRect.width,
|
|
103
|
+
transform.horizontal
|
|
104
|
+
);
|
|
105
|
+
const elementOriginY = getTransformOffset(
|
|
106
|
+
elementRect.height,
|
|
107
|
+
transform.vertical
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Position the element so its transform origin aligns with the anchor point
|
|
111
|
+
let x = anchorX - elementOriginX;
|
|
112
|
+
let y = anchorY - elementOriginY;
|
|
113
|
+
|
|
114
|
+
// Check and adjust for viewport boundaries with margin
|
|
115
|
+
// Right boundary
|
|
116
|
+
if (x + elementRect.width > viewportWidth - margin) {
|
|
117
|
+
x = viewportWidth - elementRect.width - margin;
|
|
118
|
+
}
|
|
119
|
+
// Left boundary
|
|
120
|
+
if (x < margin) {
|
|
121
|
+
x = margin;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Bottom boundary
|
|
125
|
+
if (y + elementRect.height > viewportHeight - margin) {
|
|
126
|
+
y = viewportHeight - elementRect.height - margin;
|
|
127
|
+
}
|
|
128
|
+
// Top boundary
|
|
129
|
+
if (y < margin) {
|
|
130
|
+
y = margin;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
x: Math.max(margin, x),
|
|
135
|
+
y: Math.max(margin, y),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./delay";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
type DateToken =
|
|
2
|
+
| "Day"
|
|
3
|
+
| "day"
|
|
4
|
+
| "DD"
|
|
5
|
+
| "D"
|
|
6
|
+
| "MMMM"
|
|
7
|
+
| "MMM"
|
|
8
|
+
| "MM"
|
|
9
|
+
| "YYYY"
|
|
10
|
+
| "YY"
|
|
11
|
+
| "HH"
|
|
12
|
+
| "H"
|
|
13
|
+
| "hh"
|
|
14
|
+
| "h"
|
|
15
|
+
| "mm"
|
|
16
|
+
| "m"
|
|
17
|
+
| "ss"
|
|
18
|
+
| "s"
|
|
19
|
+
| "A"
|
|
20
|
+
| "a";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Formats a date according to the provided format string.
|
|
24
|
+
*
|
|
25
|
+
* @param dateInput - The date to format (as Date object, timestamp, or date string)
|
|
26
|
+
* @param format - The format string using tokens (default: "day D MMM, YYYY")
|
|
27
|
+
*
|
|
28
|
+
* @returns The formatted date string
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* formatDate(new Date(), "Day, DD MMMM YYYY")
|
|
32
|
+
* // => "Thursday, 02 January 2026"
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* formatDate(new Date(), "DD/MM/YYYY HH:mm:ss")
|
|
36
|
+
* // => "02/01/2026 14:30:45"
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* formatDate(new Date(), "h:mm A")
|
|
40
|
+
* // => "2:30 PM"
|
|
41
|
+
*
|
|
42
|
+
* Available tokens:
|
|
43
|
+
* - Day: Full day name (e.g., "Monday")
|
|
44
|
+
* - day: Short day name (e.g., "Mon")
|
|
45
|
+
* - DD: Day of month, zero-padded (e.g., "05")
|
|
46
|
+
* - D: Day of month (e.g., "5")
|
|
47
|
+
* - MMMM: Full month name (e.g., "January")
|
|
48
|
+
* - MMM: Short month name (e.g., "Jan")
|
|
49
|
+
* - MM: Month number, zero-padded (e.g., "01")
|
|
50
|
+
* - YYYY: Full year (e.g., "2026")
|
|
51
|
+
* - YY: Two-digit year (e.g., "26")
|
|
52
|
+
* - HH: Hours (24-hour), zero-padded (e.g., "14")
|
|
53
|
+
* - H: Hours (24-hour) (e.g., "14")
|
|
54
|
+
* - hh: Hours (12-hour), zero-padded (e.g., "02")
|
|
55
|
+
* - h: Hours (12-hour) (e.g., "2")
|
|
56
|
+
* - mm: Minutes, zero-padded (e.g., "05")
|
|
57
|
+
* - m: Minutes (e.g., "5")
|
|
58
|
+
* - ss: Seconds, zero-padded (e.g., "09")
|
|
59
|
+
* - s: Seconds (e.g., "9")
|
|
60
|
+
* - A: AM/PM uppercase (e.g., "PM")
|
|
61
|
+
* - a: am/pm lowercase (e.g., "pm")
|
|
62
|
+
*/
|
|
63
|
+
export function formatDate(
|
|
64
|
+
dateInput: number | string | Date,
|
|
65
|
+
format: string = "HH:mm:ss day D MMM, YYYY"
|
|
66
|
+
) {
|
|
67
|
+
const date = new Date(dateInput);
|
|
68
|
+
|
|
69
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
70
|
+
|
|
71
|
+
const daysFull = [
|
|
72
|
+
"Sunday",
|
|
73
|
+
"Monday",
|
|
74
|
+
"Tuesday",
|
|
75
|
+
"Wednesday",
|
|
76
|
+
"Thursday",
|
|
77
|
+
"Friday",
|
|
78
|
+
"Saturday",
|
|
79
|
+
];
|
|
80
|
+
const daysShort = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
81
|
+
const monthsFull = [
|
|
82
|
+
"January",
|
|
83
|
+
"February",
|
|
84
|
+
"March",
|
|
85
|
+
"April",
|
|
86
|
+
"May",
|
|
87
|
+
"June",
|
|
88
|
+
"July",
|
|
89
|
+
"August",
|
|
90
|
+
"September",
|
|
91
|
+
"October",
|
|
92
|
+
"November",
|
|
93
|
+
"December",
|
|
94
|
+
];
|
|
95
|
+
const monthsShort = monthsFull.map((m) => m.slice(0, 3));
|
|
96
|
+
|
|
97
|
+
// Extract all values once
|
|
98
|
+
const dayIndex = date.getDay();
|
|
99
|
+
const dateNum = date.getDate();
|
|
100
|
+
const monthIndex = date.getMonth();
|
|
101
|
+
const year = date.getFullYear();
|
|
102
|
+
const hours24 = date.getHours();
|
|
103
|
+
const minutes = date.getMinutes();
|
|
104
|
+
const seconds = date.getSeconds();
|
|
105
|
+
|
|
106
|
+
const hours12 = hours24 % 12 || 12;
|
|
107
|
+
const isPM = hours24 >= 12;
|
|
108
|
+
const yearStr = String(year);
|
|
109
|
+
|
|
110
|
+
const replacements: Record<DateToken, string> = {
|
|
111
|
+
Day: daysFull[dayIndex],
|
|
112
|
+
day: daysShort[dayIndex],
|
|
113
|
+
DD: pad(dateNum),
|
|
114
|
+
D: String(dateNum),
|
|
115
|
+
MMMM: monthsFull[monthIndex],
|
|
116
|
+
MMM: monthsShort[monthIndex],
|
|
117
|
+
MM: pad(monthIndex + 1),
|
|
118
|
+
YYYY: yearStr,
|
|
119
|
+
YY: yearStr.slice(-2),
|
|
120
|
+
HH: pad(hours24),
|
|
121
|
+
H: String(hours24),
|
|
122
|
+
hh: pad(hours12),
|
|
123
|
+
h: String(hours12),
|
|
124
|
+
mm: pad(minutes),
|
|
125
|
+
m: String(minutes),
|
|
126
|
+
ss: pad(seconds),
|
|
127
|
+
s: String(seconds),
|
|
128
|
+
A: isPM ? "PM" : "AM",
|
|
129
|
+
a: isPM ? "pm" : "am",
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Sort tokens by length (longest first) to avoid partial replacements
|
|
133
|
+
const tokens = (Object.keys(replacements) as DateToken[]).sort(
|
|
134
|
+
(a, b) => b.length - a.length
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Use a single regex replacement to avoid overlapping matches
|
|
138
|
+
const pattern = new RegExp(tokens.join("|"), "g");
|
|
139
|
+
return format.replace(pattern, (match) => replacements[match as DateToken]);
|
|
140
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a human-readable relative time string vs Date.now().
|
|
3
|
+
* Accepts Date | number (ms) | date-string.
|
|
4
|
+
* Fast, allocation-light, no Intl.
|
|
5
|
+
*/
|
|
6
|
+
export function relativeTime(
|
|
7
|
+
input: Date | number | string
|
|
8
|
+
): string {
|
|
9
|
+
const t: number =
|
|
10
|
+
input instanceof Date
|
|
11
|
+
? input.getTime()
|
|
12
|
+
: typeof input === "number"
|
|
13
|
+
? input
|
|
14
|
+
: Date.parse(input);
|
|
15
|
+
|
|
16
|
+
if (!Number.isFinite(t)) return "invalid date";
|
|
17
|
+
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const diffMs = t - now;
|
|
20
|
+
const absMs = Math.abs(diffMs);
|
|
21
|
+
const isFuture = diffMs > 0;
|
|
22
|
+
|
|
23
|
+
// time constants (ms)
|
|
24
|
+
const MIN = 60_000;
|
|
25
|
+
const HOUR = 3_600_000;
|
|
26
|
+
const DAY = 86_400_000;
|
|
27
|
+
const WEEK = 604_800_000;
|
|
28
|
+
const YEAR = 31_536_000_000;
|
|
29
|
+
|
|
30
|
+
// very far away → coarse wording
|
|
31
|
+
if (absMs > 5 * YEAR) {
|
|
32
|
+
return isFuture ? "a long time from now" : "a long time ago";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (absMs < MIN) {
|
|
36
|
+
return "just now";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let value: number;
|
|
40
|
+
let unit: "minute" | "hour" | "day" | "week" | "year";
|
|
41
|
+
|
|
42
|
+
if (absMs < HOUR) {
|
|
43
|
+
value = Math.round(absMs / MIN);
|
|
44
|
+
unit = "minute";
|
|
45
|
+
} else if (absMs < DAY) {
|
|
46
|
+
value = Math.round(absMs / HOUR);
|
|
47
|
+
unit = "hour";
|
|
48
|
+
} else if (absMs < WEEK) {
|
|
49
|
+
value = Math.round(absMs / DAY);
|
|
50
|
+
unit = "day";
|
|
51
|
+
} else if (absMs < YEAR) {
|
|
52
|
+
value = Math.round(absMs / WEEK);
|
|
53
|
+
unit = "week";
|
|
54
|
+
} else {
|
|
55
|
+
value = Math.round(absMs / YEAR);
|
|
56
|
+
unit = "year";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (value !== 1) unit += "s";
|
|
60
|
+
|
|
61
|
+
return isFuture
|
|
62
|
+
? `in ${value} ${unit}`
|
|
63
|
+
: `${value} ${unit} ago`;
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./initialiseForm";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ActionResponse } from "../API/apiRes";
|
|
2
|
+
|
|
3
|
+
export function initialiseForm<T extends string = string>(
|
|
4
|
+
...fieldNames: T[]
|
|
5
|
+
): ActionResponse<T, T[]> {
|
|
6
|
+
let fieldErrors = {} as Record<T, T[]>;
|
|
7
|
+
for (const fieldName of fieldNames) {
|
|
8
|
+
fieldErrors[fieldName] = [] as T[];
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
success: false,
|
|
12
|
+
fieldErrors,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./remap";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This function returns the rounded corresponding value in the target range using linear interpolation.
|
|
3
|
+
*
|
|
4
|
+
* @param value - The input value to remap.
|
|
5
|
+
* @param from - The source range as a tuple [min, max].
|
|
6
|
+
* @param to - The target range as a tuple [min, max].
|
|
7
|
+
* @returns The remapped value in the target range.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* remap(3, [1, 10], [1, 5]); // 2
|
|
11
|
+
*/
|
|
12
|
+
export function remap(
|
|
13
|
+
value: number,
|
|
14
|
+
from: [number, number],
|
|
15
|
+
to: [number, number],
|
|
16
|
+
roundTo: "floor" | "ceil" = "floor",
|
|
17
|
+
): number {
|
|
18
|
+
const [fromMin, fromMax] = from;
|
|
19
|
+
const [toMin, toMax] = to;
|
|
20
|
+
|
|
21
|
+
if (roundTo == "ceil")
|
|
22
|
+
return Math.ceil(
|
|
23
|
+
toMin + ((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin),
|
|
24
|
+
);
|
|
25
|
+
return Math.floor(
|
|
26
|
+
toMin + ((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./mergeRefs";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RefObject, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
export function mergeRefs<T>(
|
|
4
|
+
...refs: (Ref<T> | undefined)[]
|
|
5
|
+
): (instance: T | null) => void {
|
|
6
|
+
return (instance) => {
|
|
7
|
+
for (const ref of refs) {
|
|
8
|
+
if (!ref) continue;
|
|
9
|
+
if (typeof ref === "function") {
|
|
10
|
+
ref(instance);
|
|
11
|
+
} else {
|
|
12
|
+
(ref as RefObject<T | null>).current = instance;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function cssSafeString(str: string) {
|
|
2
|
+
const encoded = encodeURIComponent(str)
|
|
3
|
+
.toLowerCase()
|
|
4
|
+
.replace(/\.|%[0-9a-z]{2}/gi, "");
|
|
5
|
+
|
|
6
|
+
const firstChar = encoded.charAt(0);
|
|
7
|
+
if (firstChar.match(/^[0-9-]/)) {
|
|
8
|
+
// Check if the first character is a number or hyphen
|
|
9
|
+
return "_" + encoded;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return encoded;
|
|
13
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
function toWords(str: string): string[] {
|
|
2
|
+
return str
|
|
3
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase → camel Case
|
|
4
|
+
.replace(/[_\-]+/g, " ") // snake_case, kebab-case → spaces
|
|
5
|
+
.trim()
|
|
6
|
+
.split(/\s+/)
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((w) => w.toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function toCapitalCase(str: string | undefined): string {
|
|
12
|
+
if (!str) return "";
|
|
13
|
+
const words = toWords(str);
|
|
14
|
+
return words.map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toCamelCase(str: string | undefined): string {
|
|
18
|
+
if (!str) return "";
|
|
19
|
+
|
|
20
|
+
const words = toWords(str);
|
|
21
|
+
return words
|
|
22
|
+
.map((w, i) => (i === 0 ? w : w[0].toUpperCase() + w.slice(1)))
|
|
23
|
+
.join("");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function toKebabCase(str: string | undefined): string {
|
|
27
|
+
if (!str) return "";
|
|
28
|
+
|
|
29
|
+
return toWords(str).join("-");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toSnakeCase(str: string | undefined): string {
|
|
33
|
+
if (!str) return "";
|
|
34
|
+
|
|
35
|
+
return toWords(str).join("_");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function toConstantCase(str: string | undefined): string {
|
|
39
|
+
if (!str) return "";
|
|
40
|
+
|
|
41
|
+
return toWords(str).join("_").toUpperCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function toCapitalised(str: string | undefined): string {
|
|
45
|
+
if (!str) return "";
|
|
46
|
+
|
|
47
|
+
const words = toWords(str);
|
|
48
|
+
if (words.length === 0) return "";
|
|
49
|
+
return words.map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./API/_index";
|
|
2
|
+
export * from "./Calculations/_index";
|
|
3
|
+
export * from "./Code/_index";
|
|
4
|
+
export * from "./Dates/_index";
|
|
5
|
+
export * from "./Forms/_index";
|
|
6
|
+
export * from "./Numbers/_index";
|
|
7
|
+
export * from "./React/_index";
|
|
8
|
+
export * from "./Strings/_index";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
|
|
12
|
+
/* Bundler mode */
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"moduleDetection": "force",
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
// Output
|
|
20
|
+
"declaration": true,
|
|
21
|
+
"outDir": "./dist",
|
|
22
|
+
|
|
23
|
+
/* Linting */
|
|
24
|
+
"strict": true,
|
|
25
|
+
"noUnusedLocals": true,
|
|
26
|
+
"noUnusedParameters": true,
|
|
27
|
+
"erasableSyntaxOnly": true,
|
|
28
|
+
"noFallthroughCasesInSwitch": true,
|
|
29
|
+
"noUncheckedSideEffectImports": true
|
|
30
|
+
},
|
|
31
|
+
"include": ["src"]
|
|
32
|
+
}
|