@hsu-react/ui 0.0.15 → 0.0.17

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.
@@ -33,19 +33,3 @@
33
33
  }
34
34
  }
35
35
  }
36
-
37
- .Copyed {
38
- display: flex;
39
- align-items: center;
40
- column-gap: 5px;
41
-
42
- font-size: 14px;
43
- line-height: 18px;
44
- color: var(--text-color);
45
-
46
- cursor: default;
47
-
48
- .icon {
49
- font-size: 18px;
50
- }
51
- }
@@ -1,7 +1,8 @@
1
1
  import React, { ReactNode } from "react";
2
2
  export interface CopyProps {
3
3
  id: string;
4
- isMessage?: true;
4
+ /** true (default): report via a global toast; false: swap the button for an inline "已复制!" state */
5
+ isMessage?: boolean;
5
6
  md?: boolean;
6
7
  text?: string;
7
8
  copyIcon?: ReactNode;
@@ -34,7 +34,7 @@
34
34
 
35
35
  font-size: 14px;
36
36
  line-height: 18px;
37
- color: var(--text-color);
37
+ color: var(--cf-text);
38
38
 
39
39
  cursor: default;
40
40
 
@@ -1,14 +1,14 @@
1
- var _excluded = ["className", "children"],
2
- _excluded2 = ["children"];
3
1
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
4
- function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
5
- function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
2
+ var _excluded = ["className", "children", "node"],
3
+ _excluded2 = ["children", "node"];
6
4
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
7
5
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
8
6
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
9
7
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
10
8
  function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
11
- import React from "react";
9
+ function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
10
+ function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
11
+ import React, { createContext, useContext, useId } from "react";
12
12
  import ReactMarkdown from "react-markdown";
13
13
  import rehypeHighlight from "rehype-highlight";
14
14
  import rehypeKatex from "rehype-katex";
@@ -18,7 +18,6 @@ import classNames from "classnames";
18
18
  import styles from "./index.module.scss";
19
19
  import "highlight.js/styles/atom-one-dark.min.css";
20
20
  import "katex/dist/katex.min.css";
21
- import { generateRandomStr } from "hsu-utils";
22
21
  import Copy from "../../Copy";
23
22
  import MermaidBlock from "./MermaidBlock";
24
23
  import ArtifactBlock from "./ArtifactBlock";
@@ -41,77 +40,109 @@ var extractText = function extractText(node) {
41
40
  }
42
41
  return "";
43
42
  };
