one-design-next 0.0.39 → 0.0.41

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.
@@ -35,6 +35,14 @@ export interface Attachment {
35
35
  status?: 'uploading' | 'error' | 'expired' | 'done';
36
36
  /** status='error' 时展示的失败原因文案。 */
37
37
  errorMessage?: string;
38
+ /**
39
+ * 图片自然像素宽 / 高(image/* 附件可选携带)。
40
+ * 主要用途:图片过期后 url 已失效、无法再测量,用存下来的原始尺寸
41
+ * 让「已过期」占位复刻原图 footprint(单图场景),消息流不因过期而跳版。
42
+ * 拿不到时占位退化成 88×88(同多图)。
43
+ */
44
+ width?: number;
45
+ height?: number;
38
46
  /**
39
47
  * 特殊变体:
40
48
  * - 'report':AI 生成报告的大卡片(含创建时间 + 右侧预览缩略图)。
@@ -3,11 +3,16 @@ import HoverFill from "../hover-fill";
3
3
  import Icon from "../icon";
4
4
  import "./style";
5
5
  var categoryMap = {
6
- image: {
7
- icon: 'image',
6
+ sheet: {
7
+ icon: 'table',
8
8
  color: 'var(--odn-color-green-6)',
9
9
  bg: 'var(--odn-color-green-2)'
10
10
  },
11
+ slides: {
12
+ icon: 'presentation',
13
+ color: 'var(--odn-color-orange-6)',
14
+ bg: 'var(--odn-color-orange-2)'
15
+ },
11
16
  code: {
12
17
  icon: 'file-code',
13
18
  color: 'var(--odn-color-blue-8)',
@@ -40,17 +45,17 @@ var defaultMeta = {
40
45
  bg: 'var(--odn-color-solid-black-3)'
41
46
  };
42
47
  var mimeCategory = {
43
- 'application/pdf': 'doc',
48
+ 'application/pdf': 'text',
44
49
  'application/msword': 'doc',
45
50
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'doc',
46
- 'application/vnd.ms-excel': 'doc',
47
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'doc',
48
- 'application/vnd.ms-powerpoint': 'doc',
49
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'doc',
51
+ 'application/vnd.ms-excel': 'sheet',
52
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'sheet',
53
+ 'application/vnd.ms-powerpoint': 'slides',
54
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'slides',
50
55
  'application/json': 'data',
51
56
  'application/xml': 'data',
52
57
  'text/xml': 'data',
53
- 'text/csv': 'data',
58
+ 'text/csv': 'sheet',
54
59
  'text/html': 'code',
55
60
  'text/css': 'code',
56
61
  'text/javascript': 'code',
@@ -71,7 +76,6 @@ var mimeCategory = {
71
76
  function getFileMeta(mime) {
72
77
  if (mimeCategory[mime]) return categoryMap[mimeCategory[mime]];
73
78
  var prefix = mime.split('/')[0];
74
- if (prefix === 'image') return categoryMap.image;
75
79
  if (prefix === 'text') return categoryMap.text;
76
80
  return defaultMeta;
77
81
  }
@@ -201,6 +201,14 @@ export interface ComposerProps {
201
201
  * `SkillItem.initialState='pending'` 插入后会自动打开。
202
202
  */
203
203
  renderParamPanel?: (ctx: ComposerParamPanelContext) => React.ReactNode;
204
+ /**
205
+ * 工具栏按钮被点击时触发。
206
+ *
207
+ * - `tool`:被点击的工具类型(`'attachment'` / `'webSearch'` / `'skill'`)。
208
+ * - 对于 `'skill'`:在 skill 弹窗打开时触发(而非选中某个 skill 时)。
209
+ * - 此回调为纯通知性质,不影响组件内部行为(若需阻止,请使用 `disabled` 等控制)。
210
+ */
211
+ onToolClick?: (tool: ComposerTool) => void;
204
212
  className?: string;
205
213
  style?: React.CSSProperties;
206
214
  }
@@ -117,6 +117,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
117
117
  _ref$accept = _ref.accept,
118
118
  accept = _ref$accept === void 0 ? DEFAULT_ACCEPT : _ref$accept,
119
119
  renderParamPanel = _ref.renderParamPanel,
120
+ onToolClick = _ref.onToolClick,
120
121
  className = _ref.className,
121
122
  style = _ref.style;
122
123
  var _useControlledState = useControlledState(defaultValue, valueProp),
@@ -489,7 +490,6 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
489
490
  var composerBoxFileDragAttrs = fileDragOver ? {
490
491
  'data-odn-composer-file-drag': ''
491
492
  } : {};
