@cleen/ui-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +42 -0
- package/src/hooks/useAnimateNumber.ts +56 -0
- package/src/hooks/useControlled.ts +40 -0
- package/src/hooks/useDebounce.ts +17 -0
- package/src/hooks/useDisclosure.ts +33 -0
- package/src/hooks/useForm.ts +38 -0
- package/src/hooks/useOutsideClick.ts +42 -0
- package/src/hooks/usePaginationState.ts +39 -0
- package/src/hooks/usePositionClose.ts +69 -0
- package/src/hooks/useValidation.ts +33 -0
- package/src/hooks/useWatchResize.ts +52 -0
- package/src/index.ts +21 -0
- package/src/store/colors.ts +98 -0
- package/src/types/position.ts +9 -0
- package/src/types/styles.ts +24 -0
- package/src/types/utils.ts +6 -0
- package/src/utils/audio.ts +69 -0
- package/src/utils/cn.ts +13 -0
- package/src/utils/colors.ts +159 -0
- package/src/utils/images.ts +42 -0
- package/src/utils/object.ts +86 -0
- package/src/utils/position.ts +140 -0
- package/src/utils/string.ts +27 -0
- package/styles/react-day-styles.css +457 -0
- package/tailwind-entry.css +81 -0
- package/tailwind.config.js +10 -0
- package/tailwind.preset.js +30 -0
- package/tsconfig.json +27 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Minimal synchronous PCM WAV encoder – no extra deps needed =w= */
|
|
2
|
+
export const encodeWav = (buffer: AudioBuffer): Blob => {
|
|
3
|
+
const { numberOfChannels: channels, sampleRate, length } = buffer;
|
|
4
|
+
const bitsPerSample = 16;
|
|
5
|
+
const blockAlign = (channels * bitsPerSample) / 8;
|
|
6
|
+
const byteRate = sampleRate * blockAlign;
|
|
7
|
+
const dataSize = length * blockAlign;
|
|
8
|
+
const wavBuffer = new ArrayBuffer(44 + dataSize);
|
|
9
|
+
const v = new DataView(wavBuffer);
|
|
10
|
+
|
|
11
|
+
const str = (offset: number, s: string) => {
|
|
12
|
+
for (let i = 0; i < s.length; i++) v.setUint8(offset + i, s.charCodeAt(i));
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
str(0, 'RIFF');
|
|
16
|
+
v.setUint32(4, 36 + dataSize, true);
|
|
17
|
+
str(8, 'WAVE');
|
|
18
|
+
str(12, 'fmt ');
|
|
19
|
+
v.setUint32(16, 16, true);
|
|
20
|
+
v.setUint16(20, 1, true); // PCM
|
|
21
|
+
v.setUint16(22, channels, true);
|
|
22
|
+
v.setUint32(24, sampleRate, true);
|
|
23
|
+
v.setUint32(28, byteRate, true);
|
|
24
|
+
v.setUint16(32, blockAlign, true);
|
|
25
|
+
v.setUint16(34, bitsPerSample, true);
|
|
26
|
+
str(36, 'data');
|
|
27
|
+
v.setUint32(40, dataSize, true);
|
|
28
|
+
|
|
29
|
+
let offset = 44;
|
|
30
|
+
for (let i = 0; i < length; i++) {
|
|
31
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
32
|
+
const s = Math.max(-1, Math.min(1, buffer.getChannelData(ch)[i]));
|
|
33
|
+
v.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
|
34
|
+
offset += 2;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new Blob([wavBuffer], { type: 'audio/wav' });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const trimBlob = async (
|
|
42
|
+
blob: Blob,
|
|
43
|
+
startSec: number,
|
|
44
|
+
endSec: number
|
|
45
|
+
): Promise<Blob> => {
|
|
46
|
+
const ctx = new AudioContext();
|
|
47
|
+
const decoded = await ctx.decodeAudioData(await blob.arrayBuffer());
|
|
48
|
+
await ctx.close();
|
|
49
|
+
|
|
50
|
+
const { sampleRate, numberOfChannels } = decoded;
|
|
51
|
+
const startSample = Math.round(startSec * sampleRate);
|
|
52
|
+
const endSample = Math.round(endSec * sampleRate);
|
|
53
|
+
const trimLength = endSample - startSample;
|
|
54
|
+
|
|
55
|
+
// OfflineAudioContext would add latency; just slice the buffers directly
|
|
56
|
+
const trimmedBuffer = new AudioContext().createBuffer(
|
|
57
|
+
numberOfChannels,
|
|
58
|
+
trimLength,
|
|
59
|
+
sampleRate
|
|
60
|
+
);
|
|
61
|
+
for (let ch = 0; ch < numberOfChannels; ch++) {
|
|
62
|
+
trimmedBuffer.copyToChannel(
|
|
63
|
+
decoded.getChannelData(ch).subarray(startSample, endSample),
|
|
64
|
+
ch
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return encodeWav(trimmedBuffer);
|
|
69
|
+
};
|
package/src/utils/cn.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import clsx, { type ClassValue } from 'clsx';
|
|
2
|
+
import { createTailwindMerge, getDefaultConfig } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
const twConfig = () => ({
|
|
5
|
+
...getDefaultConfig(),
|
|
6
|
+
prefix: 'cleen-',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const twMerge = createTailwindMerge(twConfig);
|
|
10
|
+
|
|
11
|
+
export const cn = (...inputs: ClassValue[]) => {
|
|
12
|
+
return twMerge(clsx(inputs));
|
|
13
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
darken,
|
|
3
|
+
getLuminance,
|
|
4
|
+
lighten,
|
|
5
|
+
parseToRgba,
|
|
6
|
+
toHex,
|
|
7
|
+
toRgba,
|
|
8
|
+
} from 'color2k';
|
|
9
|
+
|
|
10
|
+
type RGBA = [number, number, number, number];
|
|
11
|
+
|
|
12
|
+
export class ColorHelpers {
|
|
13
|
+
/**
|
|
14
|
+
* Calculates the Delta E (CIE94) between two RGB colors.
|
|
15
|
+
*
|
|
16
|
+
* Delta E is a metric for color difference. Based on the value, we can describe how different the colors are:
|
|
17
|
+
*
|
|
18
|
+
* Perception Levels:
|
|
19
|
+
* - Delta E <= 1.0: Not perceptible by human eyes.
|
|
20
|
+
* - Delta E between 1-2: Perceptible through close observation.
|
|
21
|
+
* - Delta E between 2-10: Perceptible at a glance.
|
|
22
|
+
* - Delta E between 11-49: Colors are more similar than opposite.
|
|
23
|
+
* - Delta E = 100: Colors are exact opposites.
|
|
24
|
+
*
|
|
25
|
+
* @param {Array<number>} rgb1 - The first RGB color as an array [R, G, B].
|
|
26
|
+
* @param {Array<number>} rgb2 - The second RGB color as an array [R, G, B].
|
|
27
|
+
* @returns {number} The Delta E value representing the difference between the two colors.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* deltaE([128, 0, 255], [128, 0, 255]); // 0
|
|
31
|
+
* deltaE([128, 0, 255], [128, 0, 230]); // 3.175
|
|
32
|
+
* deltaE([128, 0, 255], [255, 0, 0]); // 61.24
|
|
33
|
+
*/
|
|
34
|
+
static deltaE(rgbA: RGBA, rgbB: RGBA) {
|
|
35
|
+
const labA = ColorHelpers.rgb2lab(rgbA);
|
|
36
|
+
const labB = ColorHelpers.rgb2lab(rgbB);
|
|
37
|
+
|
|
38
|
+
const deltaL = labA[0] - labB[0];
|
|
39
|
+
const deltaA = labA[1] - labB[1];
|
|
40
|
+
const deltaB = labA[2] - labB[2];
|
|
41
|
+
const c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
|
|
42
|
+
const c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
|
|
43
|
+
const deltaC = c1 - c2;
|
|
44
|
+
let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
|
|
45
|
+
deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
|
|
46
|
+
const sc = 1.0 + 0.045 * c1;
|
|
47
|
+
const sh = 1.0 + 0.015 * c1;
|
|
48
|
+
const deltaLKlsl = deltaL / 1.0;
|
|
49
|
+
const deltaCkcsc = deltaC / sc;
|
|
50
|
+
const deltaHkhsh = deltaH / sh;
|
|
51
|
+
const i =
|
|
52
|
+
deltaLKlsl * deltaLKlsl +
|
|
53
|
+
deltaCkcsc * deltaCkcsc +
|
|
54
|
+
deltaHkhsh * deltaHkhsh;
|
|
55
|
+
return i < 0 ? 0 : Math.sqrt(i);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static rgb2lab(rgb: RGBA) {
|
|
59
|
+
let r = rgb[0] / 255,
|
|
60
|
+
g = rgb[1] / 255,
|
|
61
|
+
b = rgb[2] / 255,
|
|
62
|
+
x,
|
|
63
|
+
y,
|
|
64
|
+
z;
|
|
65
|
+
r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
|
|
66
|
+
g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
|
|
67
|
+
b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
|
|
68
|
+
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
|
|
69
|
+
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.0;
|
|
70
|
+
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
|
|
71
|
+
x = x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116;
|
|
72
|
+
y = y > 0.008856 ? Math.pow(y, 1 / 3) : 7.787 * y + 16 / 116;
|
|
73
|
+
z = z > 0.008856 ? Math.pow(z, 1 / 3) : 7.787 * z + 16 / 116;
|
|
74
|
+
return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static isSimilar(color1: string, color2: string, defaultDeltaE = 24) {
|
|
78
|
+
const e = ColorHelpers.deltaE(parseToRgba(color1), parseToRgba(color2));
|
|
79
|
+
return e < defaultDeltaE;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static adjustColorForContrast(hexColor: string, amount = 0.25) {
|
|
83
|
+
// Always convert to hex for polished
|
|
84
|
+
let _colorAsHex = toHex(hexColor);
|
|
85
|
+
let result = hexColor;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// If color is almost white, force to pure white to avoid hue bias
|
|
89
|
+
if (
|
|
90
|
+
_colorAsHex.match(/^#f{2,}e{0,2}$/i) ||
|
|
91
|
+
getLuminance(_colorAsHex) > 0.98
|
|
92
|
+
) {
|
|
93
|
+
_colorAsHex = '#ffffff';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (getLuminance(_colorAsHex) > 0.6) {
|
|
97
|
+
const darkenedColorHSLA = darken(_colorAsHex, amount);
|
|
98
|
+
const darkenedColorRGBA = toRgba(darkenedColorHSLA);
|
|
99
|
+
result = darkenedColorRGBA;
|
|
100
|
+
} else {
|
|
101
|
+
const lightenedColorHSLA = lighten(_colorAsHex, amount);
|
|
102
|
+
const lightenedColorRGBA = toRgba(lightenedColorHSLA);
|
|
103
|
+
result = lightenedColorRGBA;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
} catch {
|
|
108
|
+
return 'rgba(var(--cleen-white)';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Util functions to get color based on pass string (CSS variables can be parsed as well)
|
|
113
|
+
static getComputedColor(color?: string) {
|
|
114
|
+
if (!color?.includes('var(')) {
|
|
115
|
+
return color;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Extract variable name from var(--variable-name)
|
|
119
|
+
const varName = color.match(/var\((--[^,)]+)/)?.[1];
|
|
120
|
+
|
|
121
|
+
if (varName && typeof window !== 'undefined') {
|
|
122
|
+
// Get the computed style value from the document
|
|
123
|
+
const color = getComputedStyle(document.documentElement)
|
|
124
|
+
.getPropertyValue(varName)
|
|
125
|
+
.trim();
|
|
126
|
+
|
|
127
|
+
return color ? `rgb(${color})` : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static getComputedRgb(color?: string) {
|
|
134
|
+
if (!color) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!color?.includes('var(')) {
|
|
139
|
+
const [r, g, b] = parseToRgba(color);
|
|
140
|
+
return `${r}, ${g}, ${b}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extract variable name from var(--variable-name)
|
|
144
|
+
const varName = color.match(/var\((--[^,)]+)/)?.[1];
|
|
145
|
+
|
|
146
|
+
if (varName && typeof window !== 'undefined') {
|
|
147
|
+
// Get the computed style value from the document
|
|
148
|
+
return getComputedStyle(document.documentElement)
|
|
149
|
+
.getPropertyValue(varName)
|
|
150
|
+
.trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static trasparentize(color?: string, value = 0) {
|
|
157
|
+
return `color-mix(in srgb, ${color} ${(1 - value) * 100}%, transparent)`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const PICSUM_BASE_URL = 'https://picsum.photos';
|
|
2
|
+
|
|
3
|
+
interface PicsumOptions {
|
|
4
|
+
width: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
grayscale?: boolean;
|
|
7
|
+
blur?: number; // 1 to 10
|
|
8
|
+
seed?: string; // for consistent images
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const getRandomImageUrl = (options: PicsumOptions): string => {
|
|
12
|
+
const { width, height, grayscale, blur, seed } = options;
|
|
13
|
+
|
|
14
|
+
let url = PICSUM_BASE_URL;
|
|
15
|
+
|
|
16
|
+
if (seed) {
|
|
17
|
+
url += `/seed/${encodeURIComponent(seed)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
url += `/${width}/${height || width}`;
|
|
21
|
+
|
|
22
|
+
const params = new URLSearchParams();
|
|
23
|
+
|
|
24
|
+
if (grayscale) {
|
|
25
|
+
params.append('grayscale', '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (blur) {
|
|
29
|
+
params.append('blur', blur.toString());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (seed) {
|
|
33
|
+
params.append('seed', seed);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const queryString = params.toString();
|
|
37
|
+
if (queryString) {
|
|
38
|
+
url += `?${queryString}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return url;
|
|
42
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep partial type — makes all properties optional recursively.
|
|
3
|
+
*/
|
|
4
|
+
export type PartialDeep<T> = T extends (...args: unknown[]) => unknown
|
|
5
|
+
? T
|
|
6
|
+
: T extends Array<infer U>
|
|
7
|
+
? Array<PartialDeep<U>>
|
|
8
|
+
: T extends object
|
|
9
|
+
? { [K in keyof T]?: PartialDeep<T[K]> }
|
|
10
|
+
: T | undefined;
|
|
11
|
+
|
|
12
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return (
|
|
14
|
+
typeof value === 'object' &&
|
|
15
|
+
value !== null &&
|
|
16
|
+
!Array.isArray(value) &&
|
|
17
|
+
Object.prototype.toString.call(value) === '[object Object]'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Recursively apply values on top of default values.
|
|
23
|
+
*
|
|
24
|
+
* Rules / assumptions:
|
|
25
|
+
* - Plain objects are merged recursively.
|
|
26
|
+
* - Arrays are replaced entirely by provided values (not concatenated).
|
|
27
|
+
* - If a provided value is `undefined`, the default is kept.
|
|
28
|
+
* - If a provided value is `null`, it overrides the default.
|
|
29
|
+
* - Non-plain objects (Date, RegExp, Map, Set, class instances) are replaced by the provided value when present.
|
|
30
|
+
*
|
|
31
|
+
* Example:
|
|
32
|
+
* const defaults = { a: 1, b: { c: 2, d: [1] } };
|
|
33
|
+
* const vals = { b: { c: 3 } };
|
|
34
|
+
* applyDefaults(defaults, vals) // => { a: 1, b: { c: 3, d: [1] } }
|
|
35
|
+
*/
|
|
36
|
+
export function applyDefaults<T>(defaults: T, values?: PartialDeep<T>): T {
|
|
37
|
+
// Internal recursive merge
|
|
38
|
+
const merge = (d: unknown, v: unknown): unknown => {
|
|
39
|
+
// If v is explicitly undefined, keep default
|
|
40
|
+
if (v === undefined) {
|
|
41
|
+
return cloneValue(d);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If both are plain objects, merge recursively
|
|
45
|
+
if (isPlainObject(d) && isPlainObject(v)) {
|
|
46
|
+
const result: Record<string, unknown> = {};
|
|
47
|
+
const keys = new Set<string>([...Object.keys(d), ...Object.keys(v)]);
|
|
48
|
+
keys.forEach(key => {
|
|
49
|
+
const dv = (d as Record<string, unknown>)[key];
|
|
50
|
+
const vv = (v as Record<string, unknown>)[key];
|
|
51
|
+
result[key] = merge(dv, vv);
|
|
52
|
+
});
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Arrays: prefer provided value (replace), or clone default if v is undefined handled above
|
|
57
|
+
if (Array.isArray(d) || Array.isArray(v)) {
|
|
58
|
+
if (v === undefined) return cloneValue(d);
|
|
59
|
+
// If provided a value for array, return clone of that array
|
|
60
|
+
return cloneValue(v);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// For all other cases, if v is provided (including null), use it; otherwise clone default
|
|
64
|
+
return v !== undefined ? cloneValue(v) : cloneValue(d);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Shallow/deep clone helper for primitives, arrays and plain objects
|
|
68
|
+
const cloneValue = (x: unknown): unknown => {
|
|
69
|
+
if (x === undefined) return undefined;
|
|
70
|
+
if (x === null) return null;
|
|
71
|
+
if (Array.isArray(x)) return (x as unknown[]).map(item => cloneValue(item));
|
|
72
|
+
if (isPlainObject(x)) {
|
|
73
|
+
const o: Record<string, unknown> = {};
|
|
74
|
+
Object.keys(x as Record<string, unknown>).forEach(k => {
|
|
75
|
+
o[k] = cloneValue((x as Record<string, unknown>)[k]);
|
|
76
|
+
});
|
|
77
|
+
return o;
|
|
78
|
+
}
|
|
79
|
+
// For non-plain objects (Date, RegExp, Map, Set, etc.) and primitives, return as is
|
|
80
|
+
return x;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return merge(defaults, values) as T;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default applyDefaults;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Position } from '@/types/position';
|
|
2
|
+
|
|
3
|
+
// Neat lil' helper to calculate the optimal position based on viewport
|
|
4
|
+
export const calculateOptimalPosition = (
|
|
5
|
+
overlayRect: DOMRect,
|
|
6
|
+
triggerRect: DOMRect,
|
|
7
|
+
preferredPosition: Position
|
|
8
|
+
): Position => {
|
|
9
|
+
const viewport = {
|
|
10
|
+
width: window.innerWidth,
|
|
11
|
+
height: window.innerHeight,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let newPosition = preferredPosition;
|
|
15
|
+
|
|
16
|
+
// Handle side positions (left/right)
|
|
17
|
+
if (preferredPosition === 'left' || preferredPosition === 'right') {
|
|
18
|
+
const isLeft = preferredPosition === 'left';
|
|
19
|
+
const spaceLeft = triggerRect.left;
|
|
20
|
+
const spaceRight = viewport.width - triggerRect.right;
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
isLeft &&
|
|
24
|
+
spaceLeft < overlayRect.width &&
|
|
25
|
+
spaceRight > overlayRect.width
|
|
26
|
+
) {
|
|
27
|
+
return 'right';
|
|
28
|
+
} else if (
|
|
29
|
+
!isLeft &&
|
|
30
|
+
spaceRight < overlayRect.width &&
|
|
31
|
+
spaceLeft > overlayRect.width
|
|
32
|
+
) {
|
|
33
|
+
return 'left';
|
|
34
|
+
}
|
|
35
|
+
return newPosition;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check vertical bounds for top/bottom positions
|
|
39
|
+
const isBottom = preferredPosition.startsWith('bottom');
|
|
40
|
+
const spaceBelow = viewport.height - triggerRect.bottom;
|
|
41
|
+
const spaceAbove = triggerRect.top;
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
isBottom &&
|
|
45
|
+
spaceBelow < overlayRect.height &&
|
|
46
|
+
spaceAbove > overlayRect.height
|
|
47
|
+
) {
|
|
48
|
+
newPosition = preferredPosition.replace('bottom', 'top') as Position;
|
|
49
|
+
} else if (
|
|
50
|
+
!isBottom &&
|
|
51
|
+
preferredPosition.startsWith('top') &&
|
|
52
|
+
spaceAbove < overlayRect.height &&
|
|
53
|
+
spaceBelow > overlayRect.height
|
|
54
|
+
) {
|
|
55
|
+
newPosition = preferredPosition.replace('top', 'bottom') as Position;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check horizontal bounds for corner positions
|
|
59
|
+
if (preferredPosition.includes('-')) {
|
|
60
|
+
const isLeft = preferredPosition.endsWith('left');
|
|
61
|
+
const spaceRight = viewport.width - triggerRect.right;
|
|
62
|
+
const spaceLeft = triggerRect.left;
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
isLeft &&
|
|
66
|
+
spaceRight < overlayRect.width &&
|
|
67
|
+
spaceLeft > overlayRect.width
|
|
68
|
+
) {
|
|
69
|
+
newPosition = newPosition.replace('left', 'right') as Position;
|
|
70
|
+
} else if (
|
|
71
|
+
!isLeft &&
|
|
72
|
+
spaceRight < overlayRect.width &&
|
|
73
|
+
spaceLeft > overlayRect.width
|
|
74
|
+
) {
|
|
75
|
+
newPosition = newPosition.replace('right', 'left') as Position;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return newPosition;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Calculate the top/left values based on the final position
|
|
83
|
+
export const calculatePositionValues = (
|
|
84
|
+
overlayRect: DOMRect,
|
|
85
|
+
triggerRect: DOMRect,
|
|
86
|
+
position: Position,
|
|
87
|
+
offset?: number
|
|
88
|
+
) => {
|
|
89
|
+
if (!triggerRect) {
|
|
90
|
+
return { top: 0, left: 0 };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const scrollY = window.scrollY;
|
|
94
|
+
const scrollX = window.scrollX;
|
|
95
|
+
const gap = offset || 0;
|
|
96
|
+
|
|
97
|
+
// Handle vertical positioning (top/bottom)
|
|
98
|
+
let top = 0;
|
|
99
|
+
if (position.startsWith('bottom')) {
|
|
100
|
+
top = triggerRect.bottom + scrollY + gap;
|
|
101
|
+
} else if (position.startsWith('top')) {
|
|
102
|
+
top = triggerRect.top + scrollY - (overlayRect?.height || 0) - gap;
|
|
103
|
+
} else {
|
|
104
|
+
// left/right positions - vertically center
|
|
105
|
+
top =
|
|
106
|
+
triggerRect.top +
|
|
107
|
+
scrollY +
|
|
108
|
+
triggerRect.height / 2 -
|
|
109
|
+
(overlayRect?.height || 0) / 2;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle horizontal positioning (left/right)
|
|
113
|
+
let left = 0;
|
|
114
|
+
if (position === 'left') {
|
|
115
|
+
// Pure "left" - place overlay outside to the left of trigger
|
|
116
|
+
left = triggerRect.left + scrollX - (overlayRect?.width || 0) - gap;
|
|
117
|
+
} else if (position === 'right') {
|
|
118
|
+
// Pure "right" - place overlay outside to the right of trigger
|
|
119
|
+
left = triggerRect.right + scrollX + gap;
|
|
120
|
+
} else if (
|
|
121
|
+
position.endsWith('left') ||
|
|
122
|
+
position === 'bottom' ||
|
|
123
|
+
position === 'top'
|
|
124
|
+
) {
|
|
125
|
+
left = triggerRect.left + scrollX;
|
|
126
|
+
} else if (position.endsWith('right')) {
|
|
127
|
+
left = triggerRect.right + scrollX - (overlayRect?.width || 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Special handling for centered positions
|
|
131
|
+
if (position === 'bottom' || position === 'top') {
|
|
132
|
+
left =
|
|
133
|
+
triggerRect.left +
|
|
134
|
+
scrollX +
|
|
135
|
+
triggerRect.width / 2 -
|
|
136
|
+
(overlayRect?.width || 0) / 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { top, left };
|
|
140
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const formatFileSize = (bytes?: number) => {
|
|
2
|
+
if (bytes === 0 || !bytes) {
|
|
3
|
+
return '0 B';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const k = 1024;
|
|
7
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
8
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
9
|
+
return Math.round(bytes / Math.pow(k, i)) + ' ' + sizes[i];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Calculate "mm:ss" from seconds
|
|
13
|
+
export const formatAudioTime = (seconds: number): string => {
|
|
14
|
+
const mins = Math.floor(seconds / 60);
|
|
15
|
+
const secs = Math.floor(seconds % 60);
|
|
16
|
+
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Calculate "created X days ago"
|
|
20
|
+
export const getCreatedDate = (date: string) => {
|
|
21
|
+
const created = new Date(date);
|
|
22
|
+
const now = new Date();
|
|
23
|
+
|
|
24
|
+
const diffTime = Math.abs(now.getTime() - created.getTime());
|
|
25
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
26
|
+
return `Created ${diffDays} days ago`;
|
|
27
|
+
};
|