beez-ui 0.6.2 → 0.8.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/CHANGELOG.md +150 -96
- package/README.md +33 -6
- package/dist/components/account-menu.d.ts +53 -0
- package/dist/components/account-menu.js +405 -0
- package/dist/components/alert.js +1 -1
- package/dist/components/animated-collapse.d.ts +19 -0
- package/dist/components/animated-collapse.js +81 -0
- package/dist/components/animated-count.d.ts +13 -0
- package/dist/components/animated-count.js +105 -0
- package/dist/components/animated-list-item.d.ts +25 -0
- package/dist/components/animated-list-item.js +132 -0
- package/dist/components/avatar.d.ts +2 -1
- package/dist/components/avatar.js +4 -3
- package/dist/components/bouncing-dots-loader.d.ts +19 -0
- package/dist/components/bouncing-dots-loader.js +47 -0
- package/dist/components/calendar.js +102 -0
- package/dist/components/carousel.js +61 -48
- package/dist/components/checkbox.js +1 -1
- package/dist/components/confirm-delete-button.d.ts +29 -0
- package/dist/components/confirm-delete-button.js +254 -0
- package/dist/components/data-table.js +34 -4
- package/dist/components/dropdown-menu.js +5 -2
- package/dist/components/empty-state.d.ts +24 -0
- package/dist/components/empty-state.js +37 -0
- package/dist/components/error-state.d.ts +20 -0
- package/dist/components/error-state.js +145 -0
- package/dist/components/external-browser-handoff.d.ts +27 -0
- package/dist/components/external-browser-handoff.js +306 -0
- package/dist/components/file-upload.d.ts +78 -0
- package/dist/components/file-upload.js +678 -0
- package/dist/components/filter-query-bar.js +11 -6
- package/dist/components/form.d.ts +5 -0
- package/dist/components/form.js +11 -9
- package/dist/components/info-popover.d.ts +23 -0
- package/dist/components/info-popover.js +158 -0
- package/dist/components/input-group.js +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/month-calendar-header.d.ts +65 -0
- package/dist/components/month-calendar-header.js +337 -0
- package/dist/components/month-grid.d.ts +59 -0
- package/dist/components/month-grid.js +301 -0
- package/dist/components/notification-bell.d.ts +50 -0
- package/dist/components/notification-bell.js +338 -0
- package/dist/components/notification-panel.d.ts +56 -0
- package/dist/components/notification-panel.js +281 -0
- package/dist/components/open-in-browser-cta.d.ts +22 -0
- package/dist/components/open-in-browser-cta.js +61 -0
- package/dist/components/pagination.d.ts +1 -0
- package/dist/components/pagination.js +9 -4
- package/dist/components/presence-swap.d.ts +22 -0
- package/dist/components/presence-swap.js +162 -0
- package/dist/components/progress-ring.d.ts +20 -0
- package/dist/components/progress-ring.js +53 -0
- package/dist/components/pwa-update-control.d.ts +18 -0
- package/dist/components/pwa-update-control.js +119 -0
- package/dist/components/radio-group.js +1 -1
- package/dist/components/reaction-button.d.ts +26 -0
- package/dist/components/reaction-button.js +114 -0
- package/dist/components/rich-link-editor.d.ts +25 -0
- package/dist/components/rich-link-editor.js +320 -0
- package/dist/components/rich-markdown-content.d.ts +10 -0
- package/dist/components/rich-markdown-content.js +56 -0
- package/dist/components/rich-text-content.d.ts +19 -0
- package/dist/components/rich-text-content.js +26 -0
- package/dist/components/select.js +1 -1
- package/dist/components/sheet.js +12 -9
- package/dist/components/sidebar.js +9 -5
- package/dist/components/skeleton.d.ts +6 -0
- package/dist/components/skeleton.js +7 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/textarea.js +1 -1
- package/dist/components/typing-animation.js +42 -15
- package/dist/emoji-picker.d.ts +27 -0
- package/dist/emoji-picker.js +283 -0
- package/dist/hooks/use-horizontal-swipe.d.ts +23 -0
- package/dist/hooks/use-horizontal-swipe.js +80 -0
- package/dist/hooks/use-is-hydrated.d.ts +6 -0
- package/dist/hooks/use-is-hydrated.js +20 -0
- package/dist/hooks/use-minute-clock.d.ts +23 -0
- package/dist/hooks/use-minute-clock.js +147 -0
- package/dist/hooks/use-month-transition-direction.d.ts +21 -0
- package/dist/hooks/use-month-transition-direction.js +110 -0
- package/dist/hooks/use-rich-link-editor.d.ts +55 -0
- package/dist/hooks/use-rich-link-editor.js +515 -0
- package/dist/hooks/use-viewer-time-zone.d.ts +6 -0
- package/dist/hooks/use-viewer-time-zone.js +20 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +43 -0
- package/dist/lib/browser-clipboard.d.ts +13 -0
- package/dist/lib/browser-clipboard.js +48 -0
- package/dist/lib/browser-navigation.d.ts +21 -0
- package/dist/lib/browser-navigation.js +42 -0
- package/dist/lib/column-filter-value.d.ts +12 -0
- package/dist/lib/column-filter-value.js +52 -0
- package/dist/lib/file-acceptance.d.ts +43 -0
- package/dist/lib/file-acceptance.js +107 -0
- package/dist/lib/format-file-size.d.ts +16 -0
- package/dist/lib/format-file-size.js +34 -0
- package/dist/lib/fuzzy-search.d.ts +17 -0
- package/dist/lib/fuzzy-search.js +183 -0
- package/dist/lib/horizontal-swipe.d.ts +23 -0
- package/dist/lib/horizontal-swipe.js +29 -0
- package/dist/lib/in-app-browser.d.ts +35 -0
- package/dist/lib/in-app-browser.js +80 -0
- package/dist/lib/month-grid.d.ts +42 -0
- package/dist/lib/month-grid.js +74 -0
- package/dist/lib/name-initials.d.ts +7 -0
- package/dist/lib/name-initials.js +11 -0
- package/dist/lib/rich-text/link-markdown-constants.d.ts +147 -0
- package/dist/lib/rich-text/link-markdown-constants.js +458 -0
- package/dist/lib/rich-text/link-markdown-types.d.ts +53 -0
- package/dist/lib/rich-text/link-markdown-types.js +1 -0
- package/dist/lib/rich-text/link-markdown.d.ts +193 -0
- package/dist/lib/rich-text/link-markdown.js +592 -0
- package/dist/lib/rich-text/rich-markdown.d.ts +41 -0
- package/dist/lib/rich-text/rich-markdown.js +105 -0
- package/dist/lib/rich-text/rich-text-editor-dom.d.ts +26 -0
- package/dist/lib/rich-text/rich-text-editor-dom.js +79 -0
- package/dist/motion/glide-indicator.d.ts +37 -3
- package/dist/motion/glide-indicator.js +37 -3
- package/dist/motion/motion-slot.js +47 -18
- package/dist/motion/surface-keyframes.js +18 -6
- package/dist/motion/tokens.d.ts +41 -2
- package/dist/motion/tokens.js +40 -1
- package/dist/providers/beez-ui-provider.js +49 -17
- package/dist/styles.css +1 -1
- package/package.json +212 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Browser navigation helpers: full navigations and address-bar updates without history entries. */
|
|
2
|
+
export declare function reloadCurrentPage(): void;
|
|
3
|
+
export declare function navigateToUrl(url: string): void;
|
|
4
|
+
/**
|
|
5
|
+
* Sets (or removes, with `null`) query parameters of the current URL in a
|
|
6
|
+
* single `history.replaceState`, which the Next.js router tracks, so the
|
|
7
|
+
* address bar reflects UI state without a navigation or a history entry.
|
|
8
|
+
* Parameters not listed keep their current value, as do the path and hash.
|
|
9
|
+
*
|
|
10
|
+
* @param searchParams - New value per query parameter name, or null to remove it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function replaceCurrentUrlSearchParams(searchParams: Readonly<Record<string, string | null>>): void;
|
|
13
|
+
/**
|
|
14
|
+
* Replaces every value of one repeatable query parameter of the current URL
|
|
15
|
+
* (an empty list removes it) through `history.replaceState`, like
|
|
16
|
+
* {@link replaceCurrentUrlSearchParams}.
|
|
17
|
+
*
|
|
18
|
+
* @param name - Query parameter name.
|
|
19
|
+
* @param values - New values, in order.
|
|
20
|
+
*/
|
|
21
|
+
export declare function replaceCurrentUrlSearchParamValues(name: string, values: readonly string[]): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Browser navigation helpers: full navigations and address-bar updates without history entries. */
|
|
2
|
+
export function reloadCurrentPage() {
|
|
3
|
+
window.location.reload();
|
|
4
|
+
}
|
|
5
|
+
export function navigateToUrl(url) {
|
|
6
|
+
window.location.href = url;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Sets (or removes, with `null`) query parameters of the current URL in a
|
|
10
|
+
* single `history.replaceState`, which the Next.js router tracks, so the
|
|
11
|
+
* address bar reflects UI state without a navigation or a history entry.
|
|
12
|
+
* Parameters not listed keep their current value, as do the path and hash.
|
|
13
|
+
*
|
|
14
|
+
* @param searchParams - New value per query parameter name, or null to remove it.
|
|
15
|
+
*/
|
|
16
|
+
export function replaceCurrentUrlSearchParams(searchParams) {
|
|
17
|
+
const url = new URL(window.location.href);
|
|
18
|
+
for (const [name, value] of Object.entries(searchParams)) {
|
|
19
|
+
if (value === null) {
|
|
20
|
+
url.searchParams.delete(name);
|
|
21
|
+
} else {
|
|
22
|
+
url.searchParams.set(name, value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
window.history.replaceState(null, "", url.pathname + url.search + url.hash);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Replaces every value of one repeatable query parameter of the current URL
|
|
29
|
+
* (an empty list removes it) through `history.replaceState`, like
|
|
30
|
+
* {@link replaceCurrentUrlSearchParams}.
|
|
31
|
+
*
|
|
32
|
+
* @param name - Query parameter name.
|
|
33
|
+
* @param values - New values, in order.
|
|
34
|
+
*/
|
|
35
|
+
export function replaceCurrentUrlSearchParamValues(name, values) {
|
|
36
|
+
const url = new URL(window.location.href);
|
|
37
|
+
url.searchParams.delete(name);
|
|
38
|
+
for (const value of values) {
|
|
39
|
+
url.searchParams.append(name, value);
|
|
40
|
+
}
|
|
41
|
+
window.history.replaceState(null, "", url.pathname + url.search + url.hash);
|
|
42
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Normalizes text for accent- and case-insensitive comparisons. */
|
|
2
|
+
export declare function normalizeFilterToken(value: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Evalúa el valor de una celda contra un filtro de columna de la barra de filtros.
|
|
5
|
+
* Entiende los valores estructurados (`numberRange`, `enum`, `presence`, `yearMonthRange`)
|
|
6
|
+
* y, para texto plano, busca coincidencias parciales sin distinguir acentos ni mayúsculas.
|
|
7
|
+
* Una celda sin valor comparable no cumple los rangos; un filtro vacío deja pasar la fila.
|
|
8
|
+
* @param cellValue - Valor de la celda tal como lo devuelve el accessor de la columna.
|
|
9
|
+
* @param filterValue - Valor activo del filtro de esa columna.
|
|
10
|
+
* @returns `true` cuando la fila debe mostrarse.
|
|
11
|
+
*/
|
|
12
|
+
export declare function matchesColumnFilterValue(cellValue: unknown, filterValue: unknown): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { matchesAdvancedYearMonthRangeFilter } from "./year-month-filter.js";
|
|
2
|
+
const DIACRITICS_PATTERN = /[̀-ͯ]/g;
|
|
3
|
+
/** Normalizes text for accent- and case-insensitive comparisons. */
|
|
4
|
+
export function normalizeFilterToken(value) {
|
|
5
|
+
return value.normalize("NFD").replace(DIACRITICS_PATTERN, "").toLocaleLowerCase().trim();
|
|
6
|
+
}
|
|
7
|
+
/** Distinguishes the grammar's structured values from plain text written by the search input. */
|
|
8
|
+
function isColumnFilterValue(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && typeof value.kind === "string";
|
|
10
|
+
}
|
|
11
|
+
/** Reads numbers and numeric strings; anything else has no comparable value. */
|
|
12
|
+
function toFiniteNumber(value) {
|
|
13
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
14
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
15
|
+
const parsed = Number(value);
|
|
16
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/** Treats `null`, `undefined`, blank strings and empty lists as a missing value. */
|
|
21
|
+
function isMissingValue(value) {
|
|
22
|
+
if (value == null) return true;
|
|
23
|
+
if (typeof value === "string") return value.trim() === "";
|
|
24
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Evalúa el valor de una celda contra un filtro de columna de la barra de filtros.
|
|
29
|
+
* Entiende los valores estructurados (`numberRange`, `enum`, `presence`, `yearMonthRange`)
|
|
30
|
+
* y, para texto plano, busca coincidencias parciales sin distinguir acentos ni mayúsculas.
|
|
31
|
+
* Una celda sin valor comparable no cumple los rangos; un filtro vacío deja pasar la fila.
|
|
32
|
+
* @param cellValue - Valor de la celda tal como lo devuelve el accessor de la columna.
|
|
33
|
+
* @param filterValue - Valor activo del filtro de esa columna.
|
|
34
|
+
* @returns `true` cuando la fila debe mostrarse.
|
|
35
|
+
*/
|
|
36
|
+
export function matchesColumnFilterValue(cellValue, filterValue) {
|
|
37
|
+
if (!isColumnFilterValue(filterValue)) {
|
|
38
|
+
const query = normalizeFilterToken(String(filterValue ?? ""));
|
|
39
|
+
return query === "" || normalizeFilterToken(String(cellValue ?? "")).includes(query);
|
|
40
|
+
}
|
|
41
|
+
switch (filterValue.kind) {
|
|
42
|
+
case "numberRange": {
|
|
43
|
+
const value = toFiniteNumber(cellValue);
|
|
44
|
+
if (value == null) return false;
|
|
45
|
+
if (filterValue.min != null && value < filterValue.min) return false;
|
|
46
|
+
return filterValue.max == null || value <= filterValue.max;
|
|
47
|
+
}
|
|
48
|
+
case "enum": return Array.isArray(cellValue) ? cellValue.some((item) => String(item) === filterValue.value) : String(cellValue ?? "") === filterValue.value;
|
|
49
|
+
case "presence": return filterValue.value === "hasValue" ? !isMissingValue(cellValue) : isMissingValue(cellValue);
|
|
50
|
+
case "yearMonthRange": return matchesAdvancedYearMonthRangeFilter(filterValue, toFiniteNumber(cellValue));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates picked or dropped files against an `accept` list and a size limit, mirroring the
|
|
3
|
+
* native `<input accept>` syntax. Files without a MIME type (common on some Android pickers and
|
|
4
|
+
* drag sources) fall back to their extension.
|
|
5
|
+
*/
|
|
6
|
+
/** File metadata the checks need; a `File` satisfies it. */
|
|
7
|
+
export interface AcceptableFile {
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
size: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Extracts the lowercase extension from a file name.
|
|
14
|
+
* @param fileName - The file name.
|
|
15
|
+
* @returns The extension with its leading dot, or null when there is none.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getFileExtension(fileName: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Tells whether a file matches an `accept` list such as `"image/*,.pdf"`.
|
|
20
|
+
* @param file - File to check.
|
|
21
|
+
* @param accept - Comma-separated extensions, exact MIME types or `type/*` wildcards; empty accepts all.
|
|
22
|
+
* @returns Whether the file is accepted.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isFileTypeAccepted(file: AcceptableFile, accept?: string): boolean;
|
|
25
|
+
export interface ClassifyFilesOptions {
|
|
26
|
+
accept?: string;
|
|
27
|
+
/** Maximum size in bytes; checked before the type. */
|
|
28
|
+
maxSize?: number;
|
|
29
|
+
/** When false, only the first file is considered. */
|
|
30
|
+
allowsMultiple?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface ClassifiedFiles<TFile extends AcceptableFile> {
|
|
33
|
+
accepted: TFile[];
|
|
34
|
+
unaccepted: TFile[];
|
|
35
|
+
oversized: TFile[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Splits files into accepted, wrong-type and oversized groups.
|
|
39
|
+
* @param files - Picked or dropped files.
|
|
40
|
+
* @param options - Accept list, size limit and multiplicity.
|
|
41
|
+
* @returns The three groups, preserving input order.
|
|
42
|
+
*/
|
|
43
|
+
export declare function classifyFiles<TFile extends AcceptableFile>(files: readonly TFile[], { accept, maxSize, allowsMultiple }?: ClassifyFilesOptions): ClassifiedFiles<TFile>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates picked or dropped files against an `accept` list and a size limit, mirroring the
|
|
3
|
+
* native `<input accept>` syntax. Files without a MIME type (common on some Android pickers and
|
|
4
|
+
* drag sources) fall back to their extension.
|
|
5
|
+
*/
|
|
6
|
+
const MIME_TYPE_EXTENSION_FALLBACKS = {
|
|
7
|
+
"application/pdf": [".pdf"],
|
|
8
|
+
"audio/mpeg": [".mp3"],
|
|
9
|
+
"image/gif": [".gif"],
|
|
10
|
+
"image/heic": [".heic"],
|
|
11
|
+
"image/heif": [".heif"],
|
|
12
|
+
"image/jpeg": [".jpg", ".jpeg"],
|
|
13
|
+
"image/png": [".png"],
|
|
14
|
+
"image/webp": [".webp"],
|
|
15
|
+
"text/plain": [".txt"],
|
|
16
|
+
"video/mp4": [".mp4"]
|
|
17
|
+
};
|
|
18
|
+
const WILDCARD_MIME_PREFIX_EXTENSION_FALLBACKS = {
|
|
19
|
+
audio: [
|
|
20
|
+
".aac",
|
|
21
|
+
".flac",
|
|
22
|
+
".m4a",
|
|
23
|
+
".mp3",
|
|
24
|
+
".ogg",
|
|
25
|
+
".wav"
|
|
26
|
+
],
|
|
27
|
+
image: [
|
|
28
|
+
".avif",
|
|
29
|
+
".bmp",
|
|
30
|
+
".gif",
|
|
31
|
+
".heic",
|
|
32
|
+
".heif",
|
|
33
|
+
".jpeg",
|
|
34
|
+
".jpg",
|
|
35
|
+
".png",
|
|
36
|
+
".svg",
|
|
37
|
+
".webp"
|
|
38
|
+
],
|
|
39
|
+
text: [
|
|
40
|
+
".csv",
|
|
41
|
+
".md",
|
|
42
|
+
".txt"
|
|
43
|
+
],
|
|
44
|
+
video: [
|
|
45
|
+
".avi",
|
|
46
|
+
".m4v",
|
|
47
|
+
".mov",
|
|
48
|
+
".mp4",
|
|
49
|
+
".mkv",
|
|
50
|
+
".webm"
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
const ACCEPT_LIST_SEPARATOR = ",";
|
|
54
|
+
const EXTENSION_PREFIX = ".";
|
|
55
|
+
const MIME_WILDCARD_SUFFIX = "/*";
|
|
56
|
+
const MIME_TYPE_SEPARATOR = "/";
|
|
57
|
+
/**
|
|
58
|
+
* Extracts the lowercase extension from a file name.
|
|
59
|
+
* @param fileName - The file name.
|
|
60
|
+
* @returns The extension with its leading dot, or null when there is none.
|
|
61
|
+
*/
|
|
62
|
+
export function getFileExtension(fileName) {
|
|
63
|
+
const fileNameParts = fileName.split(EXTENSION_PREFIX);
|
|
64
|
+
const extension = fileNameParts.length > 1 ? fileNameParts.pop() : null;
|
|
65
|
+
return extension ? `${EXTENSION_PREFIX}${extension.toLowerCase()}` : null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Tells whether a file matches an `accept` list such as `"image/*,.pdf"`.
|
|
69
|
+
* @param file - File to check.
|
|
70
|
+
* @param accept - Comma-separated extensions, exact MIME types or `type/*` wildcards; empty accepts all.
|
|
71
|
+
* @returns Whether the file is accepted.
|
|
72
|
+
*/
|
|
73
|
+
export function isFileTypeAccepted(file, accept) {
|
|
74
|
+
if (!accept) return true;
|
|
75
|
+
const fileExtension = getFileExtension(file.name);
|
|
76
|
+
const normalizedFileType = file.type.toLowerCase();
|
|
77
|
+
const hasMimeType = normalizedFileType.length > 0;
|
|
78
|
+
const extensionMatches = (candidateExtensions) => Boolean(fileExtension && candidateExtensions?.includes(fileExtension));
|
|
79
|
+
return accept.split(ACCEPT_LIST_SEPARATOR).map((acceptedType) => acceptedType.trim().toLowerCase()).filter(Boolean).some((acceptedType) => {
|
|
80
|
+
if (acceptedType.startsWith(EXTENSION_PREFIX)) return fileExtension === acceptedType;
|
|
81
|
+
if (acceptedType.endsWith(MIME_WILDCARD_SUFFIX)) {
|
|
82
|
+
const typePrefix = acceptedType.split(MIME_TYPE_SEPARATOR)[0];
|
|
83
|
+
return hasMimeType ? normalizedFileType.startsWith(`${typePrefix}${MIME_TYPE_SEPARATOR}`) : extensionMatches(WILDCARD_MIME_PREFIX_EXTENSION_FALLBACKS[typePrefix]);
|
|
84
|
+
}
|
|
85
|
+
return hasMimeType && normalizedFileType === acceptedType || extensionMatches(MIME_TYPE_EXTENSION_FALLBACKS[acceptedType]);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Splits files into accepted, wrong-type and oversized groups.
|
|
90
|
+
* @param files - Picked or dropped files.
|
|
91
|
+
* @param options - Accept list, size limit and multiplicity.
|
|
92
|
+
* @returns The three groups, preserving input order.
|
|
93
|
+
*/
|
|
94
|
+
export function classifyFiles(files, { accept, maxSize, allowsMultiple = true } = {}) {
|
|
95
|
+
const classifiedFiles = {
|
|
96
|
+
accepted: [],
|
|
97
|
+
unaccepted: [],
|
|
98
|
+
oversized: []
|
|
99
|
+
};
|
|
100
|
+
const filesToProcess = allowsMultiple ? files : files.slice(0, 1);
|
|
101
|
+
for (const file of filesToProcess) {
|
|
102
|
+
if (maxSize !== undefined && file.size > maxSize) classifiedFiles.oversized.push(file);
|
|
103
|
+
else if (isFileTypeAccepted(file, accept)) classifiedFiles.accepted.push(file);
|
|
104
|
+
else classifiedFiles.unaccepted.push(file);
|
|
105
|
+
}
|
|
106
|
+
return classifiedFiles;
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure file-size formatting helper shared by attachment and upload UIs. Framework-safe:
|
|
3
|
+
* no React, no platform APIs beyond `Intl`, so it runs on the server and the client alike.
|
|
4
|
+
*/
|
|
5
|
+
export interface FormatFileSizeOptions {
|
|
6
|
+
/** BCP 47 locale used for the decimal separator. Defaults to `es-AR`. */
|
|
7
|
+
locale?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Formats a byte count as a short human-readable size, e.g. `12,3 MB`, `850 KB`, `25 MB`.
|
|
11
|
+
*
|
|
12
|
+
* @param bytes - Raw size in bytes. Non-finite or negative values render as `0 B`.
|
|
13
|
+
* @param options - Optional locale for the number format.
|
|
14
|
+
* @returns The formatted size with its unit, separated by a space.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatFileSize(bytes: number, { locale }?: FormatFileSizeOptions): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure file-size formatting helper shared by attachment and upload UIs. Framework-safe:
|
|
3
|
+
* no React, no platform APIs beyond `Intl`, so it runs on the server and the client alike.
|
|
4
|
+
*/
|
|
5
|
+
const BYTES_PER_UNIT_STEP = 1024;
|
|
6
|
+
const FILE_SIZE_UNIT_LABELS = [
|
|
7
|
+
"B",
|
|
8
|
+
"KB",
|
|
9
|
+
"MB",
|
|
10
|
+
"GB",
|
|
11
|
+
"TB"
|
|
12
|
+
];
|
|
13
|
+
/** Spanish (Argentina) keeps the comma decimal separator of the original product copy. */
|
|
14
|
+
const DEFAULT_FILE_SIZE_LOCALE = "es-AR";
|
|
15
|
+
const SUB_UNIT_FRACTION_DIGITS = 0;
|
|
16
|
+
const SCALED_UNIT_FRACTION_DIGITS = 1;
|
|
17
|
+
/**
|
|
18
|
+
* Formats a byte count as a short human-readable size, e.g. `12,3 MB`, `850 KB`, `25 MB`.
|
|
19
|
+
*
|
|
20
|
+
* @param bytes - Raw size in bytes. Non-finite or negative values render as `0 B`.
|
|
21
|
+
* @param options - Optional locale for the number format.
|
|
22
|
+
* @returns The formatted size with its unit, separated by a space.
|
|
23
|
+
*/
|
|
24
|
+
export function formatFileSize(bytes, { locale = DEFAULT_FILE_SIZE_LOCALE } = {}) {
|
|
25
|
+
const safeBytes = Number.isFinite(bytes) && bytes > 0 ? bytes : 0;
|
|
26
|
+
let scaledValue = safeBytes;
|
|
27
|
+
let unitIndex = 0;
|
|
28
|
+
while (scaledValue >= BYTES_PER_UNIT_STEP && unitIndex < FILE_SIZE_UNIT_LABELS.length - 1) {
|
|
29
|
+
scaledValue /= BYTES_PER_UNIT_STEP;
|
|
30
|
+
unitIndex += 1;
|
|
31
|
+
}
|
|
32
|
+
const formattedValue = new Intl.NumberFormat(locale, { maximumFractionDigits: unitIndex === 0 ? SUB_UNIT_FRACTION_DIGITS : SCALED_UNIT_FRACTION_DIGITS }).format(scaledValue);
|
|
33
|
+
return `${formattedValue} ${FILE_SIZE_UNIT_LABELS[unitIndex]}`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Ranks and highlights fuzzy text matches, ignoring case and diacritics, for pickers and search lists. */
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export interface FuzzyMatchRank {
|
|
4
|
+
contiguousPairCount: number;
|
|
5
|
+
gapCount: number;
|
|
6
|
+
longestRun: number;
|
|
7
|
+
matchedChars: number;
|
|
8
|
+
maxGap: number;
|
|
9
|
+
span: number;
|
|
10
|
+
startIndex: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function compareFuzzyMatchRank(leftRank: FuzzyMatchRank, rightRank: FuzzyMatchRank): number;
|
|
13
|
+
export declare function normalizeSearchValue(value: string): string;
|
|
14
|
+
export declare function getExactMatchIndices(value: string, query: string): number[] | null;
|
|
15
|
+
export declare function getFuzzyMatchIndices(value: string, query: string): number[] | null;
|
|
16
|
+
export declare function getFuzzyMatchRank(value: string, query: string): FuzzyMatchRank | null;
|
|
17
|
+
export declare function renderHighlightedText(value: string, matchedIndices: number[], highlightClassName: string, keyPrefix: string): ReactNode;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
const DIACRITICS_PATTERN = /[\u0300-\u036f]/g;
|
|
3
|
+
export function compareFuzzyMatchRank(leftRank, rightRank) {
|
|
4
|
+
if (leftRank.longestRun !== rightRank.longestRun) {
|
|
5
|
+
return rightRank.longestRun - leftRank.longestRun;
|
|
6
|
+
}
|
|
7
|
+
if (leftRank.contiguousPairCount !== rightRank.contiguousPairCount) {
|
|
8
|
+
return rightRank.contiguousPairCount - leftRank.contiguousPairCount;
|
|
9
|
+
}
|
|
10
|
+
if (leftRank.gapCount !== rightRank.gapCount) {
|
|
11
|
+
return leftRank.gapCount - rightRank.gapCount;
|
|
12
|
+
}
|
|
13
|
+
if (leftRank.maxGap !== rightRank.maxGap) {
|
|
14
|
+
return leftRank.maxGap - rightRank.maxGap;
|
|
15
|
+
}
|
|
16
|
+
if (leftRank.span !== rightRank.span) {
|
|
17
|
+
return leftRank.span - rightRank.span;
|
|
18
|
+
}
|
|
19
|
+
if (leftRank.startIndex !== rightRank.startIndex) {
|
|
20
|
+
return leftRank.startIndex - rightRank.startIndex;
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
export function normalizeSearchValue(value) {
|
|
25
|
+
return value.normalize("NFD").replace(DIACRITICS_PATTERN, "").toLocaleLowerCase();
|
|
26
|
+
}
|
|
27
|
+
function getSearchCharsWithSourceIndices(value) {
|
|
28
|
+
const chars = [];
|
|
29
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
30
|
+
const normalizedChar = normalizeSearchValue(value[index]);
|
|
31
|
+
if (!normalizedChar) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
for (const char of normalizedChar) {
|
|
35
|
+
chars.push({
|
|
36
|
+
char,
|
|
37
|
+
sourceIndex: index
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return chars;
|
|
42
|
+
}
|
|
43
|
+
export function getExactMatchIndices(value, query) {
|
|
44
|
+
const normalizedQuery = normalizeSearchValue(query).trim();
|
|
45
|
+
if (!normalizedQuery) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const valueChars = getSearchCharsWithSourceIndices(value);
|
|
49
|
+
const queryChars = Array.from(normalizedQuery);
|
|
50
|
+
if (queryChars.length > valueChars.length) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
for (let valueIndex = 0; valueIndex <= valueChars.length - queryChars.length; valueIndex += 1) {
|
|
54
|
+
let isExactMatch = true;
|
|
55
|
+
for (let queryIndex = 0; queryIndex < queryChars.length; queryIndex += 1) {
|
|
56
|
+
if (valueChars[valueIndex + queryIndex].char !== queryChars[queryIndex]) {
|
|
57
|
+
isExactMatch = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!isExactMatch) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const matchStartSourceIndex = valueChars[valueIndex].sourceIndex;
|
|
65
|
+
const matchEndSourceIndex = valueChars[valueIndex + queryChars.length - 1].sourceIndex;
|
|
66
|
+
return Array.from({ length: matchEndSourceIndex - matchStartSourceIndex + 1 }, (_, index) => matchStartSourceIndex + index);
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
export function getFuzzyMatchIndices(value, query) {
|
|
71
|
+
const normalizedQuery = normalizeSearchValue(query).trim();
|
|
72
|
+
if (!normalizedQuery) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const valueChars = getSearchCharsWithSourceIndices(value);
|
|
76
|
+
const queryChars = Array.from(normalizedQuery);
|
|
77
|
+
const matchedIndices = [];
|
|
78
|
+
let valueCursor = 0;
|
|
79
|
+
for (const queryChar of queryChars) {
|
|
80
|
+
let foundAt = -1;
|
|
81
|
+
for (let index = valueCursor; index < valueChars.length; index += 1) {
|
|
82
|
+
if (valueChars[index].char === queryChar) {
|
|
83
|
+
foundAt = index;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (foundAt === -1) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
matchedIndices.push(valueChars[foundAt].sourceIndex);
|
|
91
|
+
valueCursor = foundAt + 1;
|
|
92
|
+
}
|
|
93
|
+
return Array.from(new Set(matchedIndices));
|
|
94
|
+
}
|
|
95
|
+
export function getFuzzyMatchRank(value, query) {
|
|
96
|
+
const normalizedQuery = normalizeSearchValue(query).trim();
|
|
97
|
+
if (!normalizedQuery) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const valueChars = getSearchCharsWithSourceIndices(value);
|
|
101
|
+
const queryChars = Array.from(normalizedQuery);
|
|
102
|
+
const matchedSourceIndices = [];
|
|
103
|
+
let valueCursor = 0;
|
|
104
|
+
for (const queryChar of queryChars) {
|
|
105
|
+
let foundAt = -1;
|
|
106
|
+
for (let index = valueCursor; index < valueChars.length; index += 1) {
|
|
107
|
+
if (valueChars[index].char === queryChar) {
|
|
108
|
+
foundAt = index;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (foundAt === -1) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
matchedSourceIndices.push(valueChars[foundAt].sourceIndex);
|
|
116
|
+
valueCursor = foundAt + 1;
|
|
117
|
+
}
|
|
118
|
+
const startIndex = matchedSourceIndices[0] ?? 0;
|
|
119
|
+
const endIndex = matchedSourceIndices[matchedSourceIndices.length - 1] ?? startIndex;
|
|
120
|
+
let contiguousPairCount = 0;
|
|
121
|
+
let gapCount = 0;
|
|
122
|
+
let longestRun = matchedSourceIndices.length > 0 ? 1 : 0;
|
|
123
|
+
let maxGap = 0;
|
|
124
|
+
let currentRun = matchedSourceIndices.length > 0 ? 1 : 0;
|
|
125
|
+
for (let index = 1; index < matchedSourceIndices.length; index += 1) {
|
|
126
|
+
if (matchedSourceIndices[index] === matchedSourceIndices[index - 1] + 1) {
|
|
127
|
+
contiguousPairCount += 1;
|
|
128
|
+
currentRun += 1;
|
|
129
|
+
} else {
|
|
130
|
+
currentRun = 1;
|
|
131
|
+
}
|
|
132
|
+
if (currentRun > longestRun) {
|
|
133
|
+
longestRun = currentRun;
|
|
134
|
+
}
|
|
135
|
+
const gap = Math.max(matchedSourceIndices[index] - matchedSourceIndices[index - 1] - 1, 0);
|
|
136
|
+
gapCount += gap;
|
|
137
|
+
if (gap > maxGap) {
|
|
138
|
+
maxGap = gap;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
contiguousPairCount,
|
|
143
|
+
gapCount,
|
|
144
|
+
longestRun,
|
|
145
|
+
matchedChars: queryChars.length,
|
|
146
|
+
maxGap,
|
|
147
|
+
span: Math.max(endIndex - startIndex, 0),
|
|
148
|
+
startIndex
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function renderHighlightedText(value, matchedIndices, highlightClassName, keyPrefix) {
|
|
152
|
+
if (matchedIndices.length === 0) {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
const matchedIndexSet = new Set(matchedIndices);
|
|
156
|
+
const highlightedParts = [];
|
|
157
|
+
let currentStart = 0;
|
|
158
|
+
let partIndex = 0;
|
|
159
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
160
|
+
if (!matchedIndexSet.has(index)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (currentStart < index) {
|
|
164
|
+
highlightedParts.push(/* @__PURE__ */ _jsx("span", { children: value.slice(currentStart, index) }, `${keyPrefix}-text-${partIndex}`));
|
|
165
|
+
partIndex += 1;
|
|
166
|
+
}
|
|
167
|
+
let matchEnd = index + 1;
|
|
168
|
+
while (matchEnd < value.length && matchedIndexSet.has(matchEnd)) {
|
|
169
|
+
matchEnd += 1;
|
|
170
|
+
}
|
|
171
|
+
highlightedParts.push(/* @__PURE__ */ _jsx("mark", {
|
|
172
|
+
className: highlightClassName,
|
|
173
|
+
children: value.slice(index, matchEnd)
|
|
174
|
+
}, `${keyPrefix}-match-${partIndex}`));
|
|
175
|
+
partIndex += 1;
|
|
176
|
+
currentStart = matchEnd;
|
|
177
|
+
index = matchEnd - 1;
|
|
178
|
+
}
|
|
179
|
+
if (currentStart < value.length) {
|
|
180
|
+
highlightedParts.push(/* @__PURE__ */ _jsx("span", { children: value.slice(currentStart) }, `${keyPrefix}-text-${partIndex}`));
|
|
181
|
+
}
|
|
182
|
+
return /* @__PURE__ */ _jsx("span", { children: highlightedParts }, `${keyPrefix}-highlighted`);
|
|
183
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure classification of a pointer drag into a horizontal swipe. Kept apart
|
|
3
|
+
* from the DOM so thresholds are unit-testable and shared by any pager.
|
|
4
|
+
*/
|
|
5
|
+
export declare const HORIZONTAL_SWIPE_DIRECTION: {
|
|
6
|
+
readonly next: "next";
|
|
7
|
+
readonly previous: "previous";
|
|
8
|
+
};
|
|
9
|
+
export type HorizontalSwipeDirection = (typeof HORIZONTAL_SWIPE_DIRECTION)[keyof typeof HORIZONTAL_SWIPE_DIRECTION];
|
|
10
|
+
export type HorizontalSwipeGesture = {
|
|
11
|
+
deltaX: number;
|
|
12
|
+
deltaY: number;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Decides whether a drag is a deliberate horizontal swipe. A leftward swipe
|
|
17
|
+
* moves to the next page and a rightward one to the previous page, like
|
|
18
|
+
* turning a calendar page.
|
|
19
|
+
*
|
|
20
|
+
* @param gesture - Pointer travel and duration.
|
|
21
|
+
* @returns The swipe direction, or null when the drag is not a swipe.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveHorizontalSwipe(gesture: HorizontalSwipeGesture): HorizontalSwipeDirection | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure classification of a pointer drag into a horizontal swipe. Kept apart
|
|
3
|
+
* from the DOM so thresholds are unit-testable and shared by any pager.
|
|
4
|
+
*/
|
|
5
|
+
export const HORIZONTAL_SWIPE_DIRECTION = {
|
|
6
|
+
next: "next",
|
|
7
|
+
previous: "previous"
|
|
8
|
+
};
|
|
9
|
+
/** Minimum horizontal travel, in CSS pixels, to count as a swipe. */
|
|
10
|
+
const SWIPE_MIN_DISTANCE_PX = 60;
|
|
11
|
+
/** Horizontal travel must dominate vertical travel by this ratio. */
|
|
12
|
+
const SWIPE_HORIZONTAL_DOMINANCE_RATIO = 1.5;
|
|
13
|
+
/** Slower drags read as reading/scrolling, not as a page flip. */
|
|
14
|
+
const SWIPE_MAX_DURATION_MS = 800;
|
|
15
|
+
/**
|
|
16
|
+
* Decides whether a drag is a deliberate horizontal swipe. A leftward swipe
|
|
17
|
+
* moves to the next page and a rightward one to the previous page, like
|
|
18
|
+
* turning a calendar page.
|
|
19
|
+
*
|
|
20
|
+
* @param gesture - Pointer travel and duration.
|
|
21
|
+
* @returns The swipe direction, or null when the drag is not a swipe.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveHorizontalSwipe(gesture) {
|
|
24
|
+
const horizontalDistance = Math.abs(gesture.deltaX);
|
|
25
|
+
if (horizontalDistance < SWIPE_MIN_DISTANCE_PX || horizontalDistance < Math.abs(gesture.deltaY) * SWIPE_HORIZONTAL_DOMINANCE_RATIO || gesture.durationMs > SWIPE_MAX_DURATION_MS) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return gesture.deltaX < 0 ? HORIZONTAL_SWIPE_DIRECTION.next : HORIZONTAL_SWIPE_DIRECTION.previous;
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Detects in-app browsers and builds deep links that reopen a URL in the default browser. */
|
|
2
|
+
export type InAppBrowserDetectionResult = {
|
|
3
|
+
isInAppBrowser: boolean;
|
|
4
|
+
isIos: boolean;
|
|
5
|
+
isAndroid: boolean;
|
|
6
|
+
isMercadoPago: boolean;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Classifies a user agent: in-app browsers (social, messaging and payment apps, iOS web views)
|
|
10
|
+
* often block sign-in popups or lose sessions, so callers can offer to continue elsewhere.
|
|
11
|
+
* Safe on the server: pass the request `User-Agent` header.
|
|
12
|
+
*
|
|
13
|
+
* @param userAgent - User agent string, or nothing when unknown.
|
|
14
|
+
* @returns Whether it is an in-app browser and which platform it runs on.
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectInAppBrowser(userAgent: string | null | undefined): InAppBrowserDetectionResult;
|
|
17
|
+
export declare const EXTERNAL_BROWSER_PLATFORM: {
|
|
18
|
+
readonly android: "android";
|
|
19
|
+
readonly ios: "ios";
|
|
20
|
+
};
|
|
21
|
+
export type ExternalBrowserPlatform = (typeof EXTERNAL_BROWSER_PLATFORM)[keyof typeof EXTERNAL_BROWSER_PLATFORM];
|
|
22
|
+
type BuildExternalBrowserUrlInput = {
|
|
23
|
+
platform: ExternalBrowserPlatform;
|
|
24
|
+
targetHttpsUrl: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Builds a deep link that asks the OS to open the given https URL in the
|
|
28
|
+
* user's default external browser instead of the in-app browser.
|
|
29
|
+
*
|
|
30
|
+
* @param input - Target URL and detected platform.
|
|
31
|
+
* @returns A deep-link URL for iOS Safari or Android Chrome. Returns null when
|
|
32
|
+
* the target URL does not use the https scheme.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildExternalBrowserUrl({ platform, targetHttpsUrl, }: BuildExternalBrowserUrlInput): string | null;
|
|
35
|
+
export {};
|