492
- var showFileDragOverlay = fileDragOver && !disabled && !attachmentsDisabled;
493
493
  var handleComposerDragEnter = function handleComposerDragEnter(e) {
494
494
  e.preventDefault();
495
495
  if (disabled || attachmentsDisabled) return;
@@ -670,7 +670,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
670
670
  type: "button",
671
671
  onClick: function onClick() {
672
672
  var _fileInputRef$current;
673
- return (_fileInputRef$current = fileInputRef.current) === null || _fileInputRef$current === void 0 ? void 0 : _fileInputRef$current.click();
673
+ onToolClick === null || onToolClick === void 0 || onToolClick('attachment');
674
+ (_fileInputRef$current = fileInputRef.current) === null || _fileInputRef$current === void 0 || _fileInputRef$current.click();
674
675
  },
675
676
  disabled: btnDisabled,
676
677
  "data-odn-composer-tool-btn": true,
@@ -694,7 +695,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
694
695
  }, /*#__PURE__*/React.createElement("button", {
695
696
  type: "button",
696
697
  onClick: function onClick() {
697
- return setWebSearch(function (v) {
698
+ onToolClick === null || onToolClick === void 0 || onToolClick('webSearch');
699
+ setWebSearch(function (v) {
698
700
  return !v;
699
701
  });
700
702
  },
@@ -726,6 +728,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
726
728
  visible: !disabled && toolMenuOpen,
727
729
  onVisibleChange: function onVisibleChange(v) {
728
730
  if (disabled) return;
731
+ if (v) onToolClick === null || onToolClick === void 0 || onToolClick('skill');
729
732
  setToolMenuOpen(v);
730
733
  },
731
734
  popupClassName: "odn-composer-skill-popup",
@@ -993,13 +996,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
993
996
  "data-odn-composer-lite-tools": true
994
997
  }, toolNodes), isLite && /*#__PURE__*/React.createElement("div", {
995
998
  "data-odn-composer-lite-send": true
996
- }, sendOrStopButton), showFileDragOverlay && /*#__PURE__*/React.createElement("div", {
997
- "data-odn-composer-file-drag-overlay": true,
998
- "aria-hidden": true
999
- }, /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement(Icon, {
1000
- name: "paperclip",
1001
- size: 14
1002
- }), "\u62D6\u653E\u6587\u4EF6\u5230\u6B64\u5904\u4E0A\u4F20"))), hint && /*#__PURE__*/React.createElement("p", {
999
+ }, sendOrStopButton)), hint && /*#__PURE__*/React.createElement("p", {
1003
1000
  "data-odn-composer-hint": true
1004
1001
  }, submitType === 'enter' ? 'Enter 发送 · Shift+Enter 换行 · 可拖入或粘贴文件' : 'Shift+Enter 发送 · Enter 换行 · 可拖入或粘贴文件')));
1005
1002
  });
@@ -35,42 +35,6 @@
35
35
  background: color-mix(in srgb, var(--odn-color-brand-6) 6%, var(--odn-color-bg-elevated));
36
36
  }
37
37
 
38
- [data-odn-composer-file-drag-overlay] {
39
- position: absolute;
40
- inset: 0;
41
- border-radius: inherit;
42
- background: color-mix(in srgb, var(--odn-color-brand-6) 8%, transparent);
43
- backdrop-filter: blur(4px);
44
- -webkit-backdrop-filter: blur(4px);
45
- display: flex;
46
- align-items: center;
47
- justify-content: center;
48
- pointer-events: none;
49
- z-index: 4;
50
- }
51
- [data-odn-composer-file-drag-overlay] span {
52
- display: inline-flex;
53
- align-items: center;
54
- gap: 6px;
55
- color: var(--odn-color-brand-6);
56
- font-size: 14px;
57
- line-height: 24px;
58
- font-weight: 600;
59
- }
60
-
61
- /* 拖放蒙层出现时,冻结内部交互样式事件;保留 box 自身 drop 能力。 */
62
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-editor],
63
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-toolbar],
64
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-lite-tools],
65
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-lite-send],
66
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-attachments] {
67
- pointer-events: none !important;
68
- }
69
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-editor] {
70
- caret-color: transparent;
71
- user-select: none;
72
- }
73
-
74
38
  /* ------------------------------------------------------------------
75
39
  * lite variant:
76
40
  * tools 和 send 按钮 absolute 在 box 的左下/右下;
@@ -8,7 +8,7 @@ export interface ImageProps {
8
8
  alt?: string;
9
9
  /**
10
10
  * 整图点击回调。传此 prop 即把图片包一层 button,业务方按需对接
11
- * `PreviewPanel` 等 lightbox 组件。
11
+ * `PreviewPanel` 等 lightbox 组件。失败态(unavailable / 加载出错)下自动屏蔽。
12
12
  */
13
13
  onPreview?: () => void;
14
14
  /**
@@ -16,6 +16,20 @@ export interface ImageProps {
16
16
  * trash icon,用于 Composer 待发送态等需要删除的场景。
17
17
  */