44
- var MarkdownViews = function MarkdownViews(props) {
43
+
44
+ /**
45
+ * Copy button config, handed to the code renderer through context rather than a closure so the
46
+ * renderer can stay a module-level component (see CodeBlock).
47
+ */
48
+ var CopyPropsContext = /*#__PURE__*/createContext(undefined);
49
+
50
+ /**
51
+ * Fenced code block: mermaid diagram, Artifacts preview, or highlighted code with a copy button.
52
+ *
53
+ * Declared at module scope on purpose. Defining it inline inside MarkdownViews would hand
54
+ * react-markdown a brand-new element type on every render, so React would unmount and remount
55
+ * every code block whenever the markdown re-renders — making the copy button flicker (and losing
56
+ * its "已复制!" state) on each token of streamed content.
57
+ */
58
+ var CodeBlock = function CodeBlock(props) {
59
+ var _exec;
60
+ // `node` is react-markdown's hast node — pulled out so it never reaches the DOM
45
61
  var className = props.className,
46
- copyProps = props.copyProps,
47
- extraComponents = props.components;
48
- return /*#__PURE__*/_jsx(ReactMarkdown, _objectSpread(_objectSpread({}, props), {}, {
49
- rehypePlugins: [rehypeHighlight, rehypeKatex],
50
- remarkPlugins: [remarkGfm, remarkMath],
51
- className: classNames(styles.MarkdownViews, className, "markdown-body"),
52
- components: _objectSpread({
53
- code: function code(_ref) {
54
- var className = _ref.className,
55
- children = _ref.children,
56
- props = _objectWithoutProperties(_ref, _excluded);
57
- var match = /language-(\w+)/.exec(className || "");
58
- if (match !== null && match !== void 0 && match.length) {
59
- var lang = match[1].toLowerCase();
60
- var codeText = extractText(children);
62
+ children = props.children,
63
+ _node = props.node,
64
+ codeProps = _objectWithoutProperties(props, _excluded);
65
+ var copyProps = useContext(CopyPropsContext);
66
+ // Stable for the lifetime of the block: regenerating the id on every render would leave the
67
+ // copy button's getElementById lookup pointing at a stale node. Strip ':' so the id stays
68
+ // usable with CSS selectors as well as getElementById.
69
+ var id = "md-code-".concat(useId().replace(/:/g, ""));
70
+ var lang = (_exec = /language-(\w+)/.exec(className || "")) === null || _exec === void 0 || (_exec = _exec[1]) === null || _exec === void 0 ? void 0 : _exec.toLowerCase();
71
+ if (!lang) {
72
+ return /*#__PURE__*/_jsx("code", _objectSpread(_objectSpread({}, codeProps), {}, {
73
+ children: children
74
+ }));
75
+ }
76
+ var codeText = extractText(children);
77
+
78
+ // Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
79
+ if (lang === "mermaid") {
80
+ return /*#__PURE__*/_jsx(MermaidBlock, {
81
+ code: codeText
82
+ });
83
+ }
84
+ var codeView = /*#__PURE__*/_jsxs("div", {
85
+ className: classNames(styles.code),
86
+ children: [/*#__PURE__*/_jsx("div", {
87
+ className: classNames(styles.nav),
88
+ children: /*#__PURE__*/_jsx(Copy, _objectSpread({
89
+ id: id,
90
+ md: false
91
+ }, copyProps))
92
+ }), /*#__PURE__*/_jsx("div", {
93
+ className: classNames(styles.content),
94
+ children: /*#__PURE__*/_jsx("code", _objectSpread(_objectSpread({
95
+ id: id
96
+ }, codeProps), {}, {
97
+ className: classNames(styles.code_content, className),
98
+ children: children
99
+ }))
100
+ })]
101
+ });
61
102
 
62
- // Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
63
- if (lang === "mermaid") {
64
- return /*#__PURE__*/_jsx(MermaidBlock, {
65
- code: codeText
66
- });
67
- }
68
- var id = generateRandomStr(10);
69
- var codeView = /*#__PURE__*/_jsxs("div", {
70
- className: classNames(styles.code),
71
- children: [/*#__PURE__*/_jsx("div", {
72
- className: classNames(styles.nav),
73
- children: /*#__PURE__*/_jsx(Copy, _objectSpread({
74
- id: id,
75
- md: false
76
- }, copyProps))
77
- }), /*#__PURE__*/_jsx("div", {
78
- className: classNames(styles.content),
79
- children: /*#__PURE__*/_jsx("code", _objectSpread(_objectSpread({
80
- id: id
81
- }, props), {}, {
82
- className: classNames(styles.code_content, className),
83
- children: children
84
- }))
85
- })]
86
- });
103
+ // HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
104
+ if (ARTIFACT_LANGS.has(lang)) {
105
+ return /*#__PURE__*/_jsx(ArtifactBlock, {
106
+ code: codeText,
107
+ lang: lang,
108
+ codeView: codeView
109
+ });
110
+ }
111
+ return codeView;
112
+ };
87
113
 
88
- // HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
89
- if (ARTIFACT_LANGS.has(lang)) {
90
- return /*#__PURE__*/_jsx(ArtifactBlock, {
91
- code: codeText,
92
- lang: lang,
93
- codeView: codeView
94
- });
95
- }
96
- return codeView;
97
- }
98
- return /*#__PURE__*/_jsx("code", _objectSpread(_objectSpread({}, props), {}, {
99
- children: children
100
- }));
101
- },
102
- td: function td(_ref2) {
103
- var children = _ref2.children,
104
- props = _objectWithoutProperties(_ref2, _excluded2);
105
- return /*#__PURE__*/_jsx("td", _objectSpread(_objectSpread({}, props), {}, {
106
- children: Array.isArray(children) ? children === null || children === void 0 ? void 0 : children.map(function (item) {
107
- if (item === "<br>") {
108
- return /*#__PURE__*/_jsx("br", {});
109
- }
110
- return item;
111
- }) : children
112
- }));
113
- }
114
- }, extraComponents || {})
114
+ /** Table cell that turns literal "<br>" text nodes into real line breaks. */
115
+ var TableCell = function TableCell(props) {
116
+ // `node` is react-markdown's hast node — pulled out so it never reaches the DOM
117
+ var children = props.children,
118
+ _node = props.node,
119
+ tdProps = _objectWithoutProperties(props, _excluded2);
120
+ return /*#__PURE__*/_jsx("td", _objectSpread(_objectSpread({}, tdProps), {}, {
121
+ children: Array.isArray(children) ? children === null || children === void 0 ? void 0 : children.map(function (item, index) {
122
+ return item === "<br>" ? /*#__PURE__*/_jsx("br", {}, index) : item;
123
+ }) : children
115
124
  }));
116
125
  };
126
+ var MarkdownViews = function MarkdownViews(props) {
127
+ var className = props.className,
128
+ copyProps = props.copyProps,
129
+ extraComponents = props.components;
130
+ return (
131
+ /*#__PURE__*/
132
+ // The components map below may be a fresh object each render; that is harmless because the
133
+ // renderers it points at are stable module-level types, which is what React reconciles on.
134
+ _jsx(CopyPropsContext.Provider, {
135
+ value: copyProps,
136
+ children: /*#__PURE__*/_jsx(ReactMarkdown, _objectSpread(_objectSpread({}, props), {}, {
137
+ rehypePlugins: [rehypeHighlight, rehypeKatex],
138
+ remarkPlugins: [remarkGfm, remarkMath],
139
+ className: classNames(styles.MarkdownViews, className, "markdown-body"),
140
+ components: _objectSpread({
141
+ code: CodeBlock,
142
+ td: TableCell
143
+ }, extraComponents || {})
144
+ }))
145
+ })
146
+ );
147
+ };
117
148
  export default MarkdownViews;
@@ -68,6 +68,21 @@
68
68
  color: #efefef;
69
69
 
70
70
  background: #212121;
71
+
72
+ // Copy's root div (class is module-hashed, so it can only be reached as a child).
73
+ // It colors itself from theme tokens — in light mode --cf-text-2 is near-black,
74
+ // which disappears against this strip. The strip stays dark on purpose to match the
75
+ // atom-one-dark code body, so the button is pinned to light-on-dark rather than
76
+ // following the page theme.
77
+ > div {
78
+ color: rgba(255, 255, 255, 0.65);
79
+
80
+ &:hover {
81
+ color: #ffffff;
82
+
83
+ background-color: rgba(255, 255, 255, 0.12);
84
+ }
85
+ }
71
86
  }
72
87
  }
73
88
  }
@@ -107,6 +107,8 @@ var TextEllipsis = function TextEllipsis(_ref) {
107
107
  };
108
108
  var textContent = getTextContent(children);
109
109
  var update = function update() {
110
+ // Detached nodes measure 0/0 — never let them overwrite a live measurement
111
+ if (!element.isConnected) return;
110
112
  var isTextOverflow = element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;
111
113
  setIsOverflow(isTextOverflow);
112
114
  setMeasuredWidth(element.clientWidth);
@@ -179,24 +181,24 @@ var TextEllipsis = function TextEllipsis(_ref) {
179
181
  })]
