@tensor_1/react 2.18.0 → 2.19.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/README.md +102 -1
- package/dist/components/Comments.d.ts +18 -2
- package/dist/components/Comments.js +37 -12
- package/dist/components/EditorIcons.d.ts +39 -0
- package/dist/components/EditorIcons.js +51 -0
- package/dist/components/EmojiGlyph.d.ts +10 -0
- package/dist/components/EmojiGlyph.js +18 -0
- package/dist/components/EmojiPicker.d.ts +60 -0
- package/dist/components/EmojiPicker.js +277 -0
- package/dist/components/Markdown.d.ts +36 -0
- package/dist/components/Markdown.js +104 -0
- package/dist/components/ReactionBar.d.ts +57 -0
- package/dist/components/ReactionBar.js +81 -0
- package/dist/components/RelationshipGraph.js +23 -4
- package/dist/components/RichTextEditor.d.ts +128 -0
- package/dist/components/RichTextEditor.js +954 -0
- package/dist/components/SaveStatus.d.ts +32 -0
- package/dist/components/SaveStatus.js +21 -0
- package/dist/components/Tabs.d.ts +10 -3
- package/dist/components/Tabs.js +5 -3
- package/dist/emoji-image-WQLh8ZQP.d.cts +128 -0
- package/dist/emoji-image.d.ts +25 -0
- package/dist/emoji-image.js +80 -0
- package/dist/emoji-model.d.ts +66 -0
- package/dist/emoji-model.js +108 -0
- package/dist/emoji.cjs +871 -0
- package/dist/emoji.d.cts +57 -0
- package/dist/emoji.d.ts +6 -0
- package/dist/emoji.js +18 -0
- package/dist/index.cjs +587 -519
- package/dist/index.d.cts +59 -64
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/markdown-model.d.ts +75 -0
- package/dist/markdown-model.js +438 -0
- package/dist/markdown.cjs +585 -0
- package/dist/markdown.d.cts +115 -0
- package/dist/markdown.d.ts +4 -0
- package/dist/markdown.js +9 -0
- package/dist/overlay-position-CEGSz6Z2.d.cts +59 -0
- package/dist/rich-text/emoji-items.d.ts +14 -0
- package/dist/rich-text/emoji-items.js +7 -0
- package/dist/rich-text/pipeline.d.ts +91 -0
- package/dist/rich-text/pipeline.js +379 -0
- package/dist/rich-text/round-trip.d.ts +24 -0
- package/dist/rich-text/round-trip.js +11 -0
- package/dist/rich-text.cjs +2578 -0
- package/dist/rich-text.d.cts +148 -0
- package/dist/rich-text.d.ts +8 -0
- package/dist/rich-text.js +10 -0
- package/dist/url-policy.d.ts +9 -0
- package/dist/url-policy.js +11 -0
- package/package.json +82 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
type SaveStatusState = 'idle' | 'saving' | 'saved' | 'failed';
|
|
5
|
+
/** Every visible or announced string. Pass translations; English defaults fill the gaps. */
|
|
6
|
+
interface SaveStatusLabels {
|
|
7
|
+
saving: string;
|
|
8
|
+
saved: string;
|
|
9
|
+
failed: string;
|
|
10
|
+
retry: string;
|
|
11
|
+
}
|
|
12
|
+
declare const DEFAULT_SAVE_STATUS_LABELS: SaveStatusLabels;
|
|
13
|
+
interface SaveStatusProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
14
|
+
/** Where the save is. `idle` shows nothing but keeps the live region in place. */
|
|
15
|
+
state: SaveStatusState;
|
|
16
|
+
labels?: Partial<SaveStatusLabels>;
|
|
17
|
+
/** Offered as a button only while `state` is `failed`. */
|
|
18
|
+
onRetry?: () => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Inline save feedback over `.uix-save-status`: saving → saved → failed, with a retry.
|
|
22
|
+
* For autosave and inline edits, where a toast would be too loud and silence leaves the
|
|
23
|
+
* user guessing (LD-13).
|
|
24
|
+
*
|
|
25
|
+
* The status text sits in a polite live region that stays mounted in every state,
|
|
26
|
+
* including `idle`. A region inserted together with its text announces nothing, so
|
|
27
|
+
* rendering null while idle would silence the first "Saving…". The retry button sits
|
|
28
|
+
* outside the region so its label is not read out on every change.
|
|
29
|
+
*/
|
|
30
|
+
declare function SaveStatus({ state, labels, onRetry, className, ...props }: SaveStatusProps): react.JSX.Element;
|
|
31
|
+
|
|
32
|
+
export { DEFAULT_SAVE_STATUS_LABELS, SaveStatus, type SaveStatusLabels, type SaveStatusProps, type SaveStatusState };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../cx.js";
|
|
3
|
+
const DEFAULT_SAVE_STATUS_LABELS = {
|
|
4
|
+
saving: "Saving\u2026",
|
|
5
|
+
saved: "Saved",
|
|
6
|
+
failed: "Could not save",
|
|
7
|
+
retry: "Try again"
|
|
8
|
+
};
|
|
9
|
+
function SaveStatus({ state, labels, onRetry, className, ...props }) {
|
|
10
|
+
const text = { ...DEFAULT_SAVE_STATUS_LABELS, ...labels };
|
|
11
|
+
const message = state === "idle" ? "" : text[state];
|
|
12
|
+
return /* @__PURE__ */ jsxs("div", { className: cx("uix-save-status", className), "data-state": state, ...props, children: [
|
|
13
|
+
/* @__PURE__ */ jsx("span", { className: "uix-save-status__icon", "aria-hidden": "true" }),
|
|
14
|
+
/* @__PURE__ */ jsx("span", { className: "uix-save-status__text", role: "status", "aria-live": "polite", children: message }),
|
|
15
|
+
state === "failed" && onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--link uix-btn--sm uix-save-status__retry", onClick: onRetry, children: text.retry })
|
|
16
|
+
] });
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
DEFAULT_SAVE_STATUS_LABELS,
|
|
20
|
+
SaveStatus
|
|
21
|
+
};
|
|
@@ -19,13 +19,20 @@ interface TabPanelProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
19
19
|
/** The owning `<Tab>`'s value — the panel renders only while that tab is selected. */
|
|
20
20
|
value: string;
|
|
21
21
|
children?: ReactNode;
|
|
22
|
+
/**
|
|
23
|
+
* Keep the panel mounted while another tab is selected, `hidden` instead of removed.
|
|
24
|
+
* Opt in when leaving the tab would throw away work or a fetch the user already paid
|
|
25
|
+
* for — a loaded list, a half-filled form, a scroll position. Off by default: the
|
|
26
|
+
* panel's subtree then costs nothing until it is first shown.
|
|
27
|
+
*/
|
|
28
|
+
keepMounted?: boolean;
|
|
22
29
|
}
|
|
23
30
|
/**
|
|
24
31
|
* The tabpanel paired with a `<Tab>` by `value` (UIX-A11Y-2): labelled by its tab,
|
|
25
32
|
* `tabIndex 0` so Tab from the tablist lands in the content, rendered only while
|
|
26
|
-
* selected
|
|
27
|
-
* or anywhere the Tabs context reaches.
|
|
33
|
+
* selected — or kept `hidden` with `keepMounted`. Place it as a direct child of
|
|
34
|
+
* `<Tabs>` — it is hoisted out of the tablist — or anywhere the Tabs context reaches.
|
|
28
35
|
*/
|
|
29
|
-
declare function TabPanel({ value, children, ...props }: TabPanelProps): react.JSX.Element | null;
|
|
36
|
+
declare function TabPanel({ value, children, keepMounted, ...props }: TabPanelProps): react.JSX.Element | null;
|
|
30
37
|
|
|
31
38
|
export { Tab, TabPanel, type TabPanelProps, type TabProps, Tabs, type TabsProps };
|
package/dist/components/Tabs.js
CHANGED
|
@@ -85,16 +85,18 @@ function Tab({ value, children, disabled, className, onClick, ...props }) {
|
|
|
85
85
|
}
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
|
-
function TabPanel({ value, children, ...props }) {
|
|
88
|
+
function TabPanel({ value, children, keepMounted, ...props }) {
|
|
89
89
|
const ctx = useContext(TabsCtx);
|
|
90
|
-
|
|
90
|
+
const selected = ctx.value === value;
|
|
91
|
+
if (!selected && !keepMounted) return null;
|
|
91
92
|
return /* @__PURE__ */ jsx(
|
|
92
93
|
"div",
|
|
93
94
|
{
|
|
94
95
|
role: "tabpanel",
|
|
95
96
|
id: ctx.baseId ? `${ctx.baseId}-panel-${value}` : void 0,
|
|
96
97
|
"aria-labelledby": ctx.baseId ? `${ctx.baseId}-tab-${value}` : void 0,
|
|
97
|
-
|
|
98
|
+
hidden: !selected || void 0,
|
|
99
|
+
tabIndex: selected ? 0 : -1,
|
|
98
100
|
...props,
|
|
99
101
|
children
|
|
100
102
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactElement } from 'react';
|
|
3
|
+
import { P as Placement } from './overlay-position-CEGSz6Z2.cjs';
|
|
4
|
+
|
|
5
|
+
type EmojiLocale = 'en' | 'de';
|
|
6
|
+
interface EmojiEntry {
|
|
7
|
+
/** The Unicode emoji — the only form ever stored. */
|
|
8
|
+
emoji: string;
|
|
9
|
+
/** Localized name ("thumbs up" / "Daumen hoch"). */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Localized search terms. */
|
|
12
|
+
tags: ReadonlyArray<string>;
|
|
13
|
+
/** Index into `EmojiData.groups`. */
|
|
14
|
+
group: number;
|
|
15
|
+
}
|
|
16
|
+
interface EmojiGroup {
|
|
17
|
+
key: string;
|
|
18
|
+
/** Localized category name. */
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
21
|
+
interface EmojiData {
|
|
22
|
+
locale: EmojiLocale;
|
|
23
|
+
groups: ReadonlyArray<EmojiGroup>;
|
|
24
|
+
emojis: ReadonlyArray<EmojiEntry>;
|
|
25
|
+
/** Localized names by emoji, for aria-labels of quick picks and recents. */
|
|
26
|
+
names: ReadonlyMap<string, string>;
|
|
27
|
+
}
|
|
28
|
+
type EmojiDataLoader = (locale: EmojiLocale) => Promise<EmojiData>;
|
|
29
|
+
/** Shape of `emojibase-data/<locale>/compact.json` entries (the fields we read). */
|
|
30
|
+
interface CompactEmoji {
|
|
31
|
+
unicode: string;
|
|
32
|
+
label: string;
|
|
33
|
+
group?: number;
|
|
34
|
+
order?: number;
|
|
35
|
+
tags?: string[];
|
|
36
|
+
}
|
|
37
|
+
/** Shape of `emojibase-data/<locale>/messages.json` (the fields we read). */
|
|
38
|
+
interface EmojiMessages {
|
|
39
|
+
groups: ReadonlyArray<{
|
|
40
|
+
key: string;
|
|
41
|
+
message: string;
|
|
42
|
+
order: number;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
/** Build picker data from the emojibase JSON of one locale. */
|
|
46
|
+
declare function buildEmojiData(locale: EmojiLocale, compact: ReadonlyArray<CompactEmoji>, messages: EmojiMessages): EmojiData;
|
|
47
|
+
/**
|
|
48
|
+
* Default loader: dynamic imports of the emojibase JSON, one chunk per locale, cached.
|
|
49
|
+
* Requires the optional peer `emojibase-data`.
|
|
50
|
+
*/
|
|
51
|
+
declare const loadEmojiData: EmojiDataLoader;
|
|
52
|
+
/**
|
|
53
|
+
* Search by name and tags. Word-prefix matches rank before substring matches;
|
|
54
|
+
* dataset order breaks ties. Case and diacritics are folded, so "uberr" finds "Überraschung".
|
|
55
|
+
*/
|
|
56
|
+
declare function searchEmoji(data: EmojiData, query: string, limit?: number): EmojiEntry[];
|
|
57
|
+
|
|
58
|
+
interface EmojiPickerLabels {
|
|
59
|
+
/** Accessible name of the picker dialog. */
|
|
60
|
+
dialog: string;
|
|
61
|
+
/** Search field label and placeholder. */
|
|
62
|
+
search: string;
|
|
63
|
+
/** Heading of the recently-used section. */
|
|
64
|
+
recent: string;
|
|
65
|
+
/** Heading of the quick-pick section. */
|
|
66
|
+
quickPicks: string;
|
|
67
|
+
/** Accessible name of the category navigation. */
|
|
68
|
+
categories: string;
|
|
69
|
+
/** Shown when a search matches nothing. */
|
|
70
|
+
noResults: string;
|
|
71
|
+
/** Shown while the emoji data loads. */
|
|
72
|
+
loading: string;
|
|
73
|
+
/** Shown when the emoji data cannot be loaded. */
|
|
74
|
+
loadFailed: string;
|
|
75
|
+
}
|
|
76
|
+
declare const DEFAULT_EMOJI_PICKER_LABELS: EmojiPickerLabels;
|
|
77
|
+
interface EmojiPickerProps {
|
|
78
|
+
/** Called with the chosen emoji as Unicode text. The picker then closes. */
|
|
79
|
+
onSelect: (emoji: string) => void;
|
|
80
|
+
/** The button that opens the picker. It receives `aria-haspopup`, `aria-expanded` and a click handler. */
|
|
81
|
+
trigger: ReactElement;
|
|
82
|
+
labels?: Partial<EmojiPickerLabels>;
|
|
83
|
+
/** Language of emoji names and search terms. Default `'en'`. */
|
|
84
|
+
locale?: EmojiLocale;
|
|
85
|
+
/** Emoji shown first, before recents and categories. */
|
|
86
|
+
quickPicks?: ReadonlyArray<string>;
|
|
87
|
+
/**
|
|
88
|
+
* Same-origin folder of fallback PNGs for emoji this device cannot draw (see
|
|
89
|
+
* `emojiImageFileName` for the naming). Unset = native emoji only.
|
|
90
|
+
*/
|
|
91
|
+
emojiImageBaseUrl?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Replace the emojibase loader (tests, or another bundled dataset). The default loads
|
|
94
|
+
* a bundled chunk through dynamic `import()`; it never fetches a URL.
|
|
95
|
+
*/
|
|
96
|
+
loadData?: EmojiDataLoader;
|
|
97
|
+
/** localStorage key for recently used emoji. */
|
|
98
|
+
recentStorageKey?: string;
|
|
99
|
+
placement?: Placement;
|
|
100
|
+
onOpenChange?: (open: boolean) => void;
|
|
101
|
+
className?: string;
|
|
102
|
+
}
|
|
103
|
+
/** Fixed column count; the CSS grid uses the same number. */
|
|
104
|
+
declare const EMOJI_GRID_COLUMNS = 8;
|
|
105
|
+
/**
|
|
106
|
+
* Emoji picker in a native popover: search, quick picks, recently used and the
|
|
107
|
+
* Unicode categories, with names in English or German. The emoji dataset loads on
|
|
108
|
+
* first open. The grid is one tab stop; arrow keys, Home and End move within it.
|
|
109
|
+
*/
|
|
110
|
+
declare function EmojiPicker({ onSelect, trigger, labels: labelOverrides, locale, quickPicks, loadData, recentStorageKey, placement, onOpenChange, className, emojiImageBaseUrl, }: EmojiPickerProps): react.JSX.Element;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The usable base path, or `undefined` for unset/rejected values. Accepts "/path" and
|
|
114
|
+
* "./path"; rejects schemes ("https:", "data:", "javascript:"), protocol-relative
|
|
115
|
+
* "//host", backslashes and whitespace. Trailing slashes are removed.
|
|
116
|
+
*/
|
|
117
|
+
declare function normalizeEmojiImageBaseUrl(value: string | undefined): string | undefined;
|
|
118
|
+
/** `👩💻` → `1f469-200d-1f4bb.png` */
|
|
119
|
+
declare function emojiImageFileName(emoji: string): string;
|
|
120
|
+
/**
|
|
121
|
+
* Whether this device draws `emoji` in colour. Draws it on a small canvas and looks for a
|
|
122
|
+
* coloured pixel: a missing glyph (tofu) or a monochrome fallback font is drawn in plain
|
|
123
|
+
* black. Without a DOM or canvas (server, tests) the answer is `true`, so nothing is
|
|
124
|
+
* swapped for an image there. Results are cached per emoji.
|
|
125
|
+
*/
|
|
126
|
+
declare function canRenderEmoji(emoji: string): boolean;
|
|
127
|
+
|
|
128
|
+
export { type CompactEmoji as C, DEFAULT_EMOJI_PICKER_LABELS as D, type EmojiPickerLabels as E, type EmojiLocale as a, type EmojiDataLoader as b, EMOJI_GRID_COLUMNS as c, type EmojiData as d, type EmojiEntry as e, type EmojiGroup as f, type EmojiMessages as g, EmojiPicker as h, type EmojiPickerProps as i, buildEmojiData as j, canRenderEmoji as k, emojiImageFileName as l, loadEmojiData as m, normalizeEmojiImageBaseUrl as n, searchEmoji as s };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usable base path, or `undefined` for unset/rejected values. Accepts "/path" and
|
|
3
|
+
* "./path"; rejects schemes ("https:", "data:", "javascript:"), protocol-relative
|
|
4
|
+
* "//host", backslashes and whitespace. Trailing slashes are removed.
|
|
5
|
+
*/
|
|
6
|
+
declare function normalizeEmojiImageBaseUrl(value: string | undefined): string | undefined;
|
|
7
|
+
/** `👩💻` → `1f469-200d-1f4bb.png` */
|
|
8
|
+
declare function emojiImageFileName(emoji: string): string;
|
|
9
|
+
declare function emojiImageSrc(base: string, emoji: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Whether this device draws `emoji` in colour. Draws it on a small canvas and looks for a
|
|
12
|
+
* coloured pixel: a missing glyph (tofu) or a monochrome fallback font is drawn in plain
|
|
13
|
+
* black. Without a DOM or canvas (server, tests) the answer is `true`, so nothing is
|
|
14
|
+
* swapped for an image there. Results are cached per emoji.
|
|
15
|
+
*/
|
|
16
|
+
declare function canRenderEmoji(emoji: string): boolean;
|
|
17
|
+
/** Test hook: forget cached answers and the canvas. */
|
|
18
|
+
declare function resetEmojiSupportCache(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Emoji sequences in text: RGI emoji where the engine supports the `v` flag, otherwise
|
|
21
|
+
* pictographs with their modifiers, ZWJ joins and flag pairs.
|
|
22
|
+
*/
|
|
23
|
+
declare function emojiPattern(): RegExp;
|
|
24
|
+
|
|
25
|
+
export { canRenderEmoji, emojiImageFileName, emojiImageSrc, emojiPattern, normalizeEmojiImageBaseUrl, resetEmojiSupportCache };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const warned = /* @__PURE__ */ new Set();
|
|
2
|
+
const devWarn = (message) => {
|
|
3
|
+
if (warned.has(message) || typeof process === "undefined" || process.env.NODE_ENV === "production") return;
|
|
4
|
+
warned.add(message);
|
|
5
|
+
console.warn(message);
|
|
6
|
+
};
|
|
7
|
+
function normalizeEmojiImageBaseUrl(value) {
|
|
8
|
+
if (value === void 0 || value === "") return void 0;
|
|
9
|
+
const sameOrigin = /^(?:\/(?![/\\])|\.\/)/.test(value) && !/[\\\s]/.test(value) && !/^[^/?#]*:/.test(value);
|
|
10
|
+
if (!sameOrigin) {
|
|
11
|
+
devWarn(`uix: emojiImageBaseUrl "${value}" was ignored \u2014 only same-origin paths starting with "/" or "./" are allowed.`);
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
return value.replace(/\/+$/, "") || "/";
|
|
15
|
+
}
|
|
16
|
+
function emojiImageFileName(emoji) {
|
|
17
|
+
return `${Array.from(emoji, (ch) => ch.codePointAt(0).toString(16)).join("-")}.png`;
|
|
18
|
+
}
|
|
19
|
+
function emojiImageSrc(base, emoji) {
|
|
20
|
+
return `${base === "/" ? "" : base}/${emojiImageFileName(emoji)}`;
|
|
21
|
+
}
|
|
22
|
+
const support = /* @__PURE__ */ new Map();
|
|
23
|
+
let context;
|
|
24
|
+
const SIZE = 20;
|
|
25
|
+
function canRenderEmoji(emoji) {
|
|
26
|
+
const cached = support.get(emoji);
|
|
27
|
+
if (cached !== void 0) return cached;
|
|
28
|
+
if (typeof document === "undefined") return true;
|
|
29
|
+
if (context === void 0) {
|
|
30
|
+
try {
|
|
31
|
+
const canvas = document.createElement("canvas");
|
|
32
|
+
canvas.width = SIZE;
|
|
33
|
+
canvas.height = SIZE;
|
|
34
|
+
context = typeof canvas.getContext === "function" ? canvas.getContext("2d", { willReadFrequently: true }) : null;
|
|
35
|
+
} catch {
|
|
36
|
+
context = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!context) return true;
|
|
40
|
+
let result = true;
|
|
41
|
+
try {
|
|
42
|
+
context.clearRect(0, 0, SIZE, SIZE);
|
|
43
|
+
context.fillStyle = "#000";
|
|
44
|
+
context.textBaseline = "top";
|
|
45
|
+
context.font = `${SIZE - 4}px sans-serif`;
|
|
46
|
+
context.fillText(emoji, 0, 0);
|
|
47
|
+
const { data } = context.getImageData(0, 0, SIZE, SIZE);
|
|
48
|
+
result = false;
|
|
49
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
50
|
+
const [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]];
|
|
51
|
+
if (a > 0 && (Math.abs(r - g) > 12 || Math.abs(g - b) > 12)) {
|
|
52
|
+
result = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
result = true;
|
|
58
|
+
}
|
|
59
|
+
support.set(emoji, result);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function resetEmojiSupportCache() {
|
|
63
|
+
support.clear();
|
|
64
|
+
context = void 0;
|
|
65
|
+
}
|
|
66
|
+
function emojiPattern() {
|
|
67
|
+
try {
|
|
68
|
+
return new RegExp("\\p{RGI_Emoji}", "gv");
|
|
69
|
+
} catch {
|
|
70
|
+
return /\p{Regional_Indicator}{2}|(?:\p{Extended_Pictographic}|[#*0-9]️?⃣)(?:️|\p{Emoji_Modifier})*(?:\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})*)*/gu;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
canRenderEmoji,
|
|
75
|
+
emojiImageFileName,
|
|
76
|
+
emojiImageSrc,
|
|
77
|
+
emojiPattern,
|
|
78
|
+
normalizeEmojiImageBaseUrl,
|
|
79
|
+
resetEmojiSupportCache
|
|
80
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
type EmojiLocale = 'en' | 'de';
|
|
2
|
+
interface EmojiEntry {
|
|
3
|
+
/** The Unicode emoji — the only form ever stored. */
|
|
4
|
+
emoji: string;
|
|
5
|
+
/** Localized name ("thumbs up" / "Daumen hoch"). */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Localized search terms. */
|
|
8
|
+
tags: ReadonlyArray<string>;
|
|
9
|
+
/** Index into `EmojiData.groups`. */
|
|
10
|
+
group: number;
|
|
11
|
+
}
|
|
12
|
+
interface EmojiGroup {
|
|
13
|
+
key: string;
|
|
14
|
+
/** Localized category name. */
|
|
15
|
+
name: string;
|
|
16
|
+
}
|
|
17
|
+
interface EmojiData {
|
|
18
|
+
locale: EmojiLocale;
|
|
19
|
+
groups: ReadonlyArray<EmojiGroup>;
|
|
20
|
+
emojis: ReadonlyArray<EmojiEntry>;
|
|
21
|
+
/** Localized names by emoji, for aria-labels of quick picks and recents. */
|
|
22
|
+
names: ReadonlyMap<string, string>;
|
|
23
|
+
}
|
|
24
|
+
type EmojiDataLoader = (locale: EmojiLocale) => Promise<EmojiData>;
|
|
25
|
+
/** Shape of `emojibase-data/<locale>/compact.json` entries (the fields we read). */
|
|
26
|
+
interface CompactEmoji {
|
|
27
|
+
unicode: string;
|
|
28
|
+
label: string;
|
|
29
|
+
group?: number;
|
|
30
|
+
order?: number;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Shape of `emojibase-data/<locale>/messages.json` (the fields we read). */
|
|
34
|
+
interface EmojiMessages {
|
|
35
|
+
groups: ReadonlyArray<{
|
|
36
|
+
key: string;
|
|
37
|
+
message: string;
|
|
38
|
+
order: number;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
/** Build picker data from the emojibase JSON of one locale. */
|
|
42
|
+
declare function buildEmojiData(locale: EmojiLocale, compact: ReadonlyArray<CompactEmoji>, messages: EmojiMessages): EmojiData;
|
|
43
|
+
/**
|
|
44
|
+
* Default loader: dynamic imports of the emojibase JSON, one chunk per locale, cached.
|
|
45
|
+
* Requires the optional peer `emojibase-data`.
|
|
46
|
+
*/
|
|
47
|
+
declare const loadEmojiData: EmojiDataLoader;
|
|
48
|
+
/**
|
|
49
|
+
* Search by name and tags. Word-prefix matches rank before substring matches;
|
|
50
|
+
* dataset order breaks ties. Case and diacritics are folded, so "uberr" finds "Überraschung".
|
|
51
|
+
*/
|
|
52
|
+
declare function searchEmoji(data: EmojiData, query: string, limit?: number): EmojiEntry[];
|
|
53
|
+
declare const DEFAULT_RECENT_KEY = "uix-emoji-recent";
|
|
54
|
+
/** Recently used emoji from localStorage; empty when storage is unavailable. */
|
|
55
|
+
declare function readRecentEmoji(key?: string): string[];
|
|
56
|
+
/** Move `emoji` to the front of the recent list and persist it (best effort). */
|
|
57
|
+
declare function pushRecentEmoji(emoji: string, key?: string): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Next focus index in a grid of `count` cells with `columns` columns, for arrow keys,
|
|
60
|
+
* Home and End. Returns `null` for other keys.
|
|
61
|
+
*/
|
|
62
|
+
declare function emojiGridMove(index: number, key: string, count: number, columns: number): number | null;
|
|
63
|
+
/** Fill `{name}` placeholders in a label template. */
|
|
64
|
+
declare function formatLabel(template: string, values: Readonly<Record<string, string | number>>): string;
|
|
65
|
+
|
|
66
|
+
export { type CompactEmoji, DEFAULT_RECENT_KEY, type EmojiData, type EmojiDataLoader, type EmojiEntry, type EmojiGroup, type EmojiLocale, type EmojiMessages, buildEmojiData, emojiGridMove, formatLabel, loadEmojiData, pushRecentEmoji, readRecentEmoji, searchEmoji };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const COMPONENT_GROUP_KEY = "component";
|
|
2
|
+
const capitalize = (s) => s ? s[0].toLocaleUpperCase() + s.slice(1) : s;
|
|
3
|
+
function buildEmojiData(locale, compact, messages) {
|
|
4
|
+
const sourceGroups = [...messages.groups].sort((a, b) => a.order - b.order);
|
|
5
|
+
const groups = [];
|
|
6
|
+
const groupIndex = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const g of sourceGroups) {
|
|
8
|
+
if (g.key === COMPONENT_GROUP_KEY) continue;
|
|
9
|
+
groupIndex.set(g.order, groups.length);
|
|
10
|
+
groups.push({ key: g.key, name: capitalize(g.message) });
|
|
11
|
+
}
|
|
12
|
+
const emojis = [];
|
|
13
|
+
const names = /* @__PURE__ */ new Map();
|
|
14
|
+
const sorted = [...compact].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
15
|
+
for (const item of sorted) {
|
|
16
|
+
if (item.group === void 0) continue;
|
|
17
|
+
const group = groupIndex.get(item.group);
|
|
18
|
+
if (group === void 0) continue;
|
|
19
|
+
const entry = { emoji: item.unicode, name: item.label, tags: item.tags ?? [], group };
|
|
20
|
+
emojis.push(entry);
|
|
21
|
+
names.set(item.unicode, item.label);
|
|
22
|
+
names.set(item.unicode.replace(/️/g, ""), item.label);
|
|
23
|
+
}
|
|
24
|
+
return { locale, groups, emojis, names };
|
|
25
|
+
}
|
|
26
|
+
const cache = /* @__PURE__ */ new Map();
|
|
27
|
+
const loadEmojiData = (locale) => {
|
|
28
|
+
let pending = cache.get(locale);
|
|
29
|
+
if (!pending) {
|
|
30
|
+
const files = locale === "de" ? Promise.all([import("emojibase-data/de/compact.json"), import("emojibase-data/de/messages.json")]) : Promise.all([import("emojibase-data/en/compact.json"), import("emojibase-data/en/messages.json")]);
|
|
31
|
+
pending = files.then(
|
|
32
|
+
([compact, messages]) => buildEmojiData(
|
|
33
|
+
locale,
|
|
34
|
+
compact.default ?? compact,
|
|
35
|
+
messages.default ?? messages
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
pending.catch(() => cache.delete(locale));
|
|
39
|
+
cache.set(locale, pending);
|
|
40
|
+
}
|
|
41
|
+
return pending;
|
|
42
|
+
};
|
|
43
|
+
const fold = (s) => s.toLocaleLowerCase().normalize("NFD").replace(/\p{M}/gu, "");
|
|
44
|
+
function searchEmoji(data, query, limit = 64) {
|
|
45
|
+
const q = fold(query.trim());
|
|
46
|
+
if (!q) return [];
|
|
47
|
+
const prefix = [];
|
|
48
|
+
const contains = [];
|
|
49
|
+
for (const entry of data.emojis) {
|
|
50
|
+
const name = fold(entry.name);
|
|
51
|
+
const words = [...name.split(/[\s:,-]+/), ...entry.tags.map(fold)];
|
|
52
|
+
if (words.some((w) => w.startsWith(q)) || name.startsWith(q)) prefix.push(entry);
|
|
53
|
+
else if (name.includes(q)) contains.push(entry);
|
|
54
|
+
if (prefix.length >= limit) break;
|
|
55
|
+
}
|
|
56
|
+
return [...prefix, ...contains].slice(0, limit);
|
|
57
|
+
}
|
|
58
|
+
const DEFAULT_RECENT_KEY = "uix-emoji-recent";
|
|
59
|
+
const RECENT_MAX = 16;
|
|
60
|
+
function readRecentEmoji(key = DEFAULT_RECENT_KEY) {
|
|
61
|
+
try {
|
|
62
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
63
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
64
|
+
return Array.isArray(list) ? list.filter((e) => typeof e === "string").slice(0, RECENT_MAX) : [];
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function pushRecentEmoji(emoji, key = DEFAULT_RECENT_KEY) {
|
|
70
|
+
const next = [emoji, ...readRecentEmoji(key).filter((e) => e !== emoji)].slice(0, RECENT_MAX);
|
|
71
|
+
try {
|
|
72
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(next));
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
function emojiGridMove(index, key, count, columns) {
|
|
78
|
+
if (count === 0) return null;
|
|
79
|
+
switch (key) {
|
|
80
|
+
case "ArrowRight":
|
|
81
|
+
return Math.min(index + 1, count - 1);
|
|
82
|
+
case "ArrowLeft":
|
|
83
|
+
return Math.max(index - 1, 0);
|
|
84
|
+
case "ArrowDown":
|
|
85
|
+
return Math.min(index + columns, count - 1);
|
|
86
|
+
case "ArrowUp":
|
|
87
|
+
return index - columns < 0 ? null : index - columns;
|
|
88
|
+
case "Home":
|
|
89
|
+
return 0;
|
|
90
|
+
case "End":
|
|
91
|
+
return count - 1;
|
|
92
|
+
default:
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function formatLabel(template, values) {
|
|
97
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
DEFAULT_RECENT_KEY,
|
|
101
|
+
buildEmojiData,
|
|
102
|
+
emojiGridMove,
|
|
103
|
+
formatLabel,
|
|
104
|
+
loadEmojiData,
|
|
105
|
+
pushRecentEmoji,
|
|
106
|
+
readRecentEmoji,
|
|
107
|
+
searchEmoji
|
|
108
|
+
};
|