@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.
@@ -0,0 +1,17 @@
1
+
2
+ > @studiocubics/utils@0.0.1 build /home/runner/work/StudioCubics/StudioCubics/packages/utils
3
+ > tsup
4
+
5
+ CLI Building entry: src/index.ts
6
+ CLI Using tsconfig: tsconfig.json
7
+ CLI tsup v8.5.1
8
+ CLI Using tsup config: /home/runner/work/StudioCubics/StudioCubics/packages/utils/tsup.config.ts
9
+ CLI Target: es2022
10
+ CLI Cleaning output folder
11
+ CJS Build start
12
+ CJS dist/index.js 9.75 KB
13
+ CJS dist/index.js.map 19.24 KB
14
+ CJS ⚡️ Build success in 343ms
15
+ DTS Build start
16
+ DTS ⚡️ Build success in 2850ms
17
+ DTS dist/index.d.ts 5.81 KB
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @studiocubics/utils
2
+
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - First publish inshallah!
@@ -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,5 @@
1
+ /**
2
+ * Delays the program loop for the given amount of milliseconds
3
+ */
4
+ export const delay = (ms: number) =>
5
+ new Promise((resolve) => setTimeout(resolve, ms));
@@ -0,0 +1,2 @@
1
+ export * from "./relativeTime";
2
+ export * from "./formateDate";
@@ -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,3 @@
1
+ export * from "./cn";
2
+ export * from "./stringCases";
3
+ export * from "./cssSafeString";
@@ -0,0 +1,3 @@
1
+ export function cn(...classNames: (string | undefined)[]) {
2
+ return classNames.filter((c) => c).join(" ");
3
+ }
@@ -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
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ splitting: true,
6
+ sourcemap: true,
7
+ clean: true,
8
+ dts: true,
9
+ });