180
182
  });
181
183
 
182
- // Show the Tooltip only when overflowing and not disabled
183
- if (isOverflow && !disabled) {
184
- return /*#__PURE__*/_jsx(Tooltip, _objectSpread(_objectSpread({
185
- arrow: false,
186
- placement: "bottomLeft"
187
- }, restTooltipConfig), {}, {
188
- title: tooltipTitle,
189
- styles: _objectSpread({
190
- body: {
191
- width: tooltipWidth,
192
- overflow: "auto",
193
- maxHeight: "300px",
194
- whiteSpace: "pre-wrap"
195
- }
196
- }, styles),
197
- children: content
198
- }));
199
- }
200
- return content;
184
+ // Always keep the Tooltip wrapper mounted and toggle via title:
185
+ // conditionally wrapping remounts the text DOM when isOverflow flips, which detaches
186
+ // the node the measurement effect's ResizeObserver holds — its final callback then
187
+ // reads 0/0 from the detached node and resets isOverflow, so the Tooltip never shows.
188
+ return /*#__PURE__*/_jsx(Tooltip, _objectSpread(_objectSpread({
189
+ arrow: false,
190
+ placement: "bottomLeft"
191
+ }, restTooltipConfig), {}, {
192
+ title: isOverflow && !disabled ? tooltipTitle : undefined,
193
+ styles: _objectSpread({
194
+ body: {
195
+ width: tooltipWidth,
196
+ overflow: "auto",
197
+ maxHeight: "300px",
198
+ whiteSpace: "pre-wrap"
199
+ }
200
+ }, styles),
201
+ children: content
202
+ }));
201
203
  };
202
204
  export default TextEllipsis;
@@ -33,19 +33,3 @@
33
33
  }
34
34
  }
35
35
  }
36
-
37
- .Copyed {
38
- display: flex;
39
- align-items: center;
40
- column-gap: 5px;
41
-
42
- font-size: 14px;
43
- line-height: 18px;
44
- color: var(--text-color);
45
-
46
- cursor: default;
47
-
48
- .icon {
49
- font-size: 18px;
50
- }
51
- }
@@ -1,7 +1,8 @@
1
1
  import React, { ReactNode } from "react";