18
18
  onRemove?: () => void;
19
+ /**
20
+ * 主动标记图片不可用(如上层已知 url 失效)。为 true 时**不加载 url**、
21
+ * 直接渲染 `fallback`,并屏蔽 onPreview。
22
+ *
23
+ * 这是渲染层的中性概念——leaf 不认识「过期 / 已删除」等业务原因,只知道
24
+ * 「这张图画不出来」。业务原因与文案由上层(如 `MessageImage`)翻译后
25
+ * 通过 `unavailable` + `fallback` 传入。
26
+ */
27
+ unavailable?: boolean;
28
+ /**
29
+ * 画不出图时(`unavailable` 或 `<img>` 运行时 onError)渲染的兜底内容。
30
+ * 不传则用内置中性占位(image-off 图标)。
31
+ */
32
+ fallback?: React.ReactNode;
19
33
  /** 透传到容器(非 img 本体)的 className,用于上层套尺寸约束。 */
20
34
  className?: string;
21
35
  /** 透传到容器(非 img 本体)的 style,用于上层套尺寸约束。 */
@@ -32,12 +46,13 @@ export interface ImageProps {
32
46
  * 不职责:
33
47
  * - 不带尺寸 opinion(max-width / max-height / object-fit 全由上层套)
34
48
  * - 不带消息态 / 输入态等业务语义
49
+ * - 不认识「过期 / 已删除」等业务原因,只认「画得出 / 画不出」(unavailable / fallback)
35
50
  *
36
51
  * 业务态包装在更上层的组件里实现:
37
52
  * - 消息态:`<MessageImage>` 套一层 max 280×360 contain(单图)或 88×88 cover(多图)容器
38
53
  * - Composer 待发送态:composer 内部直接套一层 72×72 cover 容器
39
54
  */
40
- export declare function Image({ url, alt, onPreview, onRemove, className, style }: ImageProps): import("react").JSX.Element;
55
+ export declare function Image({ url, alt, onPreview, onRemove, unavailable, fallback, className, style, }: ImageProps): import("react").JSX.Element;
41
56
  export declare namespace Image {
42
57
  var displayName: string;
43
58
  }
@@ -1,3 +1,10 @@
1
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
2
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
3
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
4
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
5
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
7
+ import { useState } from 'react';
1
8
  import Icon from "../icon";
2
9
  import "./style";
3
10
  /**
@@ -11,6 +18,7 @@ import "./style";
11
18
  * 不职责:
12
19
  * - 不带尺寸 opinion(max-width / max-height / object-fit 全由上层套)
13
20
  * - 不带消息态 / 输入态等业务语义
21
+ * - 不认识「过期 / 已删除」等业务原因,只认「画得出 / 画不出」(unavailable / fallback)
14
22
  *
15
23
  * 业务态包装在更上层的组件里实现:
16
24
  * - 消息态:`<MessageImage>` 套一层 max 280×360 contain(单图)或 88×88 cover(多图)容器
@@ -21,18 +29,41 @@ export function Image(_ref) {
21
29
  alt = _ref.alt,
22
30
  onPreview = _ref.onPreview,
23
31
  onRemove = _ref.onRemove,
32
+ unavailable = _ref.unavailable,
33
+ fallback = _ref.fallback,
24
34
  className = _ref.className,
25
35
  style = _ref.style;
26
- var imgEl = /*#__PURE__*/React.createElement("img", {
36
+ // 运行时加载失败:url 存活性不由上层保证(签名过期 / 404 / 断网都可能),
37
+ // onError 兜住让「画不出」自动降级,不出碎图。
38
+ var _useState = useState(false),
39
+ _useState2 = _slicedToArray(_useState, 2),
40
+ loadError = _useState2[0],
41
+ setLoadError = _useState2[1];
42
+ var showFallback = !!unavailable || loadError;
43
+
44
+ // 默认占位只有图标、无文案——leaf 不知道「为什么画不出」,任何原因文字
45
+ // 都是业务语义,由上层通过 fallback 传入(如 MessageImage 传「已过期」)。
46
+ var fallbackEl = /*#__PURE__*/React.createElement("div", {
47
+ "data-odn-image-fallback": true
48
+ }, fallback !== null && fallback !== void 0 ? fallback : /*#__PURE__*/React.createElement(Icon, {
49
+ name: "image-off",
50
+ size: 20
51
+ }));
52
+ var imgEl = showFallback ? fallbackEl : /*#__PURE__*/React.createElement("img", {
27
53
  src: url,
28
54
  alt: alt !== null && alt !== void 0 ? alt : '',
29
- loading: "lazy"
55
+ loading: "lazy",
56
+ onError: function onError() {
57
+ return setLoadError(true);
58
+ }
30
59
  });
60
+ var canPreview = !!onPreview && !showFallback;
31
61
  return /*#__PURE__*/React.createElement("div", {
32
62
  "data-odn-image": true,
63
+ "data-odn-image-unavailable": showFallback || undefined,
33
64
  className: className,
34
65
  style: style
35
- }, onPreview ? /*#__PURE__*/React.createElement("button", {
66
+ }, canPreview ? /*#__PURE__*/React.createElement("button", {
36
67
  type: "button",
37
68
  onClick: onPreview,
38
69
  "data-odn-image-trigger": true,
@@ -7,6 +7,30 @@
7
7
  display: block;
8
8
  }
9
9
 
10
+ /* 不可用态(unavailable 或运行时加载失败):渲染中性占位。
11
+ * 弱化感由占位块自身的纯色灰底 + 纯色图标承担,不用容器 opacity——
12
+ * 整体降透明会让图标 / 文字变成半透明合成,视觉上「层叠」发虚。 */
13
+ /* 占位块:和 <img> 一样填满上层套的尺寸盒(width/height 100%),leaf 不带
14
+ * 尺寸 opinion;上层没套尺寸时退化成按内容(图标 / 文字)撑开——不硬编码 min。
15
+ * 纯色 solid-black:底 solid-black-2、图标 / 文字 solid-black-8,
16
+ * 全程不透明,避免 alpha token 叠底产生的透视感。 */
17
+ [data-odn-image-fallback] {
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ justify-content: center;
22
+ gap: 8px;
23
+ width: 100%;
24
+ height: 100%;
25
+ padding: 8px;
26
+ box-sizing: border-box;
27
+ background: var(--odn-color-solid-black-2);
28
+ color: var(--odn-color-solid-black-8);
29
+ font-size: 12px;
30
+ line-height: 1.25;
31
+ text-align: center;
32
+ }
33
+
10
34
  /** 整图作为 onPreview 触发区时的 button 包装;样式上完全融入容器(无边框、无 padding)。 */
11
35
  [data-odn-image-trigger] {
12
36
  display: block;
@@ -0,0 +1,24 @@
1
+ /// <reference types="react" />
2
+ /**
3
+ * 大图查看器的最小数据形态。刻意不复用 `MessageImageItem`——查看器只认
4
+ * url / alt,与「消息态」无关,保持解耦(MessageImageItem 结构上可直接传入)。
5
+ */
6
+ export interface ImageViewerItem {
7
+ url: string;
8
+ alt?: string;
9
+ }
10
+ interface ImageViewerProps {
11
+ images: ImageViewerItem[];
12
+ initialIndex: number;
13
+ onClose: () => void;
14
+ }
15
+ /**
16
+ * MessageImage 的**私有内置**大图查看器——不写进 `components/index.ts`,
17
+ * 外部 import 不到。多个图片组件(未来含 Attachments)可共用这一份实现。
18
+ *
19
+ * 交互:ESC / 点遮罩 关闭;←/→ 切换;点图本身不关。
20
+ * 用 portal 挂到 body,规避祖先 `transform` / `overflow` 对 `position: fixed`
21
+ * 的截断(聊天流容器常带这些)。mounted guard 兜 SSR。
22
+ */
23
+ export declare function ImageViewer({ images, initialIndex, onClose }: ImageViewerProps): import("react").ReactPortal | null;
24
+ export default ImageViewer;
@@ -0,0 +1,119 @@
1
+ 'use client';
2
+
3
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
4
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
5
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
6
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
7
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
8
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
9
+ import { useCallback, useEffect, useState } from 'react';
10
+ import { createPortal } from 'react-dom';
11
+ import Icon from "../icon";
12
+
13
+ /**
14
+ * 大图查看器的最小数据形态。刻意不复用 `MessageImageItem`——查看器只认
15
+ * url / alt,与「消息态」无关,保持解耦(MessageImageItem 结构上可直接传入)。
16
+ */
17
+
18
+ /**
19
+ * MessageImage 的**私有内置**大图查看器——不写进 `components/index.ts`,
20
+ * 外部 import 不到。多个图片组件(未来含 Attachments)可共用这一份实现。
21
+ *
22
+ * 交互:ESC / 点遮罩 关闭;←/→ 切换;点图本身不关。
23
+ * 用 portal 挂到 body,规避祖先 `transform` / `overflow` 对 `position: fixed`
24
+ * 的截断(聊天流容器常带这些)。mounted guard 兜 SSR。
25
+ */
26
+ export function ImageViewer(_ref) {
27
+ var _it$alt;
28
+ var images = _ref.images,
29
+ initialIndex = _ref.initialIndex,
30
+ onClose = _ref.onClose;
31
+ var _useState = useState(initialIndex),
32
+ _useState2 = _slicedToArray(_useState, 2),
33
+ index = _useState2[0],
34
+ setIndex = _useState2[1];
35
+ var _useState3 = useState(false),
36
+ _useState4 = _slicedToArray(_useState3, 2),
37
+ mounted = _useState4[0],
38
+ setMounted = _useState4[1];
39
+ var total = images.length;
40
+ var prev = useCallback(function () {
41
+ return setIndex(function (i) {
42
+ return (i - 1 + total) % total;
43
+ });
44
+ }, [total]);
45
+ var next = useCallback(function () {
46
+ return setIndex(function (i) {
47
+ return (i + 1) % total;
48
+ });
49
+ }, [total]);
50
+ useEffect(function () {
51
+ return setMounted(true);
52
+ }, []);
53
+ useEffect(function () {
54
+ var onKey = function onKey(e) {
55
+ if (e.key === 'Escape') onClose();
56
+ if (e.key === 'ArrowLeft') prev();
57
+ if (e.key === 'ArrowRight') next();
58
+ };
59
+ document.addEventListener('keydown', onKey);
60
+ return function () {
61
+ return document.removeEventListener('keydown', onKey);
62
+ };
63
+ }, [onClose, prev, next]);
64
+ var it = images[index];
65
+ if (!mounted || !it) return null;
66
+ var overlay = /*#__PURE__*/React.createElement("div", {
67
+ "data-odn-image-viewer": true,
68
+ role: "dialog",
69
+ "aria-modal": "true",
70
+ onClick: onClose
71
+ }, /*#__PURE__*/React.createElement("img", {
72
+ src: it.url,
73
+ alt: (_it$alt = it.alt) !== null && _it$alt !== void 0 ? _it$alt : '',
74
+ onClick: function onClick(e) {
75
+ return e.stopPropagation();
76
+ },
77
+ "data-odn-image-viewer-img": true
78
+ }), /*#__PURE__*/React.createElement("button", {
79
+ type: "button",
80
+ onClick: function onClick(e) {
81
+ e.stopPropagation();
82
+ onClose();
83
+ },
84
+ "data-odn-image-viewer-btn": "close",
85
+ "aria-label": "\u5173\u95ED\uFF08ESC\uFF09",
86
+ title: "\u5173\u95ED\uFF08ESC\uFF09"
87
+ }, /*#__PURE__*/React.createElement(Icon, {
88
+ name: "x",
89
+ size: 20
90
+ })), total > 1 && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
91
+ type: "button",
92
+ onClick: function onClick(e) {
93
+ e.stopPropagation();
94
+ prev();
95
+ },
96
+ "data-odn-image-viewer-btn": "prev",
97
+ "aria-label": "\u4E0A\u4E00\u5F20\uFF08\u2190\uFF09",
98
+ title: "\u4E0A\u4E00\u5F20\uFF08\u2190\uFF09"
99
+ }, /*#__PURE__*/React.createElement(Icon, {
100
+ name: "chevron-left",
101
+ size: 24
102
+ })), /*#__PURE__*/React.createElement("button", {
103
+ type: "button",
104
+ onClick: function onClick(e) {
105
+ e.stopPropagation();
106
+ next();
107
+ },
108
+ "data-odn-image-viewer-btn": "next",
109
+ "aria-label": "\u4E0B\u4E00\u5F20\uFF08\u2192\uFF09",
110
+ title: "\u4E0B\u4E00\u5F20\uFF08\u2192\uFF09"
111
+ }, /*#__PURE__*/React.createElement(Icon, {
112
+ name: "chevron-right",
113
+ size: 24
114
+ })), /*#__PURE__*/React.createElement("div", {
115
+ "data-odn-image-viewer-counter": true
116
+ }, index + 1, " / ", total)));
117
+ return /*#__PURE__*/createPortal(overlay, document.body);
118
+ }
119
+ export default ImageViewer;
@@ -8,15 +8,35 @@ import './style';
8
8
  export interface MessageImageItem {
9
9
  url: string;
10
10
  alt?: string;
11
+ /**
12
+ * 图片已过期(消息流只读态)——历史消息里的图 url 随时间失效。
13
+ * 这是「消息态」的业务语义,由 MessageImage 翻译成 `Image` leaf 的
14
+ * `unavailable` + 「已过期」占位(leaf 本身不认识「过期」这个词)。
15
+ */
16
+ expired?: boolean;
17
+ /**
18
+ * 图片自然像素宽 / 高(可选)。仅用于单图过期时让占位复刻原图尺寸;
19
+ * 拿不到则占位退化成 88×88。
20
+ */
21
+ width?: number;
22
+ height?: number;
11
23
  }
12
24
  export interface MessageImageProps {
13
25
  /** 待渲染的图片列表。 */
14
26
  images: MessageImageItem[];
15
27
  /**
16
- * 点击图片回调,传入图片 index。组件不内置大图查看器,业务方按需对接
17
- * `PreviewPanel` / 自己实现的 lightbox。
28
+ * 是否可点开看大图(**内置**查看器,opt-out)。默认 `true`——「点开看大图」是
29
+ * 消息图片的标配语义。纯展示 / 由业务自己接预览的场景传 `false` 关掉。
30
+ *
31
+ * 过期 / 不可用的图始终不可点开,与此开关无关(`Image` leaf 自动屏蔽)。
32
+ */
33
+ preview?: boolean;
34
+ /**
35
+ * 图片点击**通知**(埋点 / 统计用),传入被点图片 index。这**不是**「自定义
36
+ * 查看器」的挂载点——大图预览由内置查看器(`preview`)负责,此回调只是知会
37
+ * 上层「第 i 张被点了」,不阻止内置行为。过期图不触发。
18
38
  */
19
- onPreview?: (index: number) => void;
39
+ onImageClick?: (index: number) => void;
20
40
  /**
21
41
  * 删除按钮点击回调,传入图片 index。本期 MessageImage 主要承载消息态展示,
22
42
  * 该回调主要为未来扩展(如撤回未发送消息中的图)保留。
@@ -35,7 +55,7 @@ export interface MessageImageProps {
35
55
  * Composer 待发送态等其他场景**不应消费此组件**——那是输入态语义,
36
56
  * 应直接用 `Image` leaf + 自己的尺寸 wrap。
37
57
  */
38
- export declare function MessageImage({ images, onPreview, onRemove }: MessageImageProps): import("react").JSX.Element | null;
58
+ export declare function MessageImage({ images, preview, onImageClick, onRemove, }: MessageImageProps): import("react").JSX.Element | null;
39
59
  export declare namespace MessageImage {
40
60
  var displayName: string;
41
61
  }
@@ -1,6 +1,55 @@
1
+ 'use client';
2
+
3
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
4
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
5
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
6
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
7
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
8
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
9
+ import { useCallback, useMemo, useState } from 'react';
1
10
  import Image from "../image";
11
+ import Icon from "../icon";
12
+ import ImageViewer from "./image-viewer";
2
13
  import "./style";
3
14
 
15
+ /**
16
+ * 单图过期占位的尺寸边界,镜像 style/index.scss 的尺寸 token:
17
+ * - SINGLE_MAX_* 对齐 --odn-message-image-max-width / -max-height
18
+ * - THUMB 对齐 --odn-message-image-thumb-size(多图缩略边长)
19
+ * 放在 wrapper(MessageImage)而非 leaf——尺寸始终归 wrapper。
20
+ */
21
+ var SINGLE_MAX_W = 280;
22
+ var SINGLE_MAX_H = 360;
23
+ var THUMB = 88;
24
+
25
+ /** 过期图片占位:文案由 MessageImage(消息态)传入 leaf——leaf 不认识「过期」。 */
26
+ function ExpiredFallback() {
27
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Icon, {
28
+ name: "image-off",
29
+ size: 20
30
+ }), /*#__PURE__*/React.createElement("span", null, "\u56FE\u7247\u5DF2\u8FC7\u671F"));
31
+ }
32
+
33
+ /**
34
+ * 单图过期时占位盒的尺寸:
35
+ * - 有原图尺寸 → 按 contain 缩放到 max 280×360 内,复刻原图 footprint,不跳版
36
+ * - 没有原图尺寸 → 退化成 88×88(同多图缩略)
37
+ * 多图不走这里——多图恒为 88×88,由 scss 的 multi 选择器兜住。
38
+ */
39
+ function expiredSingleSize(item) {
40
+ if (item.width && item.height) {
41
+ var scale = Math.min(SINGLE_MAX_W / item.width, SINGLE_MAX_H / item.height, 1);
42
+ return {
43
+ width: Math.round(item.width * scale),
44
+ height: Math.round(item.height * scale)
45
+ };
46
+ }
47
+ return {
48
+ width: THUMB,
49
+ height: THUMB
50
+ };
51
+ }
52
+
4
53
  /**
5
54
  * MessageImage 渲染的图片项。
6
55
  * 与 `Attachment` 不同:图片消息的「主体」就是图本身,不需要 name / type / size 等
@@ -21,25 +70,61 @@ import "./style";
21
70
  */
22
71
  export function MessageImage(_ref) {
23
72
  var images = _ref.images,
24
- onPreview = _ref.onPreview,
73
+ _ref$preview = _ref.preview,
74
+ preview = _ref$preview === void 0 ? true : _ref$preview,
75
+ onImageClick = _ref.onImageClick,
25
76
  onRemove = _ref.onRemove;
77
+ var _useState = useState(null),
78
+ _useState2 = _slicedToArray(_useState, 2),
79
+ viewerIndex = _useState2[0],
80
+ setViewerIndex = _useState2[1];
81
+
82
+ // 查看器只收「画得出」的图——过期 / 不可用的排除,避免上下张切到碎图。
83
+ var viewable = useMemo(function () {
84
+ return images.filter(function (img) {
85
+ return !img.expired;
86
+ });
87
+ }, [images]);
88
+
89
+ // 点第 i 张(原始 index):先知会上层,再按需打开内置查看器。
90
+ var handleClick = useCallback(function (i) {
91
+ onImageClick === null || onImageClick === void 0 || onImageClick(i);
92
+ if (!preview) return;
93
+ var pos = viewable.indexOf(images[i]);
94
+ if (pos >= 0) setViewerIndex(pos);
95
+ }, [onImageClick, preview, images, viewable]);
26
96
  if (images.length === 0) return null;
27
97
  var isMulti = images.length > 1;
28
- return /*#__PURE__*/React.createElement("div", {
98
+ // 只要「可预览」或「要通知」,就把图包成可点区(过期图由 leaf 内部再屏蔽)。
99
+ var clickable = preview || !!onImageClick;
100
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
29
101
  "data-odn-message-image": true,
30
102
  "data-odn-message-image-multi": isMulti || undefined
31
103
  }, images.map(function (img, i) {
32
104
  return /*#__PURE__*/React.createElement(Image, {
33
105
  key: i,
34
106
  url: img.url,
35
- alt: img.alt,
36
- onPreview: onPreview ? function () {
37
- return onPreview(i);
107
+ alt: img.alt
108
+ // 「过期」是消息态业务语义,翻译成 leaf 的渲染态:不加载失效 url + 已过期占位。
109
+ ,
110
+ unavailable: img.expired || undefined,
111
+ fallback: img.expired ? /*#__PURE__*/React.createElement(ExpiredFallback, null) : undefined
112
+ // 单图过期时用原图尺寸复刻 footprint;多图恒 88×88 由 scss 兜。
113
+ ,
114
+ style: !isMulti && img.expired ? expiredSingleSize(img) : undefined,
115
+ onPreview: clickable ? function () {
116
+ return handleClick(i);
38
117
  } : undefined,
39
118
  onRemove: onRemove ? function () {
40
119
  return onRemove(i);
41
120
  } : undefined
42
121
  });
122
+ })), viewerIndex !== null && viewable.length > 0 && /*#__PURE__*/React.createElement(ImageViewer, {
123
+ images: viewable,
124
+ initialIndex: viewerIndex,
125
+ onClose: function onClose() {
126
+ return setViewerIndex(null);
127
+ }
43
128
  }));
44
129
  }
45
130
  MessageImage.displayName = 'MessageImage';
@@ -57,4 +57,102 @@ html {
57
57
  height: 100%;
58
58
  max-height: none;
59
59
  object-fit: cover;
60
+ }
61
+
62
+ /* 内置大图查看器(私有 image-viewer.tsx 渲染,portal 到 body):
63
+ * ESC / 点遮罩 关闭;←/→ 切换;点图不关。样式从 message-image-verify 的
64
+ * SimpleLightbox 收编而来——不再散落在 demo 里各写一份。 */
65
+ [data-odn-image-viewer] {
66
+ position: fixed;
67
+ inset: 0;
68
+ z-index: 9999;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ padding: 64px;
73
+ background: rgba(0, 0, 0, 0.78);
74
+ cursor: zoom-out;
75
+ animation: odn-image-viewer-fade 0.15s ease-out;
76
+ }
77
+
78
+ @keyframes odn-image-viewer-fade {
79
+ from {
80
+ opacity: 0;
81
+ }
82
+ to {
83
+ opacity: 1;
84
+ }
85
+ }
86
+ [data-odn-image-viewer-img] {
87
+ max-width: 90vw;
88
+ max-height: 90vh;
89
+ object-fit: contain;
90
+ border-radius: 8px;
91
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
92
+ cursor: default;
93
+ }
94
+
95
+ [data-odn-image-viewer-btn] {
96
+ position: absolute;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ border: none;
101
+ border-radius: 50%;
102
+ background: rgba(255, 255, 255, 0.12);
103
+ color: rgba(255, 255, 255, 0.95);
104
+ cursor: pointer;
105
+ backdrop-filter: blur(8px);
106
+ transition: background 0.15s, transform 0.1s;
107
+ }
108
+ [data-odn-image-viewer-btn]:hover {
109
+ background: rgba(255, 255, 255, 0.22);
110
+ }
111
+ [data-odn-image-viewer-btn]:active {
112
+ transform: scale(0.94);
113
+ }
114
+ [data-odn-image-viewer-btn]:focus-visible {
115
+ outline: 2px solid color-mix(in srgb, var(--odn-color-primary) 60%, transparent);
116
+ outline-offset: 2px;
117
+ }
118
+
119
+ [data-odn-image-viewer-btn=close] {
120
+ top: 20px;
121
+ right: 24px;
122
+ width: 40px;
123
+ height: 40px;
124
+ }
125
+
126
+ [data-odn-image-viewer-btn=prev],
127
+ [data-odn-image-viewer-btn=next] {
128
+ top: 50%;
129
+ width: 48px;
130
+ height: 48px;
131
+ transform: translateY(-50%);
132
+ }
133
+ [data-odn-image-viewer-btn=prev]:active,
134
+ [data-odn-image-viewer-btn=next]:active {
135
+ transform: translateY(-50%) scale(0.94);
136
+ }
137
+
138
+ [data-odn-image-viewer-btn=prev] {
139
+ left: 24px;
140
+ }
141
+
142
+ [data-odn-image-viewer-btn=next] {
143
+ right: 24px;
144
+ }
145
+
146
+ [data-odn-image-viewer-counter] {
147
+ position: absolute;
148
+ bottom: 24px;
149
+ left: 50%;
150
+ padding: 6px 14px;
151
+ border-radius: 999px;
152
+ background: rgba(0, 0, 0, 0.4);
153
+ color: rgba(255, 255, 255, 0.85);
154
+ font-size: 13px;
155
+ font-variant-numeric: tabular-nums;
156
+ backdrop-filter: blur(8px);
157
+ transform: translateX(-50%);
60
158
  }
@@ -6,16 +6,15 @@ export interface UserBubbleProps {
6
6
  message: ChatMessage;
7
7
  onFileClick?: (attachment: Attachment) => void;
8
8
  /**
9
- * 点击图片附件回调,业务方按此对接自己的 lightbox / 大图查看器。
9
+ * 图片点击**通知**(埋点 / 统计用)。大图预览由 `MessageImage` 内置查看器
10
+ * 负责——用户点图默认即可看大图,**业务方无需对接任何 lightbox**。此回调只
11
+ * 知会「第 i 张被点了」,不影响内置预览;过期图不触发。
10
12
  * @param index 被点击的图片在「当前消息内已分流的图片子集」中的位置(0-based)
11
- * @param images 当前消息内所有 image attachments(已转成 `MessageImageItem`
12
- * 的形态,包含 `url` 与 `alt`),方便直接 push 到 lightbox
13
- * 实现上下张切换
14
- * 不传 → 图片不可点击预览。
13
+ * @param images 当前消息内所有 image attachments(已转成 `MessageImageItem` 形态)
15
14
  */
16
- onImagePreview?: (index: number, images: MessageImageItem[]) => void;
15
+ onImageClick?: (index: number, images: MessageImageItem[]) => void;
17
16
  }
18
- export declare function UserBubble({ message, onFileClick, onImagePreview }: UserBubbleProps): import("react").JSX.Element;
17
+ export declare function UserBubble({ message, onFileClick, onImageClick }: UserBubbleProps): import("react").JSX.Element;
19
18
  export declare namespace UserBubble {
20
19
  var displayName: string;
21
20
  }
@@ -6,7 +6,7 @@ export function UserBubble(_ref) {
6
6
  var _message$content, _message$attachments;
7
7
  var message = _ref.message,
8
8
  onFileClick = _ref.onFileClick,
9
- onImagePreview = _ref.onImagePreview;
9
+ onImageClick = _ref.onImageClick;
10
10
  var segments = message.segments;
11
11
  var hasText = segments !== null && segments !== void 0 && segments.length ? segmentsHaveVisibleContent(segments) : !!((_message$content = message.content) !== null && _message$content !== void 0 && _message$content.trim());
12
12
  var textBody = segments !== null && segments !== void 0 && segments.length ? renderSegmentsContent(segments) : message.content;
@@ -20,7 +20,10 @@ export function UserBubble(_ref) {
20
20
  }).map(function (a) {
21
21
  return {
22
22
  url: a.url,
23
- alt: a.name
23
+ alt: a.name,
24
+ expired: a.status === 'expired',
25
+ width: a.width,
26
+ height: a.height
24
27
  };
25
28
  });
26
29
  var files = all.filter(function (a) {
@@ -34,8 +37,8 @@ export function UserBubble(_ref) {
34
37
  "data-odn-user-bubble-images": true
35
38
  }, /*#__PURE__*/React.createElement(MessageImage, {
36
39
  images: images,
37
- onPreview: onImagePreview ? function (i) {
38
- return onImagePreview(i, images);
40
+ onImageClick: onImageClick ? function (i) {
41
+ return onImageClick(i, images);
39
42
  } : undefined
40
43
  })), hasText && textBody != null && /*#__PURE__*/React.createElement("div", {
41
44
  "data-odn-user-bubble-text": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-next",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "One Design Next from TAD@tencent.com",
5
5
  "packageManager": "pnpm@11.8.0",
6
6
  "module": "dist/index.js",