@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,277 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cloneElement, isValidElement, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { Popover } from "./Popover.js";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_RECENT_KEY,
|
|
8
|
+
emojiGridMove,
|
|
9
|
+
loadEmojiData,
|
|
10
|
+
pushRecentEmoji,
|
|
11
|
+
readRecentEmoji,
|
|
12
|
+
searchEmoji
|
|
13
|
+
} from "../emoji-model.js";
|
|
14
|
+
import { useEmojiRenderer } from "./EmojiGlyph.js";
|
|
15
|
+
const DEFAULT_EMOJI_PICKER_LABELS = {
|
|
16
|
+
dialog: "Choose an emoji",
|
|
17
|
+
search: "Search emoji",
|
|
18
|
+
recent: "Recently used",
|
|
19
|
+
quickPicks: "Frequently used",
|
|
20
|
+
categories: "Emoji categories",
|
|
21
|
+
noResults: "No emoji found",
|
|
22
|
+
loading: "Loading emoji\u2026",
|
|
23
|
+
loadFailed: "Emoji could not be loaded."
|
|
24
|
+
};
|
|
25
|
+
const EMOJI_GRID_COLUMNS = 8;
|
|
26
|
+
function EmojiPicker({
|
|
27
|
+
onSelect,
|
|
28
|
+
trigger,
|
|
29
|
+
labels: labelOverrides,
|
|
30
|
+
locale = "en",
|
|
31
|
+
quickPicks,
|
|
32
|
+
loadData = loadEmojiData,
|
|
33
|
+
recentStorageKey = DEFAULT_RECENT_KEY,
|
|
34
|
+
placement = "bottom-start",
|
|
35
|
+
onOpenChange,
|
|
36
|
+
className,
|
|
37
|
+
emojiImageBaseUrl
|
|
38
|
+
}) {
|
|
39
|
+
const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
|
|
40
|
+
const labels = { ...DEFAULT_EMOJI_PICKER_LABELS, ...labelOverrides };
|
|
41
|
+
const id = useId();
|
|
42
|
+
const popoverId = `${id}-picker`;
|
|
43
|
+
const anchorRef = useRef(null);
|
|
44
|
+
const searchRef = useRef(null);
|
|
45
|
+
const gridRef = useRef(null);
|
|
46
|
+
const pointerWasOpen = useRef(null);
|
|
47
|
+
const [open, setOpen] = useState(false);
|
|
48
|
+
const [data, setData] = useState(null);
|
|
49
|
+
const [failed, setFailed] = useState(false);
|
|
50
|
+
const [query, setQuery] = useState("");
|
|
51
|
+
const [recent, setRecent] = useState([]);
|
|
52
|
+
const [active, setActive] = useState(0);
|
|
53
|
+
const popoverEl = () => typeof document === "undefined" ? null : document.getElementById(popoverId);
|
|
54
|
+
const isOpen = (el) => {
|
|
55
|
+
try {
|
|
56
|
+
return !!el?.matches(":popover-open");
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const el = popoverEl();
|
|
63
|
+
if (!el) return;
|
|
64
|
+
const onToggle = (event) => {
|
|
65
|
+
const { newState } = event;
|
|
66
|
+
const nowOpen = newState ? newState === "open" : isOpen(el);
|
|
67
|
+
setOpen(nowOpen);
|
|
68
|
+
onOpenChange?.(nowOpen);
|
|
69
|
+
if (nowOpen) {
|
|
70
|
+
setRecent(readRecentEmoji(recentStorageKey));
|
|
71
|
+
setQuery("");
|
|
72
|
+
setActive(0);
|
|
73
|
+
requestAnimationFrame(() => searchRef.current?.focus());
|
|
74
|
+
} else if (el.contains(document.activeElement) || document.activeElement === document.body) {
|
|
75
|
+
focusTrigger();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
el.addEventListener("toggle", onToggle);
|
|
79
|
+
return () => el.removeEventListener("toggle", onToggle);
|
|
80
|
+
}, [popoverId, recentStorageKey, onOpenChange]);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!open || data?.locale === locale) return;
|
|
83
|
+
let live = true;
|
|
84
|
+
setFailed(false);
|
|
85
|
+
loadData(locale).then(
|
|
86
|
+
(loaded) => {
|
|
87
|
+
if (live) setData(loaded);
|
|
88
|
+
},
|
|
89
|
+
() => {
|
|
90
|
+
if (live) setFailed(true);
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
return () => {
|
|
94
|
+
live = false;
|
|
95
|
+
};
|
|
96
|
+
}, [open, locale, loadData, data?.locale]);
|
|
97
|
+
const focusTrigger = () => {
|
|
98
|
+
const el = anchorRef.current?.querySelector("button, [href], [tabindex]");
|
|
99
|
+
el?.focus();
|
|
100
|
+
};
|
|
101
|
+
const hide = () => {
|
|
102
|
+
const el = popoverEl();
|
|
103
|
+
if (el && isOpen(el)) el.hidePopover();
|
|
104
|
+
};
|
|
105
|
+
const sections = useMemo(() => {
|
|
106
|
+
if (query.trim()) {
|
|
107
|
+
return data ? [{ key: "results", title: labels.search, emojis: searchEmoji(data, query).map((e) => e.emoji) }] : [];
|
|
108
|
+
}
|
|
109
|
+
const list = [];
|
|
110
|
+
if (quickPicks?.length) list.push({ key: "quick", title: labels.quickPicks, emojis: [...new Set(quickPicks)] });
|
|
111
|
+
if (recent.length) list.push({ key: "recent", title: labels.recent, emojis: recent });
|
|
112
|
+
if (data) {
|
|
113
|
+
data.groups.forEach((group, index) => {
|
|
114
|
+
list.push({ key: group.key, title: group.name, emojis: data.emojis.filter((e) => e.group === index).map((e) => e.emoji) });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return list;
|
|
118
|
+
}, [query, data, quickPicks, recent, labels.search, labels.quickPicks, labels.recent]);
|
|
119
|
+
const flat = useMemo(() => sections.flatMap((s) => s.emojis), [sections]);
|
|
120
|
+
const sectionStart = useMemo(() => {
|
|
121
|
+
const starts = /* @__PURE__ */ new Map();
|
|
122
|
+
let at = 0;
|
|
123
|
+
for (const section of sections) {
|
|
124
|
+
starts.set(section.key, at);
|
|
125
|
+
at += section.emojis.length;
|
|
126
|
+
}
|
|
127
|
+
return starts;
|
|
128
|
+
}, [sections]);
|
|
129
|
+
const activeIndex = Math.min(active, Math.max(flat.length - 1, 0));
|
|
130
|
+
const choose = (emoji) => {
|
|
131
|
+
setRecent(pushRecentEmoji(emoji, recentStorageKey));
|
|
132
|
+
onSelect(emoji);
|
|
133
|
+
hide();
|
|
134
|
+
};
|
|
135
|
+
const focusCell = useCallback((index) => {
|
|
136
|
+
setActive(index);
|
|
137
|
+
const find = () => gridRef.current?.querySelector(`[data-emoji-index="${index}"]`);
|
|
138
|
+
const cell = find();
|
|
139
|
+
if (cell) cell.focus();
|
|
140
|
+
else requestAnimationFrame(() => find()?.focus());
|
|
141
|
+
}, []);
|
|
142
|
+
const onGridKeyDown = (event) => {
|
|
143
|
+
const target = event.target;
|
|
144
|
+
const index = Number(target.dataset.emojiIndex ?? -1);
|
|
145
|
+
if (index < 0) return;
|
|
146
|
+
const next = emojiGridMove(index, event.key, flat.length, EMOJI_GRID_COLUMNS);
|
|
147
|
+
if (event.key === "ArrowUp" && next === null) {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
searchRef.current?.focus();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (next === null) return;
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
focusCell(next);
|
|
155
|
+
};
|
|
156
|
+
const onSearchKeyDown = (event) => {
|
|
157
|
+
if (event.key === "Escape") {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
hide();
|
|
160
|
+
} else if (event.key === "ArrowDown" && flat.length) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
focusCell(0);
|
|
163
|
+
} else if (event.key === "Enter" && query.trim() && flat[0]) {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
choose(flat[0]);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const toggleFromTrigger = (event) => {
|
|
169
|
+
const el = popoverEl();
|
|
170
|
+
if (!el || event.defaultPrevented) return;
|
|
171
|
+
const was = pointerWasOpen.current;
|
|
172
|
+
pointerWasOpen.current = null;
|
|
173
|
+
if (was === true) return;
|
|
174
|
+
if (isOpen(el)) el.hidePopover();
|
|
175
|
+
else el.showPopover();
|
|
176
|
+
};
|
|
177
|
+
const triggerProps = isValidElement(trigger) ? trigger.props : {};
|
|
178
|
+
const triggerEl = isValidElement(trigger) ? cloneElement(trigger, {
|
|
179
|
+
"aria-haspopup": "dialog",
|
|
180
|
+
"aria-expanded": open,
|
|
181
|
+
"aria-controls": popoverId,
|
|
182
|
+
onPointerDown: (event) => {
|
|
183
|
+
pointerWasOpen.current = isOpen(popoverEl());
|
|
184
|
+
triggerProps.onPointerDown?.(event);
|
|
185
|
+
},
|
|
186
|
+
onClick: (event) => {
|
|
187
|
+
triggerProps.onClick?.(event);
|
|
188
|
+
toggleFromTrigger(event);
|
|
189
|
+
}
|
|
190
|
+
}) : trigger;
|
|
191
|
+
const names = data?.names;
|
|
192
|
+
let offset = 0;
|
|
193
|
+
const status = failed ? labels.loadFailed : !data && open ? labels.loading : query.trim() && data && flat.length === 0 ? labels.noResults : "";
|
|
194
|
+
return /* @__PURE__ */ jsxs("span", { ref: anchorRef, className: cx("uix-emoji-picker__anchor", className), children: [
|
|
195
|
+
triggerEl,
|
|
196
|
+
/* @__PURE__ */ jsx(
|
|
197
|
+
Popover,
|
|
198
|
+
{
|
|
199
|
+
id: popoverId,
|
|
200
|
+
anchor: anchorRef,
|
|
201
|
+
placement,
|
|
202
|
+
className: "uix-emoji-picker-popover",
|
|
203
|
+
role: "dialog",
|
|
204
|
+
"aria-label": labels.dialog,
|
|
205
|
+
children: open ? /* @__PURE__ */ jsxs("div", { className: "uix-emoji-picker uix-emoji-picker--full", lang: locale, children: [
|
|
206
|
+
/* @__PURE__ */ jsx(
|
|
207
|
+
"input",
|
|
208
|
+
{
|
|
209
|
+
ref: searchRef,
|
|
210
|
+
type: "search",
|
|
211
|
+
className: "uix-input uix-emoji-picker__search",
|
|
212
|
+
"aria-label": labels.search,
|
|
213
|
+
placeholder: labels.search,
|
|
214
|
+
value: query,
|
|
215
|
+
onChange: (event) => {
|
|
216
|
+
setQuery(event.target.value);
|
|
217
|
+
setActive(0);
|
|
218
|
+
},
|
|
219
|
+
onKeyDown: onSearchKeyDown,
|
|
220
|
+
autoComplete: "off",
|
|
221
|
+
spellCheck: false
|
|
222
|
+
}
|
|
223
|
+
),
|
|
224
|
+
data && !query.trim() ? /* @__PURE__ */ jsx("div", { className: "uix-emoji-picker__nav", role: "group", "aria-label": labels.categories, children: sections.map((section) => /* @__PURE__ */ jsx(
|
|
225
|
+
"button",
|
|
226
|
+
{
|
|
227
|
+
type: "button",
|
|
228
|
+
className: "uix-emoji-picker__nav-btn",
|
|
229
|
+
"aria-label": section.title,
|
|
230
|
+
title: section.title,
|
|
231
|
+
onClick: () => {
|
|
232
|
+
const start = section.emojis.length ? sectionStart.get(section.key) ?? -1 : -1;
|
|
233
|
+
gridRef.current?.querySelector(`[data-section="${section.key}"]`)?.scrollIntoView({ block: "start" });
|
|
234
|
+
if (start >= 0) focusCell(start);
|
|
235
|
+
},
|
|
236
|
+
children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: section.emojis[0] ? renderEmoji(section.emojis[0]) : null })
|
|
237
|
+
},
|
|
238
|
+
section.key
|
|
239
|
+
)) }) : null,
|
|
240
|
+
/* @__PURE__ */ jsx("div", { className: "uix-emoji-picker__grid", ref: gridRef, onKeyDown: onGridKeyDown, children: sections.map((section) => {
|
|
241
|
+
const start = offset;
|
|
242
|
+
offset += section.emojis.length;
|
|
243
|
+
if (!section.emojis.length) return null;
|
|
244
|
+
const headingId = `${id}-${section.key}`;
|
|
245
|
+
return /* @__PURE__ */ jsxs("section", { className: "uix-emoji-picker__section", "data-section": section.key, "aria-labelledby": headingId, children: [
|
|
246
|
+
/* @__PURE__ */ jsx("h3", { id: headingId, className: "uix-emoji-picker__heading", children: section.title }),
|
|
247
|
+
/* @__PURE__ */ jsx("div", { className: "uix-emoji-picker__cells", role: "group", "aria-labelledby": headingId, children: section.emojis.map((emoji, i) => {
|
|
248
|
+
const index = start + i;
|
|
249
|
+
return /* @__PURE__ */ jsx(
|
|
250
|
+
"button",
|
|
251
|
+
{
|
|
252
|
+
type: "button",
|
|
253
|
+
className: "uix-emoji-picker__btn",
|
|
254
|
+
"data-emoji-index": index,
|
|
255
|
+
tabIndex: index === activeIndex ? 0 : -1,
|
|
256
|
+
"aria-label": names?.get(emoji) ?? emoji,
|
|
257
|
+
title: names?.get(emoji),
|
|
258
|
+
onClick: () => choose(emoji),
|
|
259
|
+
onFocus: () => setActive(index),
|
|
260
|
+
children: renderEmoji(emoji)
|
|
261
|
+
},
|
|
262
|
+
`${section.key}-${emoji}`
|
|
263
|
+
);
|
|
264
|
+
}) })
|
|
265
|
+
] }, section.key);
|
|
266
|
+
}) }),
|
|
267
|
+
/* @__PURE__ */ jsx("p", { className: "uix-emoji-picker__status", role: "status", children: status })
|
|
268
|
+
] }) : null
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
] });
|
|
272
|
+
}
|
|
273
|
+
export {
|
|
274
|
+
DEFAULT_EMOJI_PICKER_LABELS,
|
|
275
|
+
EMOJI_GRID_COLUMNS,
|
|
276
|
+
EmojiPicker
|
|
277
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { defaultIsSafeUrl } from '../url-policy.js';
|
|
3
|
+
|
|
4
|
+
/** Default link policy: http(s), mailto and same-app paths (`/`, `#`, `./`, `../`); never `//host`. */
|
|
5
|
+
declare const defaultMarkdownIsSafeUrl: typeof defaultIsSafeUrl;
|
|
6
|
+
interface MarkdownProps {
|
|
7
|
+
/** Raw markdown source. Empty or whitespace-only input renders nothing. */
|
|
8
|
+
children: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
/** Link policy. Unsafe links render as their text, never as an anchor. */
|
|
11
|
+
isSafeUrl?: (url: string) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Image policy. Return the URL to load, or `null` to show the image as a link
|
|
14
|
+
* (subject to `isSafeUrl`). Without it, no `<img>` is ever rendered.
|
|
15
|
+
*/
|
|
16
|
+
resolveImageSrc?: (src: string) => string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Added to markdown heading levels so content headings sit below the page's own
|
|
19
|
+
* (`#` → `<h3>` by default, as in TENSOR's original renderer). Capped at `<h6>`.
|
|
20
|
+
*/
|
|
21
|
+
headingOffset?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Safe markdown viewer over `.uix-prose`. It never produces an HTML string and never
|
|
25
|
+
* uses `dangerouslySetInnerHTML`: author text only becomes React text nodes, raw HTML
|
|
26
|
+
* shows as literal text, and link/image URLs pass the policy props first.
|
|
27
|
+
*
|
|
28
|
+
* Supports headings, paragraphs (single newline = line break), bold, italic,
|
|
29
|
+
* strikethrough, inline and fenced code, links and bare http(s) URLs, blockquotes,
|
|
30
|
+
* horizontal rules, nested bulleted/numbered lists, read-only task lists, GFM tables
|
|
31
|
+
* (in a keyboard-scrollable wrapper) and images gated by `resolveImageSrc`.
|
|
32
|
+
* Server-component safe.
|
|
33
|
+
*/
|
|
34
|
+
declare function Markdown({ children, className, isSafeUrl, resolveImageSrc, headingOffset }: MarkdownProps): react.JSX.Element | null;
|
|
35
|
+
|
|
36
|
+
export { Markdown, type MarkdownProps, defaultMarkdownIsSafeUrl };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../cx.js";
|
|
3
|
+
import { parseMarkdown, plainText } from "../markdown-model.js";
|
|
4
|
+
import { defaultIsSafeUrl } from "../url-policy.js";
|
|
5
|
+
const defaultMarkdownIsSafeUrl = defaultIsSafeUrl;
|
|
6
|
+
const isExternal = (href) => /^https?:/i.test(href.trim());
|
|
7
|
+
function renderInlines(nodes, policy) {
|
|
8
|
+
return nodes.map((node, i) => {
|
|
9
|
+
switch (node.type) {
|
|
10
|
+
case "text":
|
|
11
|
+
return node.value;
|
|
12
|
+
case "br":
|
|
13
|
+
return /* @__PURE__ */ jsx("br", {}, i);
|
|
14
|
+
case "code":
|
|
15
|
+
return /* @__PURE__ */ jsx("code", { children: node.value }, i);
|
|
16
|
+
case "strong":
|
|
17
|
+
return /* @__PURE__ */ jsx("strong", { children: renderInlines(node.children, policy) }, i);
|
|
18
|
+
case "em":
|
|
19
|
+
return /* @__PURE__ */ jsx("em", { children: renderInlines(node.children, policy) }, i);
|
|
20
|
+
case "del":
|
|
21
|
+
return /* @__PURE__ */ jsx("del", { children: renderInlines(node.children, policy) }, i);
|
|
22
|
+
case "link": {
|
|
23
|
+
const children = renderInlines(node.children, policy);
|
|
24
|
+
if (!node.href.trim() || !policy.isSafeUrl(node.href)) return /* @__PURE__ */ jsx("span", { children }, i);
|
|
25
|
+
return /* @__PURE__ */ jsx(
|
|
26
|
+
"a",
|
|
27
|
+
{
|
|
28
|
+
href: node.href.trim(),
|
|
29
|
+
title: node.title,
|
|
30
|
+
...isExternal(node.href) ? { target: "_blank", rel: "noopener noreferrer" } : {},
|
|
31
|
+
children
|
|
32
|
+
},
|
|
33
|
+
i
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
case "image": {
|
|
37
|
+
const resolved = policy.resolveImageSrc?.(node.src) ?? null;
|
|
38
|
+
if (resolved) {
|
|
39
|
+
return /* @__PURE__ */ jsx("img", { src: resolved, alt: node.alt, title: node.title, loading: "lazy", decoding: "async" }, i);
|
|
40
|
+
}
|
|
41
|
+
const label = node.alt || node.src;
|
|
42
|
+
if (!node.src.trim() || !policy.isSafeUrl(node.src)) return label;
|
|
43
|
+
return /* @__PURE__ */ jsx("a", { href: node.src.trim(), ...isExternal(node.src) ? { target: "_blank", rel: "noopener noreferrer" } : {}, children: label }, i);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function renderList(list, policy, key) {
|
|
49
|
+
const items = list.items.map((item, i) => {
|
|
50
|
+
const content = renderInlines(item.children, policy);
|
|
51
|
+
const nested = item.lists.map((sub, n) => renderList(sub, policy, n));
|
|
52
|
+
if (item.checked === null) {
|
|
53
|
+
return /* @__PURE__ */ jsxs("li", { children: [
|
|
54
|
+
content,
|
|
55
|
+
nested
|
|
56
|
+
] }, i);
|
|
57
|
+
}
|
|
58
|
+
return /* @__PURE__ */ jsxs("li", { className: "uix-markdown__task", "data-checked": item.checked || void 0, children: [
|
|
59
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", defaultChecked: item.checked, disabled: true, "aria-label": plainText(item.children) }),
|
|
60
|
+
/* @__PURE__ */ jsx("span", { children: content }),
|
|
61
|
+
nested
|
|
62
|
+
] }, i);
|
|
63
|
+
});
|
|
64
|
+
const hasTasks = list.items.some((item) => item.checked !== null);
|
|
65
|
+
const className = hasTasks ? "uix-markdown__tasks" : void 0;
|
|
66
|
+
return list.ordered ? /* @__PURE__ */ jsx("ol", { className, start: list.start !== 1 ? list.start : void 0, children: items }, key) : /* @__PURE__ */ jsx("ul", { className, children: items }, key);
|
|
67
|
+
}
|
|
68
|
+
function renderBlock(block, policy, headingOffset, key) {
|
|
69
|
+
switch (block.type) {
|
|
70
|
+
case "paragraph":
|
|
71
|
+
return /* @__PURE__ */ jsx("p", { children: renderInlines(block.children, policy) }, key);
|
|
72
|
+
case "heading": {
|
|
73
|
+
const Tag = `h${Math.min(Math.max(block.level + headingOffset, 1), 6)}`;
|
|
74
|
+
return /* @__PURE__ */ jsx(Tag, { children: renderInlines(block.children, policy) }, key);
|
|
75
|
+
}
|
|
76
|
+
case "code":
|
|
77
|
+
return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { "data-language": block.lang || void 0, children: block.value }) }, key);
|
|
78
|
+
case "quote":
|
|
79
|
+
return /* @__PURE__ */ jsx("blockquote", { children: renderInlines(block.children, policy) }, key);
|
|
80
|
+
case "rule":
|
|
81
|
+
return /* @__PURE__ */ jsx("hr", {}, key);
|
|
82
|
+
case "list":
|
|
83
|
+
return renderList(block, policy, key);
|
|
84
|
+
case "table":
|
|
85
|
+
return (
|
|
86
|
+
// A scrollable region must be reachable from the keyboard.
|
|
87
|
+
/* @__PURE__ */ jsx("div", { className: "uix-markdown__table", tabIndex: 0, children: /* @__PURE__ */ jsxs("table", { children: [
|
|
88
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: block.head.map((cell, c) => /* @__PURE__ */ jsx("th", { scope: "col", style: block.align[c] ? { textAlign: block.align[c] } : void 0, children: renderInlines(cell, policy) }, c)) }) }),
|
|
89
|
+
/* @__PURE__ */ jsx("tbody", { children: block.rows.map((row, r) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, c) => /* @__PURE__ */ jsx("td", { style: block.align[c] ? { textAlign: block.align[c] } : void 0, children: renderInlines(cell, policy) }, c)) }, r)) })
|
|
90
|
+
] }) }, key)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function Markdown({ children, className, isSafeUrl = defaultMarkdownIsSafeUrl, resolveImageSrc, headingOffset = 2 }) {
|
|
95
|
+
const source = typeof children === "string" ? children : "";
|
|
96
|
+
if (source.trim() === "") return null;
|
|
97
|
+
const policy = { isSafeUrl, resolveImageSrc };
|
|
98
|
+
const blocks = parseMarkdown(source);
|
|
99
|
+
return /* @__PURE__ */ jsx("div", { className: cx("uix-prose", "uix-markdown", className), "data-component": "markdown", children: blocks.map((block, i) => renderBlock(block, policy, headingOffset, i)) });
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
Markdown,
|
|
103
|
+
defaultMarkdownIsSafeUrl
|
|
104
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { EmojiLocale, EmojiDataLoader } from '../emoji-model.js';
|
|
3
|
+
import { EmojiPickerLabels } from './EmojiPicker.js';
|
|
4
|
+
import '../overlay-position.js';
|
|
5
|
+
|
|
6
|
+
interface ReactionSummary {
|
|
7
|
+
/** Unicode emoji. */
|
|
8
|
+
emoji: string;
|
|
9
|
+
count: number;
|
|
10
|
+
/** Whether the current user is among the reactors (chip shows pressed). */
|
|
11
|
+
reactedByMe: boolean;
|
|
12
|
+
/** Display names of the reactors, in the order to show them. */
|
|
13
|
+
names: ReadonlyArray<string>;
|
|
14
|
+
}
|
|
15
|
+
interface ReactionBarLabels {
|
|
16
|
+
/** Accessible name of the reaction group. */
|
|
17
|
+
group: string;
|
|
18
|
+
/** Add-reaction button. */
|
|
19
|
+
addReaction: string;
|
|
20
|
+
/** Chip name and tooltip. Placeholders: `{names}`, `{emoji}`, `{count}`. */
|
|
21
|
+
reactedWith: string;
|
|
22
|
+
/** Used when no names are known. Placeholders: `{count}`, `{emoji}`. */
|
|
23
|
+
reactedCount: string;
|
|
24
|
+
/** Appended to a truncated name list. Placeholder: `{count}`. */
|
|
25
|
+
others: string;
|
|
26
|
+
}
|
|
27
|
+
declare const DEFAULT_REACTION_BAR_LABELS: ReactionBarLabels;
|
|
28
|
+
declare const DEFAULT_QUICK_REACTIONS: ReadonlyArray<string>;
|
|
29
|
+
interface ReactionBarProps {
|
|
30
|
+
reactions: ReadonlyArray<ReactionSummary>;
|
|
31
|
+
/** Called with the emoji of a clicked chip, or of an emoji picked that the user has not reacted with yet. */
|
|
32
|
+
onToggle: (emoji: string) => void;
|
|
33
|
+
/** Chips and the add button stay visible but cannot be used. */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Emoji offered first in the add-reaction picker. */
|
|
36
|
+
quickPicks?: ReadonlyArray<string>;
|
|
37
|
+
labels?: Partial<ReactionBarLabels>;
|
|
38
|
+
/** Labels of the add-reaction picker. */
|
|
39
|
+
pickerLabels?: Partial<EmojiPickerLabels>;
|
|
40
|
+
/** Language of emoji names in the picker. Default `'en'`. */
|
|
41
|
+
locale?: EmojiLocale;
|
|
42
|
+
loadData?: EmojiDataLoader;
|
|
43
|
+
/** Same-origin folder of fallback emoji PNGs, as on EmojiPicker. Unset = native emoji only. */
|
|
44
|
+
emojiImageBaseUrl?: string;
|
|
45
|
+
/** Names listed before "and N more". Default 10. */
|
|
46
|
+
maxNames?: number;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Emoji reactions over `.uix-reactions`: one toggle chip per emoji (`aria-pressed`
|
|
51
|
+
* when the viewer reacted), reactor names in the tooltip and the chip's accessible
|
|
52
|
+
* name, and an add-reaction picker with quick picks. Informational only — the bar
|
|
53
|
+
* carries no approval semantics.
|
|
54
|
+
*/
|
|
55
|
+
declare function ReactionBar({ reactions, onToggle, disabled, quickPicks, labels: labelOverrides, pickerLabels, locale, loadData, maxNames, className, emojiImageBaseUrl, }: ReactionBarProps): react.JSX.Element;
|
|
56
|
+
|
|
57
|
+
export { DEFAULT_QUICK_REACTIONS, DEFAULT_REACTION_BAR_LABELS, ReactionBar, type ReactionBarLabels, type ReactionBarProps, type ReactionSummary };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cx } from "../cx.js";
|
|
4
|
+
import { formatLabel } from "../emoji-model.js";
|
|
5
|
+
import { EditorIcon } from "./EditorIcons.js";
|
|
6
|
+
import { useEmojiRenderer } from "./EmojiGlyph.js";
|
|
7
|
+
import { EmojiPicker } from "./EmojiPicker.js";
|
|
8
|
+
import { Tooltip } from "./Tooltip.js";
|
|
9
|
+
const DEFAULT_REACTION_BAR_LABELS = {
|
|
10
|
+
group: "Reactions",
|
|
11
|
+
addReaction: "Add reaction",
|
|
12
|
+
reactedWith: "{names} reacted with {emoji}",
|
|
13
|
+
reactedCount: "{count} reacted with {emoji}",
|
|
14
|
+
others: "and {count} more"
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_QUICK_REACTIONS = ["\u{1F44D}", "\u2705", "\u{1F440}", "\u{1F389}", "\u2764\uFE0F"];
|
|
17
|
+
function ReactionBar({
|
|
18
|
+
reactions,
|
|
19
|
+
onToggle,
|
|
20
|
+
disabled = false,
|
|
21
|
+
quickPicks = DEFAULT_QUICK_REACTIONS,
|
|
22
|
+
labels: labelOverrides,
|
|
23
|
+
pickerLabels,
|
|
24
|
+
locale,
|
|
25
|
+
loadData,
|
|
26
|
+
maxNames = 10,
|
|
27
|
+
className,
|
|
28
|
+
emojiImageBaseUrl
|
|
29
|
+
}) {
|
|
30
|
+
const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
|
|
31
|
+
const labels = { ...DEFAULT_REACTION_BAR_LABELS, ...labelOverrides };
|
|
32
|
+
const describe = (reaction) => {
|
|
33
|
+
if (reaction.names.length === 0) {
|
|
34
|
+
return formatLabel(labels.reactedCount, { count: reaction.count, emoji: reaction.emoji });
|
|
35
|
+
}
|
|
36
|
+
const shown = reaction.names.slice(0, maxNames).join(", ");
|
|
37
|
+
const hidden = Math.max(reaction.count, reaction.names.length) - Math.min(reaction.names.length, maxNames);
|
|
38
|
+
const names = hidden > 0 ? `${shown} ${formatLabel(labels.others, { count: hidden })}` : shown;
|
|
39
|
+
return formatLabel(labels.reactedWith, { names, emoji: reaction.emoji, count: reaction.count });
|
|
40
|
+
};
|
|
41
|
+
return /* @__PURE__ */ jsxs("div", { className: cx("uix-reactions", className), role: "group", "aria-label": labels.group, children: [
|
|
42
|
+
reactions.filter((r) => r.count > 0).map((reaction) => {
|
|
43
|
+
const text = describe(reaction);
|
|
44
|
+
return /* @__PURE__ */ jsx(Tooltip, { label: text, children: /* @__PURE__ */ jsxs(
|
|
45
|
+
"button",
|
|
46
|
+
{
|
|
47
|
+
type: "button",
|
|
48
|
+
className: "uix-reaction",
|
|
49
|
+
"aria-pressed": reaction.reactedByMe,
|
|
50
|
+
"aria-label": text,
|
|
51
|
+
"data-mine": reaction.reactedByMe || void 0,
|
|
52
|
+
disabled,
|
|
53
|
+
onClick: () => onToggle(reaction.emoji),
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: renderEmoji(reaction.emoji) }),
|
|
56
|
+
/* @__PURE__ */ jsx("span", { className: "uix-reaction__count", "aria-hidden": "true", children: reaction.count })
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
) }, reaction.emoji);
|
|
60
|
+
}),
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
EmojiPicker,
|
|
63
|
+
{
|
|
64
|
+
quickPicks,
|
|
65
|
+
labels: pickerLabels,
|
|
66
|
+
locale,
|
|
67
|
+
loadData,
|
|
68
|
+
emojiImageBaseUrl,
|
|
69
|
+
onSelect: (emoji) => {
|
|
70
|
+
if (!reactions.some((r) => r.emoji === emoji && r.reactedByMe)) onToggle(emoji);
|
|
71
|
+
},
|
|
72
|
+
trigger: /* @__PURE__ */ jsx("button", { type: "button", className: "uix-reaction-add", "aria-label": labels.addReaction, title: labels.addReaction, disabled, children: /* @__PURE__ */ jsx(EditorIcon, { name: "addReaction" }) })
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
] });
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
DEFAULT_QUICK_REACTIONS,
|
|
79
|
+
DEFAULT_REACTION_BAR_LABELS,
|
|
80
|
+
ReactionBar
|
|
81
|
+
};
|
|
@@ -47,6 +47,24 @@ const DEFAULT_RELATIONSHIP_GRAPH_LABELS = {
|
|
|
47
47
|
};
|
|
48
48
|
const EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
49
49
|
const EMPTY_LEGEND = [];
|
|
50
|
+
const RADIAL_SCALE_X = 6;
|
|
51
|
+
const RADIAL_SCALE_Y = 4;
|
|
52
|
+
const RADIAL_NODE_HALF_W = 55;
|
|
53
|
+
const RADIAL_NODE_HALF_H = 18;
|
|
54
|
+
const RADIAL_MARGIN = 12;
|
|
55
|
+
function radialViewBox(nodes) {
|
|
56
|
+
if (nodes.length === 0) return "0 0 600 400";
|
|
57
|
+
const xs = nodes.map((node) => node.x * RADIAL_SCALE_X);
|
|
58
|
+
const ys = nodes.map((node) => node.y * RADIAL_SCALE_Y);
|
|
59
|
+
const minX = Math.min(...xs) - RADIAL_NODE_HALF_W - RADIAL_MARGIN;
|
|
60
|
+
const maxX = Math.max(...xs) + RADIAL_NODE_HALF_W + RADIAL_MARGIN;
|
|
61
|
+
const minY = Math.min(...ys) - RADIAL_NODE_HALF_H - RADIAL_MARGIN;
|
|
62
|
+
const maxY = Math.max(...ys) + RADIAL_NODE_HALF_H + RADIAL_MARGIN;
|
|
63
|
+
const round = (value) => Number(value.toFixed(2));
|
|
64
|
+
const width = Math.max(round(maxX - minX), 600);
|
|
65
|
+
const height = Math.max(round(maxY - minY), 400);
|
|
66
|
+
return `${round(minX)} ${round(minY)} ${width} ${height}`;
|
|
67
|
+
}
|
|
50
68
|
function RelationshipGraph(props) {
|
|
51
69
|
const labels = useMemo(() => ({ ...DEFAULT_RELATIONSHIP_GRAPH_LABELS, ...props.labels }), [props.labels]);
|
|
52
70
|
const stateClass = cx("uix-relationship-graph uix-relationship-graph--state", props.className);
|
|
@@ -123,16 +141,16 @@ function RadialRelationshipGraph({
|
|
|
123
141
|
/* @__PURE__ */ jsx("span", { "aria-live": "polite", children: labels.zoomLevel(Math.round(zoom * 100)) })
|
|
124
142
|
] }),
|
|
125
143
|
bounded.omittedNodeCount > 0 && /* @__PURE__ */ jsx("p", { className: "uix-relationship-graph__bounded", role: "status", children: labels.bounded(bounded.nodes.length, bounded.omittedNodeCount) }),
|
|
126
|
-
/* @__PURE__ */ jsx("div", { className: "uix-relationship-graph__visual", children: /* @__PURE__ */ jsx("svg", { ref: svgRef, viewBox:
|
|
144
|
+
/* @__PURE__ */ jsx("div", { className: "uix-relationship-graph__visual", children: /* @__PURE__ */ jsx("svg", { ref: svgRef, viewBox: radialViewBox(positioned), role: "group", "aria-label": labels.graphSummary(bounded.nodes.length, bounded.edges.length), children: /* @__PURE__ */ jsxs("g", { transform: `translate(${pan.x} ${pan.y}) scale(${zoom})`, children: [
|
|
127
145
|
bounded.edges.map((edge) => {
|
|
128
146
|
const source = positionedById.get(edge.source);
|
|
129
147
|
const target = positionedById.get(edge.target);
|
|
130
|
-
return /* @__PURE__ */ jsx("line", { x1: source.x *
|
|
148
|
+
return /* @__PURE__ */ jsx("line", { x1: source.x * RADIAL_SCALE_X, y1: source.y * RADIAL_SCALE_Y, x2: target.x * RADIAL_SCALE_X, y2: target.y * RADIAL_SCALE_Y, className: "uix-relationship-graph__edge", "data-highlighted": highlightedEdgeIds.has(edge.id) || void 0, "data-conflicted": conflictedEdgeIds.has(edge.id) || void 0, children: /* @__PURE__ */ jsx("title", { children: edge.label ?? edge.type ?? labels.relationship }) }, edge.id);
|
|
131
149
|
}),
|
|
132
150
|
positioned.map((node) => /* @__PURE__ */ jsxs(
|
|
133
151
|
"g",
|
|
134
152
|
{
|
|
135
|
-
transform: `translate(${node.x *
|
|
153
|
+
transform: `translate(${node.x * RADIAL_SCALE_X} ${node.y * RADIAL_SCALE_Y})`,
|
|
136
154
|
role: "button",
|
|
137
155
|
"aria-label": `${node.label}${node.type ? `, ${node.type}` : ""}${conflictedNodeIds.has(node.id) ? `, ${labels.conflict}` : ""}`,
|
|
138
156
|
tabIndex: focusedId === node.id ? 0 : -1,
|
|
@@ -145,7 +163,8 @@ function RadialRelationshipGraph({
|
|
|
145
163
|
onKeyDown: (event) => onNodeKeyDown(event, node.id),
|
|
146
164
|
onClick: () => select(node.id),
|
|
147
165
|
children: [
|
|
148
|
-
/* @__PURE__ */ jsx("
|
|
166
|
+
/* @__PURE__ */ jsx("title", { children: node.label }),
|
|
167
|
+
/* @__PURE__ */ jsx("rect", { x: -RADIAL_NODE_HALF_W, y: -RADIAL_NODE_HALF_H, width: RADIAL_NODE_HALF_W * 2, height: RADIAL_NODE_HALF_H * 2, rx: "8" }),
|
|
149
168
|
/* @__PURE__ */ jsx("text", { textAnchor: "middle", dominantBaseline: "middle", children: node.label.length > 16 ? `${node.label.slice(0, 15)}\u2026` : node.label })
|
|
150
169
|
]
|
|
151
170
|
},
|