2
2
  export interface CopyProps {
3
3
  id: string;
4
- isMessage?: true;
4
+ /** true (default): report via a global toast; false: swap the button for an inline "已复制!" state */
5
+ isMessage?: boolean;
5
6
  md?: boolean;
6
7
  text?: string;
7
8
  copyIcon?: ReactNode;
@@ -34,7 +34,7 @@
34
34
 
35
35
  font-size: 14px;
36
36
  line-height: 18px;
37
- color: var(--text-color);
37
+ color: var(--cf-text);
38
38
 
39
39
  cursor: default;
40
40
 
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _react = _interopRequireDefault(require("react"));
7
+ var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactMarkdown = _interopRequireDefault(require("react-markdown"));
9
9
  var _rehypeHighlight = _interopRequireDefault(require("rehype-highlight"));
10
10
  var _rehypeKatex = _interopRequireDefault(require("rehype-katex"));
@@ -14,12 +14,13 @@ var _classnames = _interopRequireDefault(require("classnames"));
14
14
  var _indexModule = _interopRequireDefault(require("./index.module.scss"));
15
15
  require("highlight.js/styles/atom-one-dark.min.css");
16
16
  require("katex/dist/katex.min.css");
17
- var _hsuUtils = require("hsu-utils");
18
17
  var _Copy = _interopRequireDefault(require("../../Copy"));
19
18
  var _MermaidBlock = _interopRequireDefault(require("./MermaidBlock"));
20
19
  var _ArtifactBlock = _interopRequireDefault(require("./ArtifactBlock"));
21
20
  var _jsxRuntime = require("react/jsx-runtime");
22
21
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
22
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
23
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
23
24
  /** Code block languages that support Artifacts preview (modeled after Claude Artifacts) */const ARTIFACT_LANGS = new Set(["html", "svg"]);
24
25
 
25
26
  /**
@@ -35,86 +36,117 @@ const extractText = node => {
35
36
  }
36
37
  return "";
37
38
  };
39
+
40
+ /**
41
+ * Copy button config, handed to the code renderer through context rather than a closure so the
42
+ * renderer can stay a module-level component (see CodeBlock).
43
+ */
44
+ const CopyPropsContext = /*#__PURE__*/(0, _react.createContext)(undefined);
45
+
46
+ /**
47
+ * Fenced code block: mermaid diagram, Artifacts preview, or highlighted code with a copy button.
48
+ *
49
+ * Declared at module scope on purpose. Defining it inline inside MarkdownViews would hand
50
+ * react-markdown a brand-new element type on every render, so React would unmount and remount
51
+ * every code block whenever the markdown re-renders — making the copy button flicker (and losing
52
+ * its "已复制!" state) on each token of streamed content.
53
+ */
54
+ const CodeBlock = props => {
55
+ // `node` is react-markdown's hast node — pulled out so it never reaches the DOM
56
+ const {
57
+ className,
58
+ children,
59
+ node: _node,
60
+ ...codeProps
61
+ } = props;
62
+ const copyProps = (0, _react.useContext)(CopyPropsContext);
63
+ // Stable for the lifetime of the block: regenerating the id on every render would leave the
64
+ // copy button's getElementById lookup pointing at a stale node. Strip ':' so the id stays
65
+ // usable with CSS selectors as well as getElementById.
66
+ const id = `md-code-${(0, _react.useId)().replace(/:/g, "")}`;
67
+ const lang = /language-(\w+)/.exec(className || "")?.[1]?.toLowerCase();
68
+ if (!lang) {
69
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)("code", {
70
+ ...codeProps,
71
+ children: children
72
+ });
73
+ }
74
+ const codeText = extractText(children);
75
+
76
+ // Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
77
+ if (lang === "mermaid") {
78
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_MermaidBlock.default, {
79
+ code: codeText
80
+ });
81
+ }
82
+ const codeView = /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
83
+ className: (0, _classnames.default)(_indexModule.default.code),
84
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
85
+ className: (0, _classnames.default)(_indexModule.default.nav),
86
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Copy.default, {
87
+ id: id,
88
+ md: false,
89
+ ...copyProps
90
+ })
91
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
92
+ className: (0, _classnames.default)(_indexModule.default.content),
93
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)("code", {
94
+ id: id,
95
+ ...codeProps,
96
+ className: (0, _classnames.default)(_indexModule.default.code_content, className),
97
+ children: children
98
+ })
99
+ })]
100
+ });
101
+
102
+ // HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
103
+ if (ARTIFACT_LANGS.has(lang)) {
104
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ArtifactBlock.default, {
105
+ code: codeText,
106
+ lang: lang,
107
+ codeView: codeView
108
+ });
109
+ }
110
+ return codeView;
111
+ };
112
+
113
+ /** Table cell that turns literal "<br>" text nodes into real line breaks. */
114
+ const TableCell = props => {
115
+ // `node` is react-markdown's hast node — pulled out so it never reaches the DOM
116
+ const {
117
+ children,
118
+ node: _node,
119
+ ...tdProps
120
+ } = props;
121
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)("td", {
122
+ ...tdProps,
123
+ children: Array.isArray(children) ? children?.map((item, index) => item === "<br>" ? /*#__PURE__*/(0, _jsxRuntime.jsx)("br", {}, index) : item) : children
124
+ });
125
+ };
38
126
  const MarkdownViews = props => {
39
127
  const {
40
128
  className,
41
129
  copyProps,
42
130
  components: extraComponents
43
131
  } = props;
44
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactMarkdown.default, {
45
- ...props,
46
- rehypePlugins: [_rehypeHighlight.default, _rehypeKatex.default],
47
- remarkPlugins: [_remarkGfm.default, _remarkMath.default],
48
- className: (0, _classnames.default)(_indexModule.default.MarkdownViews, className, "markdown-body"),
49
- components: {
50
- code: ({
51
- className,
52
- children,
53
- ...props
54
- }) => {
55
- const match = /language-(\w+)/.exec(className || "");
56
- if (match?.length) {
57
- const lang = match[1].toLowerCase();
58
- const codeText = extractText(children);
59
-
60
- // Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
61
- if (lang === "mermaid") {
62
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_MermaidBlock.default, {
63
- code: codeText
64
- });
65
- }
66
- const id = (0, _hsuUtils.generateRandomStr)(10);
67
- const codeView = /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
68
- className: (0, _classnames.default)(_indexModule.default.code),
69
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
70
- className: (0, _classnames.default)(_indexModule.default.nav),
71
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Copy.default, {
72
- id: id,
73
- md: false,
74
- ...copyProps
75
- })
76
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
77
- className: (0, _classnames.default)(_indexModule.default.content),
78
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)("code", {
79
- id: id,
80
- ...props,
81
- className: (0, _classnames.default)(_indexModule.default.code_content, className),
82
- children: children
83
- })
84
- })]
85
- });
86
-
87
- // HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
88
- if (ARTIFACT_LANGS.has(lang)) {
89
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ArtifactBlock.default, {
90
- code: codeText,
91
- lang: lang,
92
- codeView: codeView
93
- });
94
- }
95
- return codeView;
132
+ return (
133
+ /*#__PURE__*/
134
+ // The components map below may be a fresh object each render; that is harmless because the
135
+ // renderers it points at are stable module-level types, which is what React reconciles on.
136
+ (0, _jsxRuntime.jsx)(CopyPropsContext.Provider, {
137
+ value: copyProps,
138
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactMarkdown.default, {
139
+ ...props,
140
+ rehypePlugins: [_rehypeHighlight.default, _rehypeKatex.default],
141
+ remarkPlugins: [_remarkGfm.default, _remarkMath.default],
142
+ className: (0, _classnames.default)(_indexModule.default.MarkdownViews, className, "markdown-body"),
143
+ components: {
144
+ code: CodeBlock,
145
+ td: TableCell,
146
+ ...(extraComponents || {})
96
147
  }
97
- return /*#__PURE__*/(0, _jsxRuntime.jsx)("code", {
98
- ...props,
99
- children: children
100
- });
101
- },
102
- td: ({
103
- children,
104
- ...props
105
- }) => {
106
- return /*#__PURE__*/(0, _jsxRuntime.jsx)("td", {
107
- ...props,
108
- children: Array.isArray(children) ? children?.map(item => {
109
- if (item === "<br>") {
110
- return /*#__PURE__*/(0, _jsxRuntime.jsx)("br", {});
111
- }
112
- return item;
113
- }) : children
114
- });
115
- },
116
- ...(extraComponents || {})
117
- }
118
- });
148
+ })
149
+ })
150
+ );
119
151
  };
