beez-ui 0.7.0 → 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 +21 -3
- package/README.md +290 -274
- package/dist/components/account-menu.d.ts +53 -0
- package/dist/components/account-menu.js +405 -0
- 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/bouncing-dots-loader.d.ts +19 -0
- package/dist/components/bouncing-dots-loader.js +47 -0
- package/dist/components/confirm-delete-button.d.ts +29 -0
- package/dist/components/confirm-delete-button.js +254 -0
- 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/info-popover.d.ts +23 -0
- package/dist/components/info-popover.js +158 -0
- 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/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/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/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/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/tokens.d.ts +17 -1
- package/dist/motion/tokens.js +17 -1
- package/dist/styles.css +1 -1
- package/package.json +211 -1
package/dist/index.d.ts
CHANGED
|
@@ -46,3 +46,47 @@ export * from "./components/typing-animation.js";
|
|
|
46
46
|
export * from "./components/themed-toaster.js";
|
|
47
47
|
export * from "./theme.js";
|
|
48
48
|
export * from "./lib/year-month-filter.js";
|
|
49
|
+
export * from "./motion/tokens.js";
|
|
50
|
+
export * from "./components/animated-collapse.js";
|
|
51
|
+
export * from "./components/animated-count.js";
|
|
52
|
+
export * from "./components/animated-list-item.js";
|
|
53
|
+
export * from "./components/presence-swap.js";
|
|
54
|
+
export * from "./components/bouncing-dots-loader.js";
|
|
55
|
+
export * from "./components/progress-ring.js";
|
|
56
|
+
export * from "./components/confirm-delete-button.js";
|
|
57
|
+
export * from "./components/info-popover.js";
|
|
58
|
+
export * from "./components/empty-state.js";
|
|
59
|
+
export * from "./components/error-state.js";
|
|
60
|
+
export * from "./components/account-menu.js";
|
|
61
|
+
export * from "./components/rich-text-content.js";
|
|
62
|
+
export * from "./components/rich-markdown-content.js";
|
|
63
|
+
export * from "./components/rich-link-editor.js";
|
|
64
|
+
export * from "./components/open-in-browser-cta.js";
|
|
65
|
+
export * from "./components/external-browser-handoff.js";
|
|
66
|
+
export * from "./components/pwa-update-control.js";
|
|
67
|
+
export * from "./components/reaction-button.js";
|
|
68
|
+
export * from "./components/notification-panel.js";
|
|
69
|
+
export * from "./components/notification-bell.js";
|
|
70
|
+
export * from "./components/month-grid.js";
|
|
71
|
+
export * from "./components/month-calendar-header.js";
|
|
72
|
+
export * from "./components/file-upload.js";
|
|
73
|
+
export * from "./hooks/use-is-hydrated.js";
|
|
74
|
+
export * from "./hooks/use-minute-clock.js";
|
|
75
|
+
export * from "./hooks/use-viewer-time-zone.js";
|
|
76
|
+
export * from "./hooks/use-horizontal-swipe.js";
|
|
77
|
+
export * from "./hooks/use-rich-link-editor.js";
|
|
78
|
+
export * from "./hooks/use-month-transition-direction.js";
|
|
79
|
+
export * from "./hooks/use-prefers-reduced-motion.js";
|
|
80
|
+
export * from "./lib/browser-clipboard.js";
|
|
81
|
+
export * from "./lib/browser-navigation.js";
|
|
82
|
+
export * from "./lib/file-acceptance.js";
|
|
83
|
+
export * from "./lib/format-file-size.js";
|
|
84
|
+
export * from "./lib/fuzzy-search.js";
|
|
85
|
+
export * from "./lib/horizontal-swipe.js";
|
|
86
|
+
export * from "./lib/in-app-browser.js";
|
|
87
|
+
export * from "./lib/month-grid.js";
|
|
88
|
+
export * from "./lib/name-initials.js";
|
|
89
|
+
export { parseRichTextSegments, normalizeMarkdownUrl, normalizeAutolinkUrl } from "./lib/rich-text/link-markdown.js";
|
|
90
|
+
export { RICH_TEXT_SEGMENT_TYPE, RICH_LINK_KIND } from "./lib/rich-text/link-markdown-constants.js";
|
|
91
|
+
export type { RichTextSegment, RichLink } from "./lib/rich-text/link-markdown-types.js";
|
|
92
|
+
export * from "./lib/rich-text/rich-markdown.js";
|
package/dist/index.js
CHANGED
|
@@ -45,3 +45,46 @@ export * from "./components/typing-animation.js";
|
|
|
45
45
|
export * from "./components/themed-toaster.js";
|
|
46
46
|
export * from "./theme.js";
|
|
47
47
|
export * from "./lib/year-month-filter.js";
|
|
48
|
+
export * from "./motion/tokens.js";
|
|
49
|
+
export * from "./components/animated-collapse.js";
|
|
50
|
+
export * from "./components/animated-count.js";
|
|
51
|
+
export * from "./components/animated-list-item.js";
|
|
52
|
+
export * from "./components/presence-swap.js";
|
|
53
|
+
export * from "./components/bouncing-dots-loader.js";
|
|
54
|
+
export * from "./components/progress-ring.js";
|
|
55
|
+
export * from "./components/confirm-delete-button.js";
|
|
56
|
+
export * from "./components/info-popover.js";
|
|
57
|
+
export * from "./components/empty-state.js";
|
|
58
|
+
export * from "./components/error-state.js";
|
|
59
|
+
export * from "./components/account-menu.js";
|
|
60
|
+
export * from "./components/rich-text-content.js";
|
|
61
|
+
export * from "./components/rich-markdown-content.js";
|
|
62
|
+
export * from "./components/rich-link-editor.js";
|
|
63
|
+
export * from "./components/open-in-browser-cta.js";
|
|
64
|
+
export * from "./components/external-browser-handoff.js";
|
|
65
|
+
export * from "./components/pwa-update-control.js";
|
|
66
|
+
export * from "./components/reaction-button.js";
|
|
67
|
+
export * from "./components/notification-panel.js";
|
|
68
|
+
export * from "./components/notification-bell.js";
|
|
69
|
+
export * from "./components/month-grid.js";
|
|
70
|
+
export * from "./components/month-calendar-header.js";
|
|
71
|
+
export * from "./components/file-upload.js";
|
|
72
|
+
export * from "./hooks/use-is-hydrated.js";
|
|
73
|
+
export * from "./hooks/use-minute-clock.js";
|
|
74
|
+
export * from "./hooks/use-viewer-time-zone.js";
|
|
75
|
+
export * from "./hooks/use-horizontal-swipe.js";
|
|
76
|
+
export * from "./hooks/use-rich-link-editor.js";
|
|
77
|
+
export * from "./hooks/use-month-transition-direction.js";
|
|
78
|
+
export * from "./hooks/use-prefers-reduced-motion.js";
|
|
79
|
+
export * from "./lib/browser-clipboard.js";
|
|
80
|
+
export * from "./lib/browser-navigation.js";
|
|
81
|
+
export * from "./lib/file-acceptance.js";
|
|
82
|
+
export * from "./lib/format-file-size.js";
|
|
83
|
+
export * from "./lib/fuzzy-search.js";
|
|
84
|
+
export * from "./lib/horizontal-swipe.js";
|
|
85
|
+
export * from "./lib/in-app-browser.js";
|
|
86
|
+
export * from "./lib/month-grid.js";
|
|
87
|
+
export * from "./lib/name-initials.js";
|
|
88
|
+
export { parseRichTextSegments, normalizeMarkdownUrl, normalizeAutolinkUrl } from "./lib/rich-text/link-markdown.js";
|
|
89
|
+
export { RICH_TEXT_SEGMENT_TYPE, RICH_LINK_KIND } from "./lib/rich-text/link-markdown-constants.js";
|
|
90
|
+
export * from "./lib/rich-text/rich-markdown.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser clipboard adapter with feature detection. The async Clipboard API
|
|
3
|
+
* is preferred; when it is missing (older WebKit, insecure context) or
|
|
4
|
+
* rejects, a hidden textarea plus `document.execCommand("copy")` is tried,
|
|
5
|
+
* which WebKit still honours inside a user gesture.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Copies text to the clipboard.
|
|
9
|
+
*
|
|
10
|
+
* @param text - Text to copy.
|
|
11
|
+
* @returns True when the text reached the clipboard.
|
|
12
|
+
*/
|
|
13
|
+
export declare function copyTextToClipboard(text: string): Promise<boolean>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser clipboard adapter with feature detection. The async Clipboard API
|
|
3
|
+
* is preferred; when it is missing (older WebKit, insecure context) or
|
|
4
|
+
* rejects, a hidden textarea plus `document.execCommand("copy")` is tried,
|
|
5
|
+
* which WebKit still honours inside a user gesture.
|
|
6
|
+
*/
|
|
7
|
+
const FALLBACK_COPY_COMMAND = "copy";
|
|
8
|
+
const FALLBACK_TEXTAREA_TAG = "textarea";
|
|
9
|
+
const FALLBACK_READONLY_ATTRIBUTE = "readonly";
|
|
10
|
+
const OFFSCREEN_POSITION = "fixed";
|
|
11
|
+
const OFFSCREEN_OFFSET = "-9999px";
|
|
12
|
+
function copyWithSelectionFallback(text) {
|
|
13
|
+
if (typeof document.execCommand !== "function") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const textarea = document.createElement(FALLBACK_TEXTAREA_TAG);
|
|
17
|
+
textarea.value = text;
|
|
18
|
+
textarea.setAttribute(FALLBACK_READONLY_ATTRIBUTE, "");
|
|
19
|
+
textarea.style.position = OFFSCREEN_POSITION;
|
|
20
|
+
textarea.style.top = OFFSCREEN_OFFSET;
|
|
21
|
+
document.body.appendChild(textarea);
|
|
22
|
+
textarea.select();
|
|
23
|
+
// iOS Safari needs an explicit range for read-only fields.
|
|
24
|
+
textarea.setSelectionRange(0, text.length);
|
|
25
|
+
try {
|
|
26
|
+
return document.execCommand(FALLBACK_COPY_COMMAND);
|
|
27
|
+
} catch {
|
|
28
|
+
// The legacy command can throw when copying is blocked; report failure.
|
|
29
|
+
return false;
|
|
30
|
+
} finally {
|
|
31
|
+
textarea.remove();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Copies text to the clipboard.
|
|
36
|
+
*
|
|
37
|
+
* @param text - Text to copy.
|
|
38
|
+
* @returns True when the text reached the clipboard.
|
|
39
|
+
*/
|
|
40
|
+
export async function copyTextToClipboard(text) {
|
|
41
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
42
|
+
try {
|
|
43
|
+
await navigator.clipboard.writeText(text);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
return copyWithSelectionFallback(text);
|
|
48
|
+
}
|
|
@@ -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,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;
|