@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.
Files changed (53) hide show
  1. package/README.md +102 -1
  2. package/dist/components/Comments.d.ts +18 -2
  3. package/dist/components/Comments.js +37 -12
  4. package/dist/components/EditorIcons.d.ts +39 -0
  5. package/dist/components/EditorIcons.js +51 -0
  6. package/dist/components/EmojiGlyph.d.ts +10 -0
  7. package/dist/components/EmojiGlyph.js +18 -0
  8. package/dist/components/EmojiPicker.d.ts +60 -0
  9. package/dist/components/EmojiPicker.js +277 -0
  10. package/dist/components/Markdown.d.ts +36 -0
  11. package/dist/components/Markdown.js +104 -0
  12. package/dist/components/ReactionBar.d.ts +57 -0
  13. package/dist/components/ReactionBar.js +81 -0
  14. package/dist/components/RelationshipGraph.js +23 -4
  15. package/dist/components/RichTextEditor.d.ts +128 -0
  16. package/dist/components/RichTextEditor.js +954 -0
  17. package/dist/components/SaveStatus.d.ts +32 -0
  18. package/dist/components/SaveStatus.js +21 -0
  19. package/dist/components/Tabs.d.ts +10 -3
  20. package/dist/components/Tabs.js +5 -3
  21. package/dist/emoji-image-WQLh8ZQP.d.cts +128 -0
  22. package/dist/emoji-image.d.ts +25 -0
  23. package/dist/emoji-image.js +80 -0
  24. package/dist/emoji-model.d.ts +66 -0
  25. package/dist/emoji-model.js +108 -0
  26. package/dist/emoji.cjs +871 -0
  27. package/dist/emoji.d.cts +57 -0
  28. package/dist/emoji.d.ts +6 -0
  29. package/dist/emoji.js +18 -0
  30. package/dist/index.cjs +587 -519
  31. package/dist/index.d.cts +59 -64
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +3 -0
  34. package/dist/markdown-model.d.ts +75 -0
  35. package/dist/markdown-model.js +438 -0
  36. package/dist/markdown.cjs +585 -0
  37. package/dist/markdown.d.cts +115 -0
  38. package/dist/markdown.d.ts +4 -0
  39. package/dist/markdown.js +9 -0
  40. package/dist/overlay-position-CEGSz6Z2.d.cts +59 -0
  41. package/dist/rich-text/emoji-items.d.ts +14 -0
  42. package/dist/rich-text/emoji-items.js +7 -0
  43. package/dist/rich-text/pipeline.d.ts +91 -0
  44. package/dist/rich-text/pipeline.js +379 -0
  45. package/dist/rich-text/round-trip.d.ts +24 -0
  46. package/dist/rich-text/round-trip.js +11 -0
  47. package/dist/rich-text.cjs +2578 -0
  48. package/dist/rich-text.d.cts +148 -0
  49. package/dist/rich-text.d.ts +8 -0
  50. package/dist/rich-text.js +10 -0
  51. package/dist/url-policy.d.ts +9 -0
  52. package/dist/url-policy.js +11 -0
  53. 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: "0 0 600 400", 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: [
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 * 6, y1: source.y * 4, x2: target.x * 6, y2: target.y * 4, 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);
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 * 6} ${node.y * 4})`,
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("rect", { x: "-55", y: "-18", width: "110", height: "36", rx: "8" }),
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
  },