120
152
  var _default = exports.default = MarkdownViews;
@@ -68,6 +68,21 @@
68
68
  color: #efefef;
69
69
 
70
70
  background: #212121;
71
+
72
+ // Copy's root div (class is module-hashed, so it can only be reached as a child).
73
+ // It colors itself from theme tokens — in light mode --cf-text-2 is near-black,
74
+ // which disappears against this strip. The strip stays dark on purpose to match the
75
+ // atom-one-dark code body, so the button is pinned to light-on-dark rather than
76
+ // following the page theme.
77
+ > div {
78
+ color: rgba(255, 255, 255, 0.65);
79
+
80
+ &:hover {
81
+ color: #ffffff;
82
+
83
+ background-color: rgba(255, 255, 255, 0.12);
84
+ }
85
+ }
71
86
  }
72
87
  }
73
88
  }
@@ -87,6 +87,8 @@ const TextEllipsis = ({
87
87
  };
88
88
  const textContent = getTextContent(children);
89
89
  const update = () => {
90
+ // Detached nodes measure 0/0 — never let them overwrite a live measurement
91
+ if (!element.isConnected) return;
90
92
  const isTextOverflow = element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;
91
93
  setIsOverflow(isTextOverflow);
92
94
  setMeasuredWidth(element.clientWidth);
@@ -157,25 +159,25 @@ const TextEllipsis = ({
157
159
  })]
158
160
  });
