@tensor_1/react 2.38.0 → 2.40.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 +65 -0
- package/dist/{chart-theme-Cl0IrkKQ.d.cts → ChartCore-BKK2HEfi.d.cts} +77 -50
- package/dist/chart-analytic-guide.d.ts +6 -0
- package/dist/chart-analytic-guide.js +10 -0
- package/dist/chart-analytic-model.d.ts +145 -0
- package/dist/chart-analytic-model.js +259 -0
- package/dist/chart-analytic.cjs +1379 -0
- package/dist/chart-analytic.d.cts +339 -0
- package/dist/chart-analytic.d.ts +19 -0
- package/dist/chart-analytic.js +95 -0
- package/dist/chart-panels-model.d.ts +92 -0
- package/dist/chart-panels-model.js +284 -0
- package/dist/chart-preset.cjs +41 -4
- package/dist/chart-preset.d.cts +2 -2
- package/dist/chart-preset.d.ts +1 -1
- package/dist/chart-stats-o4zDhpVm.d.cts +46 -0
- package/dist/chart-stats.d.ts +46 -0
- package/dist/chart-stats.js +89 -0
- package/dist/chart-theme.d.ts +3 -0
- package/dist/chart-theme.js +2 -0
- package/dist/chart.cjs +41 -4
- package/dist/chart.d.cts +2 -2
- package/dist/chart.d.ts +1 -1
- package/dist/components/Chart.d.ts +1 -1
- package/dist/components/ChartCore.d.ts +23 -3
- package/dist/components/ChartCore.js +40 -5
- package/dist/components/ChartPanels.d.ts +52 -0
- package/dist/components/ChartPanels.js +170 -0
- package/dist/components/Comments.d.ts +4 -1
- package/dist/components/Comments.js +3 -2
- package/dist/components/InsightCard.d.ts +86 -0
- package/dist/components/InsightCard.js +118 -0
- package/dist/components/Markdown.d.ts +9 -2
- package/dist/components/Markdown.js +16 -2
- package/dist/components/RichTextEditor.d.ts +17 -0
- package/dist/components/RichTextEditor.js +179 -11
- package/dist/components/ToggleRow.d.ts +8 -1
- package/dist/components/ToggleRow.js +17 -3
- package/dist/correlation-scatter-model.d.ts +68 -0
- package/dist/correlation-scatter-model.js +179 -0
- package/dist/icons/Icon.d.ts +17 -17
- package/dist/index.cjs +1663 -1427
- package/dist/index.d.cts +96 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -0
- package/dist/markdown-model-Cuwi6pIk.d.cts +110 -0
- package/dist/markdown-model.d.ts +38 -1
- package/dist/markdown-model.js +67 -0
- package/dist/markdown.cjs +90 -4
- package/dist/markdown.d.cts +11 -75
- package/dist/markdown.d.ts +1 -1
- package/dist/markdown.js +16 -2
- package/dist/rich-text/mention.d.ts +14 -0
- package/dist/rich-text/mention.js +57 -0
- package/dist/rich-text/pipeline.js +4 -1
- package/dist/rich-text.cjs +1795 -1483
- package/dist/rich-text.d.cts +27 -1
- package/dist/rich-text.d.ts +3 -0
- package/dist/rich-text.js +6 -0
- package/package.json +6 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../cx.js";
|
|
3
|
+
import { CircleCheckIcon, CircleQuestionMarkIcon, OctagonAlertIcon, TriangleAlertIcon } from "../icons/components.js";
|
|
4
|
+
const defaultNumber = new Intl.NumberFormat("en", { maximumFractionDigits: 1 });
|
|
5
|
+
const DEFAULT_INSIGHT_CARD_LABELS = {
|
|
6
|
+
coverage: (covered, total, percent, note) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7
|
+
covered,
|
|
8
|
+
" of ",
|
|
9
|
+
total,
|
|
10
|
+
" ",
|
|
11
|
+
note,
|
|
12
|
+
" (",
|
|
13
|
+
percent,
|
|
14
|
+
"%)"
|
|
15
|
+
] }),
|
|
16
|
+
bullet: (actual, target) => `${actual} against a target of ${target}`,
|
|
17
|
+
source: "Source",
|
|
18
|
+
updated: "Updated",
|
|
19
|
+
kpis: "Key figures",
|
|
20
|
+
formatNumber: (value) => defaultNumber.format(value)
|
|
21
|
+
};
|
|
22
|
+
const STATUS_ICON = {
|
|
23
|
+
ok: CircleCheckIcon,
|
|
24
|
+
warning: TriangleAlertIcon,
|
|
25
|
+
critical: OctagonAlertIcon,
|
|
26
|
+
unknown: CircleQuestionMarkIcon
|
|
27
|
+
};
|
|
28
|
+
function InsightCard({
|
|
29
|
+
eyebrow,
|
|
30
|
+
headline,
|
|
31
|
+
sowhat,
|
|
32
|
+
status,
|
|
33
|
+
kpis,
|
|
34
|
+
coverage,
|
|
35
|
+
actions,
|
|
36
|
+
source,
|
|
37
|
+
updated,
|
|
38
|
+
layout = "stack",
|
|
39
|
+
variant = "card",
|
|
40
|
+
aside,
|
|
41
|
+
headingLevel = 3,
|
|
42
|
+
labels: labelOverrides,
|
|
43
|
+
className,
|
|
44
|
+
children,
|
|
45
|
+
...props
|
|
46
|
+
}) {
|
|
47
|
+
const labels = { ...DEFAULT_INSIGHT_CARD_LABELS, ...labelOverrides };
|
|
48
|
+
const Heading = `h${headingLevel}`;
|
|
49
|
+
const StatusIcon = status ? STATUS_ICON[status.tone] : null;
|
|
50
|
+
const percent = coverage && coverage.total > 0 ? Math.round(coverage.covered / coverage.total * 100) : null;
|
|
51
|
+
const hasFooter = coverage != null || source != null || updated != null || actions != null;
|
|
52
|
+
return /* @__PURE__ */ jsxs("article", { className: cx("uix-insight", variant === "plain" && "uix-insight--plain", className), "data-layout": layout, ...props, children: [
|
|
53
|
+
/* @__PURE__ */ jsxs("header", { className: "uix-insight__header", children: [
|
|
54
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-insight__heading", children: [
|
|
55
|
+
eyebrow != null ? /* @__PURE__ */ jsx("div", { className: "uix-insight__eyebrow", children: eyebrow }) : null,
|
|
56
|
+
/* @__PURE__ */ jsx(Heading, { className: "uix-insight__headline", children: headline }),
|
|
57
|
+
sowhat != null ? /* @__PURE__ */ jsx("p", { className: "uix-insight__sowhat", children: sowhat }) : null
|
|
58
|
+
] }),
|
|
59
|
+
status && StatusIcon ? /* @__PURE__ */ jsxs("span", { className: "uix-insight__status", "data-tone": status.tone, children: [
|
|
60
|
+
/* @__PURE__ */ jsx(StatusIcon, { size: "sm", "aria-hidden": "true" }),
|
|
61
|
+
/* @__PURE__ */ jsx("span", { children: status.label })
|
|
62
|
+
] }) : null
|
|
63
|
+
] }),
|
|
64
|
+
kpis?.length ? /* @__PURE__ */ jsx("dl", { className: "uix-insight__kpis", "aria-label": labels.kpis, children: kpis.map((kpi, index) => /* @__PURE__ */ jsxs("div", { className: "uix-insight__kpi", children: [
|
|
65
|
+
/* @__PURE__ */ jsx("dt", { className: "uix-insight__kpi-label", children: kpi.label }),
|
|
66
|
+
/* @__PURE__ */ jsx("dd", { className: "uix-insight__kpi-value", children: kpi.value }),
|
|
67
|
+
kpi.comparison ? /* @__PURE__ */ jsx("dd", { className: "uix-insight__kpi-comparison", "data-tone": kpi.comparison.tone ?? "neutral", children: kpi.comparison.text }) : null,
|
|
68
|
+
kpi.target ? /* @__PURE__ */ jsx("dd", { className: "uix-insight__kpi-target", children: /* @__PURE__ */ jsx(Bullet, { ...kpi.target, label: labels.bullet(labels.formatNumber(kpi.target.actual), labels.formatNumber(kpi.target.target)) }) }) : null
|
|
69
|
+
] }, index)) }) : null,
|
|
70
|
+
children != null || aside != null ? /* @__PURE__ */ jsxs("div", { className: "uix-insight__body", children: [
|
|
71
|
+
children != null ? /* @__PURE__ */ jsx("div", { className: "uix-insight__main", children }) : null,
|
|
72
|
+
aside != null ? /* @__PURE__ */ jsx("div", { className: "uix-insight__aside", children: aside }) : null
|
|
73
|
+
] }) : null,
|
|
74
|
+
hasFooter ? /* @__PURE__ */ jsxs("footer", { className: "uix-insight__footer", children: [
|
|
75
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-insight__meta", children: [
|
|
76
|
+
coverage && percent != null ? /* @__PURE__ */ jsxs("p", { className: "uix-insight__coverage", children: [
|
|
77
|
+
labels.coverage(coverage.covered, coverage.total, percent, coverage.note),
|
|
78
|
+
coverage.partialPeriod != null ? /* @__PURE__ */ jsx("span", { className: "uix-insight__partial", children: coverage.partialPeriod }) : null
|
|
79
|
+
] }) : null,
|
|
80
|
+
source != null || updated != null ? /* @__PURE__ */ jsxs("p", { className: "uix-insight__source", children: [
|
|
81
|
+
source != null ? /* @__PURE__ */ jsxs("span", { children: [
|
|
82
|
+
labels.source,
|
|
83
|
+
": ",
|
|
84
|
+
source
|
|
85
|
+
] }) : null,
|
|
86
|
+
updated != null ? /* @__PURE__ */ jsxs("span", { children: [
|
|
87
|
+
labels.updated,
|
|
88
|
+
": ",
|
|
89
|
+
updated
|
|
90
|
+
] }) : null
|
|
91
|
+
] }) : null
|
|
92
|
+
] }),
|
|
93
|
+
actions != null ? /* @__PURE__ */ jsx("div", { className: "uix-insight__actions", children: actions }) : null
|
|
94
|
+
] }) : null
|
|
95
|
+
] });
|
|
96
|
+
}
|
|
97
|
+
function Bullet({ actual, target, ranges, max, label, className, style, ...props }) {
|
|
98
|
+
const end = Math.max(max ?? 0, ...(ranges ?? []).map((r) => r[1]), actual, target, 0) * (max ? 1 : 1.1) || 1;
|
|
99
|
+
const pct = (value) => `${Math.max(0, Math.min(100, value / end * 100))}%`;
|
|
100
|
+
return /* @__PURE__ */ jsxs("div", { className: cx("uix-bullet", className), role: "img", "aria-label": label, style, ...props, children: [
|
|
101
|
+
(ranges ?? []).map(([from, to], index) => /* @__PURE__ */ jsx(
|
|
102
|
+
"span",
|
|
103
|
+
{
|
|
104
|
+
className: "uix-bullet__range",
|
|
105
|
+
"data-band": Math.min(index, 2),
|
|
106
|
+
style: { left: pct(from), width: `calc(${pct(to)} - ${pct(from)})` }
|
|
107
|
+
},
|
|
108
|
+
index
|
|
109
|
+
)),
|
|
110
|
+
/* @__PURE__ */ jsx("span", { className: "uix-bullet__bar", style: { width: pct(actual) } }),
|
|
111
|
+
/* @__PURE__ */ jsx("span", { className: "uix-bullet__target", style: { left: pct(target) } })
|
|
112
|
+
] });
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
Bullet,
|
|
116
|
+
DEFAULT_INSIGHT_CARD_LABELS,
|
|
117
|
+
InsightCard
|
|
118
|
+
};
|
|
@@ -19,6 +19,12 @@ interface MarkdownProps {
|
|
|
19
19
|
* (`#` → `<h3>` by default, as in TENSOR's original renderer). Capped at `<h6>`.
|
|
20
20
|
*/
|
|
21
21
|
headingOffset?: number;
|
|
22
|
+
/**
|
|
23
|
+
* The name to show for a mention (`@[Name](mention:<id>)`), e.g. the person's current
|
|
24
|
+
* or masked name. Return `undefined` to show the name stored in the text. Without it
|
|
25
|
+
* every mention shows its stored name.
|
|
26
|
+
*/
|
|
27
|
+
resolveMention?: (id: string, storedLabel: string) => string | undefined;
|
|
22
28
|
}
|
|
23
29
|
/**
|
|
24
30
|
* Safe markdown viewer over `.uix-prose`. It never produces an HTML string and never
|
|
@@ -28,9 +34,10 @@ interface MarkdownProps {
|
|
|
28
34
|
* Supports headings, paragraphs (single newline = line break), bold, italic,
|
|
29
35
|
* strikethrough, inline and fenced code, links and bare http(s) URLs, blockquotes,
|
|
30
36
|
* horizontal rules, nested bulleted/numbered lists, read-only task lists, GFM tables
|
|
31
|
-
* (in a keyboard-scrollable wrapper)
|
|
37
|
+
* (in a keyboard-scrollable wrapper), images gated by `resolveImageSrc` and mentions
|
|
38
|
+
* (`@[Name](mention:<id>)`, drawn as `.uix-mention`, named by `resolveMention`).
|
|
32
39
|
* Server-component safe.
|
|
33
40
|
*/
|
|
34
|
-
declare function Markdown({ children, className, isSafeUrl, resolveImageSrc, headingOffset }: MarkdownProps): react.JSX.Element | null;
|
|
41
|
+
declare function Markdown({ children, className, isSafeUrl, resolveImageSrc, headingOffset, resolveMention, }: MarkdownProps): react.JSX.Element | null;
|
|
35
42
|
|
|
36
43
|
export { Markdown, type MarkdownProps, defaultMarkdownIsSafeUrl };
|
|
@@ -33,6 +33,13 @@ function renderInlines(nodes, policy) {
|
|
|
33
33
|
i
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
|
+
case "mention": {
|
|
37
|
+
const name = policy.resolveMention?.(node.id, node.label) ?? node.label;
|
|
38
|
+
return /* @__PURE__ */ jsxs("span", { className: "uix-mention", "data-mention-id": node.id, children: [
|
|
39
|
+
"@",
|
|
40
|
+
name
|
|
41
|
+
] }, i);
|
|
42
|
+
}
|
|
36
43
|
case "image": {
|
|
37
44
|
const resolved = policy.resolveImageSrc?.(node.src) ?? null;
|
|
38
45
|
if (resolved) {
|
|
@@ -91,10 +98,17 @@ function renderBlock(block, policy, headingOffset, key) {
|
|
|
91
98
|
);
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
|
-
function Markdown({
|
|
101
|
+
function Markdown({
|
|
102
|
+
children,
|
|
103
|
+
className,
|
|
104
|
+
isSafeUrl = defaultMarkdownIsSafeUrl,
|
|
105
|
+
resolveImageSrc,
|
|
106
|
+
headingOffset = 2,
|
|
107
|
+
resolveMention
|
|
108
|
+
}) {
|
|
95
109
|
const source = typeof children === "string" ? children : "";
|
|
96
110
|
if (source.trim() === "") return null;
|
|
97
|
-
const policy = { isSafeUrl, resolveImageSrc };
|
|
111
|
+
const policy = { isSafeUrl, resolveImageSrc, resolveMention };
|
|
98
112
|
const blocks = parseMarkdown(source);
|
|
99
113
|
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
114
|
}
|
|
@@ -2,7 +2,9 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { EmojiItem } from '@tiptap/extension-emoji';
|
|
4
4
|
import { EmojiLocale } from '../emoji-model.js';
|
|
5
|
+
import { MentionCandidate } from '../rich-text/mention.js';
|
|
5
6
|
import { EmojiPickerLabels } from './EmojiPicker.js';
|
|
7
|
+
import '@tiptap/core';
|
|
6
8
|
import '../overlay-position.js';
|
|
7
9
|
|
|
8
10
|
type RichTextFeatures = 'full' | 'comment' | 'template';
|
|
@@ -44,6 +46,14 @@ interface RichTextLabels {
|
|
|
44
46
|
emoji: string;
|
|
45
47
|
/** Accessible name of the `:shortcode` suggestion list. */
|
|
46
48
|
emojiSuggestions: string;
|
|
49
|
+
/** Accessible name of the `@` people list. */
|
|
50
|
+
mentionSuggestions: string;
|
|
51
|
+
/** Shown while `onMentionSearch` has not answered. */
|
|
52
|
+
mentionSearching: string;
|
|
53
|
+
/** Shown when `onMentionSearch` found nobody. */
|
|
54
|
+
mentionEmpty: string;
|
|
55
|
+
/** Shown when `onMentionSearch` failed. */
|
|
56
|
+
mentionFailed: string;
|
|
47
57
|
undo: string;
|
|
48
58
|
redo: string;
|
|
49
59
|
/** Toggle to markdown source. */
|
|
@@ -86,6 +96,13 @@ interface RichTextEditorProps {
|
|
|
86
96
|
onSubmitShortcut?: () => void;
|
|
87
97
|
/** Emoji button and `:shortcode` suggestions. Default `true`. */
|
|
88
98
|
emoji?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* People for the `@` list: called with the text typed after `@` (possibly empty) and
|
|
101
|
+
* answering the candidates to offer. The product owns the query and who may be
|
|
102
|
+
* mentioned; UIx never fetches. A picked person is inserted as one token, stored as
|
|
103
|
+
* `@[Name](mention:<id>)`. Without it, typing `@` does nothing special.
|
|
104
|
+
*/
|
|
105
|
+
onMentionSearch?: (query: string) => Promise<ReadonlyArray<MentionCandidate>> | ReadonlyArray<MentionCandidate>;
|
|
89
106
|
/**
|
|
90
107
|
* Same-origin folder of fallback emoji PNGs for devices without a colour emoji font
|
|
91
108
|
* (e.g. `/static/emoji`). Emoji the device cannot draw show as
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
readMarkdown,
|
|
25
25
|
writeMarkdown
|
|
26
26
|
} from "../rich-text/pipeline.js";
|
|
27
|
+
import { isMentionId } from "../markdown-model.js";
|
|
27
28
|
import { Composer, ComposerBar } from "./Composer.js";
|
|
28
29
|
import { EditorIcon } from "./EditorIcons.js";
|
|
29
30
|
import { EmojiPicker } from "./EmojiPicker.js";
|
|
@@ -62,6 +63,10 @@ const DEFAULT_RICH_TEXT_LABELS = {
|
|
|
62
63
|
imagesUnsupported: "Images can't be added here.",
|
|
63
64
|
emoji: "Emoji",
|
|
64
65
|
emojiSuggestions: "Emoji suggestions",
|
|
66
|
+
mentionSuggestions: "People",
|
|
67
|
+
mentionSearching: "Searching\u2026",
|
|
68
|
+
mentionEmpty: "No matching people",
|
|
69
|
+
mentionFailed: "People could not be loaded.",
|
|
65
70
|
undo: "Undo",
|
|
66
71
|
redo: "Redo",
|
|
67
72
|
sourceMode: "Show markdown",
|
|
@@ -92,6 +97,8 @@ function suggestEmoji(query, limit = 8) {
|
|
|
92
97
|
}
|
|
93
98
|
return [...starts, ...tagged].slice(0, limit);
|
|
94
99
|
}
|
|
100
|
+
const mentionSuggestionKey = new PluginKey("uixMentionSuggestion");
|
|
101
|
+
const usableCandidates = (items) => items.filter((c) => isMentionId(c.id) && c.label.trim() !== "").slice(0, 20);
|
|
95
102
|
const htmlAttr = (value) => value === void 0 || value === false || value === "" ? void 0 : String(value);
|
|
96
103
|
const popoverOpen = (el) => {
|
|
97
104
|
try {
|
|
@@ -190,8 +197,10 @@ function RichTextEditor(props) {
|
|
|
190
197
|
className,
|
|
191
198
|
onBlur,
|
|
192
199
|
emojiImageBaseUrl,
|
|
193
|
-
imagesUnavailableReason
|
|
200
|
+
imagesUnavailableReason,
|
|
201
|
+
onMentionSearch
|
|
194
202
|
} = props;
|
|
203
|
+
const mentionsOn = typeof onMentionSearch === "function";
|
|
195
204
|
const emojiBase = useMemo(() => normalizeEmojiImageBaseUrl(emojiImageBaseUrl), [emojiImageBaseUrl]);
|
|
196
205
|
const renderEmoji = useEmojiRenderer(emojiImageBaseUrl);
|
|
197
206
|
const labels = useMemo(() => ({ ...DEFAULT_RICH_TEXT_LABELS, ...labelOverrides }), [labelOverrides]);
|
|
@@ -199,10 +208,11 @@ function RichTextEditor(props) {
|
|
|
199
208
|
const counterId = `${uid}-counter`;
|
|
200
209
|
const statusId = `${uid}-status`;
|
|
201
210
|
const suggestId = `${uid}-emoji`;
|
|
211
|
+
const mentionId = `${uid}-people`;
|
|
202
212
|
const linkPopoverId = `${uid}-link`;
|
|
203
213
|
const controls = PRESETS[features];
|
|
204
214
|
const has = (control) => controls.has(control) && (control !== "emoji" || emoji) && (control !== "image" || !!onUploadImage);
|
|
205
|
-
const latest = useRef({ onChange, onSubmitShortcut, onBlur, isSafeUrl, resolveImageSrc, onUploadImage, placeholder, imagesOn: false, imagesOffText: "" });
|
|
215
|
+
const latest = useRef({ onChange, onSubmitShortcut, onBlur, isSafeUrl, resolveImageSrc, onUploadImage, onMentionSearch, placeholder, imagesOn: false, imagesOffText: "" });
|
|
206
216
|
latest.current = {
|
|
207
217
|
onChange,
|
|
208
218
|
onSubmitShortcut,
|
|
@@ -210,6 +220,7 @@ function RichTextEditor(props) {
|
|
|
210
220
|
isSafeUrl,
|
|
211
221
|
resolveImageSrc,
|
|
212
222
|
onUploadImage,
|
|
223
|
+
onMentionSearch,
|
|
213
224
|
placeholder,
|
|
214
225
|
imagesOn: controls.has("image") && !!onUploadImage,
|
|
215
226
|
imagesOffText: imagesUnavailableReason?.trim() || labels.imagesUnsupported
|
|
@@ -264,6 +275,59 @@ function RichTextEditor(props) {
|
|
|
264
275
|
return false;
|
|
265
276
|
}
|
|
266
277
|
};
|
|
278
|
+
const [mention, setMention] = useState(null);
|
|
279
|
+
const [mentionIndex, setMentionIndex] = useState(0);
|
|
280
|
+
const mentionState = useRef({ mention, mentionIndex });
|
|
281
|
+
mentionState.current = { mention, mentionIndex };
|
|
282
|
+
const mentionSeq = useRef(0);
|
|
283
|
+
const mentionBridge = useRef({ update: () => {
|
|
284
|
+
}, keyDown: () => false });
|
|
285
|
+
mentionBridge.current = {
|
|
286
|
+
update: (p) => {
|
|
287
|
+
const seq = ++mentionSeq.current;
|
|
288
|
+
const search = latest.current.onMentionSearch;
|
|
289
|
+
if (!p || !search) {
|
|
290
|
+
setMention(null);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const rect = p.clientRect?.() ?? null;
|
|
294
|
+
const base = { query: p.query, command: p.command, rect };
|
|
295
|
+
setMention((prev) => ({ ...base, status: "loading", items: prev?.query && p.query.startsWith(prev.query) ? prev.items : [] }));
|
|
296
|
+
setMentionIndex(0);
|
|
297
|
+
Promise.resolve().then(() => search(p.query)).then(
|
|
298
|
+
(found) => {
|
|
299
|
+
if (seq !== mentionSeq.current) return;
|
|
300
|
+
setMention({ ...base, status: "ready", items: usableCandidates(found ?? []) });
|
|
301
|
+
},
|
|
302
|
+
() => {
|
|
303
|
+
if (seq !== mentionSeq.current) return;
|
|
304
|
+
setMention({ ...base, status: "error", items: [] });
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
keyDown: ({ event }) => {
|
|
309
|
+
const { mention: open, mentionIndex: index } = mentionState.current;
|
|
310
|
+
if (!open) return false;
|
|
311
|
+
if (event.key === "Escape") {
|
|
312
|
+
mentionSeq.current += 1;
|
|
313
|
+
setMention(null);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
const picks = open.status === "ready" ? open.items : [];
|
|
317
|
+
if (picks.length === 0) return false;
|
|
318
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
319
|
+
const delta = event.key === "ArrowDown" ? 1 : -1;
|
|
320
|
+
setMentionIndex((index + delta + picks.length) % picks.length);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
324
|
+
const item = picks[Math.min(index, picks.length - 1)];
|
|
325
|
+
if (item) open.command(item);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
267
331
|
const headingKey = [...headingLevels].sort().join(",");
|
|
268
332
|
const extensions = useMemo(() => {
|
|
269
333
|
const image = Image.extend({
|
|
@@ -386,6 +450,30 @@ function RichTextEditor(props) {
|
|
|
386
450
|
})];
|
|
387
451
|
}
|
|
388
452
|
});
|
|
453
|
+
const mentionSuggest = Extension.create({
|
|
454
|
+
name: "uixMentionSuggestion",
|
|
455
|
+
addProseMirrorPlugins() {
|
|
456
|
+
return [Suggestion({
|
|
457
|
+
editor: this.editor,
|
|
458
|
+
char: "@",
|
|
459
|
+
pluginKey: mentionSuggestionKey,
|
|
460
|
+
items: () => [],
|
|
461
|
+
command: ({ editor: ed, range, props: item }) => {
|
|
462
|
+
ed.chain().focus().insertContentAt(range, [
|
|
463
|
+
{ type: "mention", attrs: { id: item.id, label: item.label.replace(/\s+/gu, " ").trim() } },
|
|
464
|
+
{ type: "text", text: " " }
|
|
465
|
+
]).run();
|
|
466
|
+
},
|
|
467
|
+
allow: ({ state: state2, range }) => !state2.doc.resolve(range.from).parent.type.spec.code,
|
|
468
|
+
render: () => ({
|
|
469
|
+
onStart: (p) => mentionBridge.current.update(p),
|
|
470
|
+
onUpdate: (p) => mentionBridge.current.update(p),
|
|
471
|
+
onExit: () => mentionBridge.current.update(null),
|
|
472
|
+
onKeyDown: (p) => mentionBridge.current.keyDown(p)
|
|
473
|
+
})
|
|
474
|
+
})];
|
|
475
|
+
}
|
|
476
|
+
});
|
|
389
477
|
return [
|
|
390
478
|
...createSchemaExtensions({
|
|
391
479
|
headingLevels: headingKey.split(",").map(Number),
|
|
@@ -396,9 +484,10 @@ function RichTextEditor(props) {
|
|
|
396
484
|
Placeholder.configure({ placeholder: () => latest.current.placeholder ?? "" }),
|
|
397
485
|
shortcuts,
|
|
398
486
|
...emoji ? [unicodeEmoji] : [],
|
|
399
|
-
...emojiBase ? [emojiImages] : []
|
|
487
|
+
...emojiBase ? [emojiImages] : [],
|
|
488
|
+
...mentionsOn ? [mentionSuggest] : []
|
|
400
489
|
];
|
|
401
|
-
}, [headingKey, emoji, emojiBase]);
|
|
490
|
+
}, [headingKey, emoji, emojiBase, mentionsOn]);
|
|
402
491
|
const managerOf = (ed) => {
|
|
403
492
|
const manager = ed.markdown ?? ed.storage.markdown.manager;
|
|
404
493
|
return patchManager(manager);
|
|
@@ -480,6 +569,8 @@ function RichTextEditor(props) {
|
|
|
480
569
|
const invalid = props["aria-invalid"] || over;
|
|
481
570
|
const describedBy = [props["aria-describedby"], maxLength !== void 0 ? counterId : void 0, status ? statusId : void 0].filter(Boolean).join(" ") || void 0;
|
|
482
571
|
const suggestOpen = !!suggest && mode === "rich";
|
|
572
|
+
const mentionOpen = !suggestOpen && !!mention && mode === "rich";
|
|
573
|
+
const mentionActive = mentionOpen && mention?.status === "ready" && mention.items.length > 0 ? `${mentionId}-${Math.min(mentionIndex, mention.items.length - 1)}` : void 0;
|
|
483
574
|
const rows = minRows ?? (variant === "composer" ? 3 : 4);
|
|
484
575
|
useEffect(() => {
|
|
485
576
|
if (!editor || editor.isDestroyed) return;
|
|
@@ -497,9 +588,9 @@ function RichTextEditor(props) {
|
|
|
497
588
|
"aria-required": htmlAttr(props["aria-required"]),
|
|
498
589
|
"aria-disabled": htmlAttr(disabled),
|
|
499
590
|
"aria-readonly": htmlAttr(readOnly),
|
|
500
|
-
"aria-autocomplete": emoji ? "list" : void 0,
|
|
501
|
-
"aria-controls": suggestOpen ? suggestId : void 0,
|
|
502
|
-
"aria-activedescendant": suggestOpen ? `${suggestId}-${suggestIndex}` :
|
|
591
|
+
"aria-autocomplete": emoji || mentionsOn ? "list" : void 0,
|
|
592
|
+
"aria-controls": suggestOpen ? suggestId : mentionOpen ? mentionId : void 0,
|
|
593
|
+
"aria-activedescendant": suggestOpen ? `${suggestId}-${suggestIndex}` : mentionActive,
|
|
503
594
|
class: "uix-rich-text__content uix-prose",
|
|
504
595
|
style: `min-height: calc(${rows} * 1lh + 2 * var(--uix-space-3))`
|
|
505
596
|
}).filter(([, v]) => v !== void 0))
|
|
@@ -520,7 +611,11 @@ function RichTextEditor(props) {
|
|
|
520
611
|
suggestOpen,
|
|
521
612
|
suggestId,
|
|
522
613
|
suggestIndex,
|
|
523
|
-
rows
|
|
614
|
+
rows,
|
|
615
|
+
mentionsOn,
|
|
616
|
+
mentionOpen,
|
|
617
|
+
mentionId,
|
|
618
|
+
mentionActive
|
|
524
619
|
]);
|
|
525
620
|
const state = useEditorState({
|
|
526
621
|
editor,
|
|
@@ -874,6 +969,16 @@ function RichTextEditor(props) {
|
|
|
874
969
|
onPick: (item) => suggest.command(item),
|
|
875
970
|
renderEmoji
|
|
876
971
|
}
|
|
972
|
+
) : null,
|
|
973
|
+
mentionOpen && mention ? /* @__PURE__ */ jsx(
|
|
974
|
+
MentionSuggestions,
|
|
975
|
+
{
|
|
976
|
+
id: mentionId,
|
|
977
|
+
labels,
|
|
978
|
+
state: mention,
|
|
979
|
+
active: Math.min(mentionIndex, Math.max(mention.items.length - 1, 0)),
|
|
980
|
+
onPick: (item) => mention.command(item)
|
|
981
|
+
}
|
|
877
982
|
) : null
|
|
878
983
|
] });
|
|
879
984
|
const counter = maxLength !== void 0 ? /* @__PURE__ */ jsx("span", { id: counterId, className: "uix-rich-text__counter", "data-over": over || void 0, children: formatLabel(labels.characterCount, { count: markdown.length, max: maxLength }) }) : null;
|
|
@@ -908,8 +1013,7 @@ function RichTextEditor(props) {
|
|
|
908
1013
|
hidden
|
|
909
1014
|
] });
|
|
910
1015
|
}
|
|
911
|
-
function
|
|
912
|
-
const listRef = useRef(null);
|
|
1016
|
+
function useCaretPosition(listRef, rect, size) {
|
|
913
1017
|
const [pos, setPos] = useState(null);
|
|
914
1018
|
useEffect(() => {
|
|
915
1019
|
const el = listRef.current;
|
|
@@ -922,7 +1026,71 @@ function EmojiSuggestions({ id, label, items, query, active, rect, onPick, rende
|
|
|
922
1026
|
{ placement: "bottom-start", offset: 4, padding: 8 }
|
|
923
1027
|
);
|
|
924
1028
|
setPos({ x: result.x, y: result.y });
|
|
925
|
-
}, [rect,
|
|
1029
|
+
}, [listRef, rect, size]);
|
|
1030
|
+
return pos;
|
|
1031
|
+
}
|
|
1032
|
+
function MentionSuggestions({ id, labels, state, active, onPick }) {
|
|
1033
|
+
const listRef = useRef(null);
|
|
1034
|
+
const items = state.status === "ready" ? state.items : [];
|
|
1035
|
+
const pos = useCaretPosition(listRef, state.rect, `${state.status}:${items.length}`);
|
|
1036
|
+
useEffect(() => {
|
|
1037
|
+
document.getElementById(`${id}-${active}`)?.scrollIntoView?.({ block: "nearest" });
|
|
1038
|
+
}, [active, id]);
|
|
1039
|
+
const style = { left: pos?.x ?? -9999, top: pos?.y ?? -9999 };
|
|
1040
|
+
if (items.length === 0) {
|
|
1041
|
+
const note = state.status === "loading" ? labels.mentionSearching : state.status === "error" ? labels.mentionFailed : labels.mentionEmpty;
|
|
1042
|
+
return /* @__PURE__ */ jsx(
|
|
1043
|
+
"div",
|
|
1044
|
+
{
|
|
1045
|
+
ref: listRef,
|
|
1046
|
+
id,
|
|
1047
|
+
role: "status",
|
|
1048
|
+
"aria-label": labels.mentionSuggestions,
|
|
1049
|
+
className: "uix-rich-text__suggest",
|
|
1050
|
+
"data-mention-list": "",
|
|
1051
|
+
"data-state": state.status === "ready" ? "empty" : state.status,
|
|
1052
|
+
style,
|
|
1053
|
+
children: /* @__PURE__ */ jsx("div", { className: "uix-rich-text__suggest-note", children: note })
|
|
1054
|
+
}
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
return /* @__PURE__ */ jsx(
|
|
1058
|
+
"div",
|
|
1059
|
+
{
|
|
1060
|
+
ref: listRef,
|
|
1061
|
+
id,
|
|
1062
|
+
role: "listbox",
|
|
1063
|
+
"aria-label": labels.mentionSuggestions,
|
|
1064
|
+
"aria-busy": state.status === "loading" || void 0,
|
|
1065
|
+
className: "uix-rich-text__suggest",
|
|
1066
|
+
"data-mention-list": "",
|
|
1067
|
+
"data-state": "ready",
|
|
1068
|
+
style,
|
|
1069
|
+
children: items.map((item, i) => /* @__PURE__ */ jsxs(
|
|
1070
|
+
"div",
|
|
1071
|
+
{
|
|
1072
|
+
id: `${id}-${i}`,
|
|
1073
|
+
role: "option",
|
|
1074
|
+
"aria-selected": i === active,
|
|
1075
|
+
className: "uix-rich-text__suggest-item uix-rich-text__suggest-item--person",
|
|
1076
|
+
"data-mention-id": item.id,
|
|
1077
|
+
onMouseDown: (event) => {
|
|
1078
|
+
event.preventDefault();
|
|
1079
|
+
onPick(item);
|
|
1080
|
+
},
|
|
1081
|
+
children: [
|
|
1082
|
+
/* @__PURE__ */ jsx("span", { className: "uix-rich-text__suggest-label", children: item.label }),
|
|
1083
|
+
item.description ? /* @__PURE__ */ jsx("span", { className: "uix-rich-text__suggest-detail", children: item.description }) : null
|
|
1084
|
+
]
|
|
1085
|
+
},
|
|
1086
|
+
item.id
|
|
1087
|
+
))
|
|
1088
|
+
}
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
function EmojiSuggestions({ id, label, items, query, active, rect, onPick, renderEmoji }) {
|
|
1092
|
+
const listRef = useRef(null);
|
|
1093
|
+
const pos = useCaretPosition(listRef, rect, items.length);
|
|
926
1094
|
useEffect(() => {
|
|
927
1095
|
document.getElementById(`${id}-${active}`)?.scrollIntoView?.({ block: "nearest" });
|
|
928
1096
|
}, [active, id]);
|
|
@@ -2,12 +2,19 @@ import * as react from 'react';
|
|
|
2
2
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
|
|
4
4
|
interface ToggleRowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
5
|
+
/** The row's visible label. It is the switch's `<label>`: clicking it flips the switch, and it names the switch. */
|
|
5
6
|
label: ReactNode;
|
|
7
|
+
/** Shown under the label and announced as the switch's description (`aria-describedby`). */
|
|
6
8
|
description?: ReactNode;
|
|
7
9
|
checked: boolean;
|
|
8
10
|
onCheckedChange: (checked: boolean) => void;
|
|
9
11
|
disabled?: boolean;
|
|
10
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The switch's name when the visible label is not text (an icon, an image).
|
|
14
|
+
* Leave it out for a text label: the label already names the switch, and a
|
|
15
|
+
* repeated `aria-label` would replace it.
|
|
16
|
+
*/
|
|
17
|
+
accessibleLabel?: string;
|
|
11
18
|
}
|
|
12
19
|
declare function ToggleRow({ label, description, checked, onCheckedChange, disabled, accessibleLabel, className, ...props }: ToggleRowProps): react.JSX.Element;
|
|
13
20
|
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
3
4
|
import { cx } from "../cx.js";
|
|
4
5
|
import { Switch } from "./Switch.js";
|
|
5
6
|
function ToggleRow({ label, description, checked, onCheckedChange, disabled, accessibleLabel, className, ...props }) {
|
|
7
|
+
const id = useId();
|
|
8
|
+
const switchId = `${id}-switch`;
|
|
9
|
+
const descriptionId = `${id}-description`;
|
|
6
10
|
return /* @__PURE__ */ jsxs("div", { className: cx("uix-toggle-row", className), ...props, children: [
|
|
7
11
|
/* @__PURE__ */ jsxs("div", { className: "uix-toggle-row__copy", children: [
|
|
8
|
-
/* @__PURE__ */ jsx("div", { className: "uix-toggle-row__label", children: label }),
|
|
9
|
-
description != null && /* @__PURE__ */ jsx("div", { className: "uix-toggle-row__description", children: description })
|
|
12
|
+
/* @__PURE__ */ jsx("div", { className: "uix-toggle-row__label", children: /* @__PURE__ */ jsx("label", { htmlFor: switchId, children: label }) }),
|
|
13
|
+
description != null && /* @__PURE__ */ jsx("div", { className: "uix-toggle-row__description", id: descriptionId, children: description })
|
|
10
14
|
] }),
|
|
11
|
-
/* @__PURE__ */ jsx(
|
|
15
|
+
/* @__PURE__ */ jsx(
|
|
16
|
+
Switch,
|
|
17
|
+
{
|
|
18
|
+
id: switchId,
|
|
19
|
+
checked,
|
|
20
|
+
disabled,
|
|
21
|
+
...accessibleLabel ? { "aria-label": accessibleLabel } : {},
|
|
22
|
+
...description != null ? { "aria-describedby": descriptionId } : {},
|
|
23
|
+
onChange: (event) => onCheckedChange(event.currentTarget.checked)
|
|
24
|
+
}
|
|
25
|
+
)
|
|
12
26
|
] });
|
|
13
27
|
}
|
|
14
28
|
export {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { EChartsOption } from 'echarts';
|
|
2
|
+
import { UixChartTokens } from './chart-theme.js';
|
|
3
|
+
import { LinearFit } from './chart-stats.js';
|
|
4
|
+
import { ChartThreshold } from './chart-analytic-model.js';
|
|
5
|
+
|
|
6
|
+
interface CorrelationPoint {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
label: string;
|
|
10
|
+
href?: string;
|
|
11
|
+
/** A point that matters (a breach): a status hue AND a distinct shape, never colour alone. */
|
|
12
|
+
emphasis?: 'danger' | 'warning';
|
|
13
|
+
}
|
|
14
|
+
interface CorrelationScatterLabels {
|
|
15
|
+
/** The stats caption. `r` is null when it is undefined (y constant). */
|
|
16
|
+
stats: (stats: {
|
|
17
|
+
r: number | null;
|
|
18
|
+
n: number;
|
|
19
|
+
}) => string;
|
|
20
|
+
notEnough: (n: number, min: number) => string;
|
|
21
|
+
percentile: (p: number) => string;
|
|
22
|
+
trend: string;
|
|
23
|
+
/** Header of the label column in the data table. */
|
|
24
|
+
point: string;
|
|
25
|
+
formatX: (x: number) => string;
|
|
26
|
+
formatY: (y: number) => string;
|
|
27
|
+
}
|
|
28
|
+
/** r to two decimals, truncated toward zero so an imperfect fit never reads "r = 1.00". */
|
|
29
|
+
declare function formatCorrelation(r: number): string;
|
|
30
|
+
declare const DEFAULT_CORRELATION_SCATTER_LABELS: CorrelationScatterLabels;
|
|
31
|
+
interface CorrelationScatterSpec {
|
|
32
|
+
points: CorrelationPoint[];
|
|
33
|
+
xLabel: string;
|
|
34
|
+
yLabel: string;
|
|
35
|
+
trend?: 'linear' | 'none';
|
|
36
|
+
/** Percentile lines of y, in percent (e.g. [50, 90]). */
|
|
37
|
+
percentiles?: number[];
|
|
38
|
+
thresholds?: {
|
|
39
|
+
x?: ChartThreshold[];
|
|
40
|
+
y?: ChartThreshold[];
|
|
41
|
+
};
|
|
42
|
+
breakpoint?: {
|
|
43
|
+
x: number;
|
|
44
|
+
label: string;
|
|
45
|
+
};
|
|
46
|
+
/** Below this many points there is no trend and no stats. Default 8. */
|
|
47
|
+
minPoints?: number;
|
|
48
|
+
}
|
|
49
|
+
interface CorrelationScatterModel {
|
|
50
|
+
/** False below `minPoints`: render the "not enough data" state instead of a trend. */
|
|
51
|
+
enough: boolean;
|
|
52
|
+
n: number;
|
|
53
|
+
minPoints: number;
|
|
54
|
+
fit: LinearFit | null;
|
|
55
|
+
option: EChartsOption;
|
|
56
|
+
statsText: string;
|
|
57
|
+
table: {
|
|
58
|
+
headers: string[];
|
|
59
|
+
rows: Record<string, string | number>[];
|
|
60
|
+
};
|
|
61
|
+
/** Index into `points` for each data item of the hit series. */
|
|
62
|
+
hitSeriesId: string;
|
|
63
|
+
}
|
|
64
|
+
/** Hit target for every point, in px: above the 24 px accessible minimum (WCAG 2.5.8) with the 2 px gap around it. */
|
|
65
|
+
declare const SCATTER_HIT_SIZE = 26;
|
|
66
|
+
declare function correlationScatterModel(spec: CorrelationScatterSpec, tokens: UixChartTokens, labelOverrides?: Partial<CorrelationScatterLabels>): CorrelationScatterModel;
|
|
67
|
+
|
|
68
|
+
export { type CorrelationPoint, type CorrelationScatterLabels, type CorrelationScatterModel, type CorrelationScatterSpec, DEFAULT_CORRELATION_SCATTER_LABELS, SCATTER_HIT_SIZE, correlationScatterModel, formatCorrelation };
|