159
161
 
160
- // Show the Tooltip only when overflowing and not disabled
161
- if (isOverflow && !disabled) {
162
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_antd.Tooltip, {
163
- arrow: false,
164
- placement: "bottomLeft",
165
- ...restTooltipConfig,
166
- title: tooltipTitle,
167
- styles: {
168
- body: {
169
- width: tooltipWidth,
170
- overflow: "auto",
171
- maxHeight: "300px",
172
- whiteSpace: "pre-wrap"
173
- },
174
- ...styles
162
+ // Always keep the Tooltip wrapper mounted and toggle via title:
163
+ // conditionally wrapping remounts the text DOM when isOverflow flips, which detaches
164
+ // the node the measurement effect's ResizeObserver holds — its final callback then
165
+ // reads 0/0 from the detached node and resets isOverflow, so the Tooltip never shows.
166
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_antd.Tooltip, {
167
+ arrow: false,
168
+ placement: "bottomLeft",
169
+ ...restTooltipConfig,
170
+ title: isOverflow && !disabled ? tooltipTitle : undefined,
171
+ styles: {
172
+ body: {
173
+ width: tooltipWidth,
174
+ overflow: "auto",
175
+ maxHeight: "300px",
176
+ whiteSpace: "pre-wrap"
175
177
  },
176
- children: content
177
- });
178
- }
179
- return content;
178
+ ...styles
179
+ },
180
+ children: content
181
+ });
180
182
  };
181
183
  var _default = exports.default = TextEllipsis;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hsu-react/ui",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "一套基于 React + Ant Design 的中后台业务组件库",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,19 +33,3 @@
33
33
  }
34
34
  }
35
35
  }
36
-
37
- .Copyed {
38
- display: flex;
39
- align-items: center;
40
- column-gap: 5px;
41
-
42
- font-size: 14px;
43
- line-height: 18px;
44
- color: var(--text-color);
45
-
46
- cursor: default;
47
-
48
- .icon {
49
- font-size: 18px;
50
- }
51
- }
@@ -34,7 +34,7 @@
34
34
 
35
35
  font-size: 14px;
36
36
  line-height: 18px;
37
- color: var(--text-color);
37
+ color: var(--cf-text);
38
38
 
39
39
  cursor: default;
40
40
 
@@ -8,7 +8,8 @@ import styles from "./index.module.scss";
8
8
 
9
9
  export interface CopyProps {
10
10
  id: string;
11
- isMessage?: true;
11
+ /** true (default): report via a global toast; false: swap the button for an inline "已复制!" state */
12
+ isMessage?: boolean;
12
13
  md?: boolean;
13
14
  text?: string;
14
15
  copyIcon?: ReactNode;
@@ -68,6 +68,21 @@
68
68
  color: #efefef;
69
69
 
70
70
  background: #212121;
71
+
72
+ // Copy's root div (class is module-hashed, so it can only be reached as a child).
73
+ // It colors itself from theme tokens — in light mode --cf-text-2 is near-black,
74
+ // which disappears against this strip. The strip stays dark on purpose to match the
75
+ // atom-one-dark code body, so the button is pinned to light-on-dark rather than
76
+ // following the page theme.
77
+ > div {
78
+ color: rgba(255, 255, 255, 0.65);
79
+
80
+ &:hover {
81
+ color: #ffffff;
82
+
83
+ background-color: rgba(255, 255, 255, 0.12);
84
+ }
85
+ }
71
86
  }
72
87
  }
73
88
  }
@@ -1,5 +1,8 @@
1
- import React from "react";
2
- import ReactMarkdown, { Options as ReactMarkdownProps } from "react-markdown";
1
+ import React, { createContext, useContext, useId } from "react";
2
+ import ReactMarkdown, {
3
+ Options as ReactMarkdownProps,
4
+ ExtraProps,
5
+ } from "react-markdown";
3
6
  import rehypeHighlight from "rehype-highlight";
4
7
  import rehypeKatex from "rehype-katex";
5
8
  import remarkGfm from "remark-gfm";
@@ -8,7 +11,6 @@ import classNames from "classnames";
8
11
  import styles from "./index.module.scss";
9
12
  import "highlight.js/styles/atom-one-dark.min.css";
10
13
  import "katex/dist/katex.min.css";
11
- import { generateRandomStr } from "hsu-utils";
12
14
  import Copy, { CopyProps } from "../../Copy";
13
15
  import MermaidBlock from "./MermaidBlock";
14
16
  import ArtifactBlock from "./ArtifactBlock";
@@ -30,6 +32,85 @@ const extractText = (node: React.ReactNode): string => {
30
32
  return "";
31
33
  };
32
34
 
35
+ /**
36
+ * Copy button config, handed to the code renderer through context rather than a closure so the
37
+ * renderer can stay a module-level component (see CodeBlock).
38
+ */
39
+ const CopyPropsContext = createContext<Omit<CopyProps, "id"> | undefined>(
40
+ undefined,
41
+ );
42
+
43
+ /**
44
+ * Fenced code block: mermaid diagram, Artifacts preview, or highlighted code with a copy button.
45
+ *
46
+ * Declared at module scope on purpose. Defining it inline inside MarkdownViews would hand
47
+ * react-markdown a brand-new element type on every render, so React would unmount and remount
48
+ * every code block whenever the markdown re-renders — making the copy button flicker (and losing
49
+ * its "已复制!" state) on each token of streamed content.
50
+ */
51
+ const CodeBlock: React.FC<React.ComponentProps<"code"> & ExtraProps> = (
52
+ props,
53
+ ) => {
54
+ // `node` is react-markdown's hast node — pulled out so it never reaches the DOM
55
+ const { className, children, node: _node, ...codeProps } = props;
56
+ const copyProps = useContext(CopyPropsContext);
57
+ // Stable for the lifetime of the block: regenerating the id on every render would leave the
58
+ // copy button's getElementById lookup pointing at a stale node. Strip ':' so the id stays
59
+ // usable with CSS selectors as well as getElementById.
60
+ const id = `md-code-${useId().replace(/:/g, "")}`;
61
+
62
+ const lang = /language-(\w+)/.exec(className || "")?.[1]?.toLowerCase();
63
+ if (!lang) {
64
+ return <code {...codeProps}>{children}</code>;
65
+ }
66
+
67
+ const codeText = extractText(children);
68
+
69
+ // Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
70
+ if (lang === "mermaid") {
71
+ return <MermaidBlock code={codeText} />;
72
+ }
73
+
74
+ const codeView = (
75
+ <div className={classNames(styles.code)}>
76
+ <div className={classNames(styles.nav)}>
77
+ <Copy id={id} md={false} {...copyProps} />
78
+ </div>
79
+ <div className={classNames(styles.content)}>
80
+ <code
81
+ id={id}
82
+ {...codeProps}
83
+ className={classNames(styles.code_content, className)}
84
+ >
85
+ {children}
86
+ </code>
87
+ </div>
88
+ </div>
89
+ );
90
+
91
+ // HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
92
+ if (ARTIFACT_LANGS.has(lang)) {
93
+ return <ArtifactBlock code={codeText} lang={lang} codeView={codeView} />;
94
+ }
95
+ return codeView;
96
+ };
97
+
98
+ /** Table cell that turns literal "<br>" text nodes into real line breaks. */
99
+ const TableCell: React.FC<React.ComponentProps<"td"> & ExtraProps> = (props) => {
100
+ // `node` is react-markdown's hast node — pulled out so it never reaches the DOM
101
+ const { children, node: _node, ...tdProps } = props;
102
+
103
+ return (
104
+ <td {...tdProps}>
105
+ {Array.isArray(children)
106
+ ? children?.map((item, index) =>
107
+ item === "<br>" ? <br key={index} /> : item,
108
+ )
109
+ : children}
110
+ </td>
111
+ );
112
+ };
113
+
33
114
  export interface MarkdownViewsProps extends ReactMarkdownProps {
34
115
  copyProps?: Omit<CopyProps, "id">;
35
116
  }
@@ -38,69 +119,21 @@ const MarkdownViews: React.FC<MarkdownViewsProps> = (props) => {
38
119
  const { className, copyProps, components: extraComponents } = props;
39
120
 
40
121
  return (
41
- <ReactMarkdown
42
- {...props}
43
- rehypePlugins={[rehypeHighlight, rehypeKatex]}
44
- remarkPlugins={[remarkGfm, remarkMath]}
45
- className={classNames(styles.MarkdownViews, className, "markdown-body")}
46
- components={{
47
- code: ({ className, children, ...props }) => {
48
- const match = /language-(\w+)/.exec(className || "");
49
- if (match?.length) {
50
- const lang = match[1].toLowerCase();
51
- const codeText = extractText(children);
52
-
53
- // Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
54
- if (lang === "mermaid") {
55
- return <MermaidBlock code={codeText} />;
56
- }
57
-
58
- const id = generateRandomStr(10);
59
- const codeView = (
60
- <div className={classNames(styles.code)}>
61
- <div className={classNames(styles.nav)}>
62
- <Copy id={id} md={false} {...copyProps} />
63
- </div>
64
- <div className={classNames(styles.content)}>
65
- <code
66
- id={id}
67
- {...props}
68
- className={classNames(styles.code_content, className)}
69
- >
70
- {children}
71
- </code>
72
- </div>
73
- </div>
74
- );
75
-
76
- // HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
77
- if (ARTIFACT_LANGS.has(lang)) {
78
- return (
79
- <ArtifactBlock code={codeText} lang={lang} codeView={codeView} />
80
- );
81
- }
82
- return codeView;
83
- }
84
- return <code {...props}>{children}</code>;
85
- },
86
- td: ({ children, ...props }) => {
87
- return (
88
- <td {...props}>
89
- {Array.isArray(children)
90
- ? children?.map((item) => {
91
- if (item === "<br>") {
92
- return <br />;
93
- }
94
-
95
- return item;
96
- })
97
- : children}
98
- </td>
99
- );
100
- },
101
- ...(extraComponents || {}),
102
- }}
103
- />
122
+ // The components map below may be a fresh object each render; that is harmless because the
123
+ // renderers it points at are stable module-level types, which is what React reconciles on.
124
+ <CopyPropsContext.Provider value={copyProps}>
125
+ <ReactMarkdown
126
+ {...props}
127
+ rehypePlugins={[rehypeHighlight, rehypeKatex]}
128
+ remarkPlugins={[remarkGfm, remarkMath]}
129
+ className={classNames(styles.MarkdownViews, className, "markdown-body")}
130
+ components={{
131
+ code: CodeBlock,
132
+ td: TableCell,
133
+ ...(extraComponents || {}),
134
+ }}
135
+ />
136
+ </CopyPropsContext.Provider>
104
137
  );
105
138
  };
106
139
 
@@ -109,6 +109,8 @@ const TextEllipsis: React.FC<TextEllipsisProps> = ({
109
109
  const textContent = getTextContent(children);
110
110
 
111
111
  const update = () => {
112
+ // Detached nodes measure 0/0 — never let them overwrite a live measurement
113
+ if (!element.isConnected) return;
112
114
  const isTextOverflow =
113
115
  element.scrollWidth > element.clientWidth ||
114
116
  element.scrollHeight > element.clientHeight;
@@ -189,30 +191,29 @@ const TextEllipsis: React.FC<TextEllipsisProps> = ({
189
191
  </span>
190
192
  );
191
193
 
192
- // Show the Tooltip only when overflowing and not disabled
193
- if (isOverflow && !disabled) {
194
- return (
195
- <Tooltip
196
- arrow={false}
197
- placement="bottomLeft"
198
- {...restTooltipConfig}
199
- title={tooltipTitle}
200
- styles={{
201
- body: {
202
- width: tooltipWidth,
203
- overflow: "auto",
204
- maxHeight: "300px",
205
- whiteSpace: "pre-wrap",
206
- },
207
- ...styles,
208
- }}
209
- >
210
- {content}
211
- </Tooltip>
212
- );
213
- }
214
-
215
- return content;
194
+ // Always keep the Tooltip wrapper mounted and toggle via title:
195
+ // conditionally wrapping remounts the text DOM when isOverflow flips, which detaches
196
+ // the node the measurement effect's ResizeObserver holds — its final callback then
197
+ // reads 0/0 from the detached node and resets isOverflow, so the Tooltip never shows.
198
+ return (
199
+ <Tooltip
200
+ arrow={false}
201
+ placement="bottomLeft"
202
+ {...restTooltipConfig}
203
+ title={isOverflow && !disabled ? tooltipTitle : undefined}
204
+ styles={{
205
+ body: {
206
+ width: tooltipWidth,
207
+ overflow: "auto",
208
+ maxHeight: "300px",
209
+ whiteSpace: "pre-wrap",
210
+ },
211
+ ...styles,
212
+ }}
213
+ >
214
+ {content}
215
+ </Tooltip>
216
+ );
216
217
  };
217
218
 
218
219
  export default TextEllipsis;