mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
feat: centralize OneTalk DOM interactions
This commit is contained in:
@@ -24,7 +24,7 @@ class ConnectionStatusTooltip {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
入口由 `src/onetalk/main-page/page-script-entry.ts` 安装;复制控件实现在 `src/onetalk/main-page/conversation-id-copy.ts`,ID 读取复用 `readCurrentConversationId(pageWindow)`。动作提示实现在 `src/onetalk/main-page/action-status-tooltip.ts`,并将 facade 安装到 `window.__tradeMessageCenterOneTalk.tooltip`。
|
入口由 `src/onetalk/main-page/page-script-entry.ts` 安装;复制控件和动作提示的 DOM 实现分别位于 `src/onetalk/main-page/dom/conversation-id-copy.ts` 与 `src/onetalk/main-page/dom/action-status-tooltip.ts`。旧根路径仅保留兼容 re-export,不能再次拥有 DOM API。复制控件的 ID 读取复用 `readCurrentConversationId(pageWindow)`;动作提示将 facade 安装到 `window.__tradeMessageCenterOneTalk.tooltip`。
|
||||||
|
|
||||||
## 3. Contracts
|
## 3. Contracts
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ class ConnectionStatusTooltip {
|
|||||||
- 按钮直接追加到标题节点,使用 `inline-flex` 和 `justify-content: center` 保持同行及文字水平居中;文字为“复制会话 Id”,字体显式为 `12px`,不能依赖会话标题的继承字体。
|
- 按钮直接追加到标题节点,使用 `inline-flex` 和 `justify-content: center` 保持同行及文字水平居中;文字为“复制会话 Id”,字体显式为 `12px`,不能依赖会话标题的继承字体。
|
||||||
- 点击处理在用户手势中调用 `navigator.clipboard.writeText(conversationId)`;成功显示“已复制”,不可用或 reject 显示“复制失败”,随后恢复按钮文案。不会以 `execCommand`、隐形文本框或其它方式降级复制。
|
- 点击处理在用户手势中调用 `navigator.clipboard.writeText(conversationId)`;成功显示“已复制”,不可用或 reject 显示“复制失败”,随后恢复按钮文案。不会以 `execCommand`、隐形文本框或其它方式降级复制。
|
||||||
- 页面 DOM 尚未就绪或 SPA 重绘时可由 `MutationObserver` 重新尝试挂载;同一页面只能存在一个该属性的按钮。
|
- 页面 DOM 尚未就绪或 SPA 重绘时可由 `MutationObserver` 重新尝试挂载;同一页面只能存在一个该属性的按钮。
|
||||||
|
- 非观测页面 DOM 查询、控件创建和更新仅归 `main-page/dom/` 所有;调用方只能使用其语义 API,不能重新取得同一 DOM 权限。
|
||||||
- 动作提示以固定定位、`pointer-events: none` 的 extension-owned `data-tmc-action-status-*` DOM 节点呈现,不依赖 OneTalk 的业务 DOM,也不改变宿主布局。多个活动条目按照首次 `start` 的顺序纵向显示。
|
- 动作提示以固定定位、`pointer-events: none` 的 extension-owned `data-tmc-action-status-*` DOM 节点呈现,不依赖 OneTalk 的业务 DOM,也不改变宿主布局。多个活动条目按照首次 `start` 的顺序纵向显示。
|
||||||
- `start` 对同一 `id` 幂等,不覆盖已有行;`update` 与 `close` 只作用于已存在的 `id`,找不到时返回 `false`,不得隐式创建行。颜色只接受 `neutral`、`info`、`warning`、`error`,任意 CSS 色值必须显式失败。
|
- `start` 对同一 `id` 幂等,不覆盖已有行;`update` 与 `close` 只作用于已存在的 `id`,找不到时返回 `false`,不得隐式创建行。颜色只接受 `neutral`、`info`、`warning`、`error`,任意 CSS 色值必须显式失败。
|
||||||
- 状态 facade 必须保留身份,并在再次安装、`start` 或 `update` 时重新挂载所有仍活动的行;宿主移除了 container 或某一行都不能改变状态顺序或丢失活动项。
|
- 状态 facade 必须保留身份,并在再次安装、`start` 或 `update` 时重新挂载所有仍活动的行;宿主移除了 container 或某一行都不能改变状态顺序或丢失活动项。
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{"file":".trellis/spec/project/source-file-conventions.md","reason":"检查新增和移动 TypeScript 文件的职责注释、bottom-up 和 main-last。"}
|
||||||
|
{"file":".trellis/spec/chrome-extension/frontend/quality-guidelines.md","reason":"检查包级依赖位置、Node test runner、typecheck 和 Vite 构建。"}
|
||||||
|
{"file":".trellis/spec/chrome-extension/frontend/onetalk/page-controls.md","reason":"验证重构后复制控件和提示浮层的回归边界。"}
|
||||||
|
{"file":".trellis/spec/chrome-extension/frontend/onetalk/page-bridge.md","reason":"验证页面身份、bridge 和自包含 MAIN 入口不发生行为漂移。"}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 非观测 DOM 边界设计
|
||||||
|
|
||||||
|
## Boundary
|
||||||
|
|
||||||
|
`apps/chrome-extension/src/onetalk/main-page/dom/` 是本任务中 OneTalk MAIN-world 非观测 DOM 访问的唯一所有者。它是 OneTalk 专属的页面适配边界,不提升到 `src/lib/`,也不与 Popup 共用。
|
||||||
|
|
||||||
|
```text
|
||||||
|
main-page/
|
||||||
|
├── dom/
|
||||||
|
│ ├── selection.ts # selected 会话和标题节点的 DOM 读取
|
||||||
|
│ ├── selection-events.ts # document click 订阅
|
||||||
|
│ ├── react-file-uploader.ts # file input 到 React uploader 的窄适配
|
||||||
|
│ ├── action-status-tooltip.ts # extension-owned 状态浮层
|
||||||
|
│ └── conversation-id-copy.ts # extension-owned 复制按钮
|
||||||
|
├── page-context.ts # URL/页面运行时身份与 selection 领域投影
|
||||||
|
├── image-send.ts # 媒体发送状态机和结果语义
|
||||||
|
└── page-script-entry.ts # 组合入口
|
||||||
|
```
|
||||||
|
|
||||||
|
`buyer-fact-observer/`、`contact-observer/` 不移动、不改 import、不改测试;它们仍自行拥有被动观察和投影。
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
| 现有责任 | 新 DOM 所有者 | 保留的业务所有者 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| selected `[data-cid]`、标题定位 | `dom/selection.ts` | `page-context.ts` 的 `none/single/multiple/unavailable` 语义 |
|
||||||
|
| document click 刷新 identity | `dom/selection-events.ts` | `page-bridge/main.ts` 的 hello/retry/bridge 生命周期 |
|
||||||
|
| 找到 file input 并上溯 React uploader | `dom/react-file-uploader.ts` | `image-send.ts` 的请求截止、上传拦截和 delivery result |
|
||||||
|
| tooltip DOM 创建、重挂载和销毁 | `dom/action-status-tooltip.ts` | connection/history 组件对 tooltip facade 的调用 |
|
||||||
|
| 复制按钮 DOM 创建、更新和移除 | `dom/conversation-id-copy.ts` | 当前会话 ID 可信来源及 Clipboard 成功/失败语义 |
|
||||||
|
|
||||||
|
移动只改变 DOM 适配层归属。公开函数签名、stable error/reason、桥接 message、SDK 发送和页面可见行为必须不变。
|
||||||
|
|
||||||
|
## Testing Library decision
|
||||||
|
|
||||||
|
`@testing-library/dom` 与 `@testing-library/user-event` 是测试依赖,而不是 MAIN-world 自动化引擎。现有代码没有主动操作 OneTalk UI 的业务命令;把 `user-event` 打进生产包既不能产生 trusted event,也会增加无消费者的 bundle 依赖。
|
||||||
|
|
||||||
|
测试层新增由 `jsdom` 创建和销毁的真实 DOM fixture,以 Testing Library 的 role/name 查询和 `userEvent.setup().click` 覆盖 extension-owned 复制按钮。项目的 Node `>=22.22.2` 基线满足当前 jsdom 的 Node 要求;每个测试必须显式传入 fixture document,并在结束时恢复替换过的全局对象。现有手写 fixture 仍可保留给页面结构异常、MutationObserver 和边界错误的单元测试。未来新增真实页面动作时,另行定义动作契约、失败语义和运行时策略,不能把测试工具直接暴露给 Bridge command。
|
||||||
|
|
||||||
|
## Compatibility and rollback
|
||||||
|
|
||||||
|
- 入口仍从 `page-script-entry.ts` 安装同名控件,MAIN/ISOLATED 入口和 Manifest 不变。
|
||||||
|
- 所有新导入必须保持 MAIN entry 的 IIFE + `inlineDynamicImports` 构建约束;Testing Library 仅被 test 文件导入,不能进入该入口依赖图。
|
||||||
|
- 这是可逆目录/依赖重构:若语义回归,恢复原 import 路径和 DOM adapter 文件即可;不得通过并行旧/新实现作运行时 fallback。
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{"file":".trellis/spec/project/module-organization.md","reason":"确定 OneTalk 专属 DOM 外部适配器的最近共同所有者和职责边界。"}
|
||||||
|
{"file":".trellis/spec/project/module-ownership.md","reason":"移动类型与导出时保持唯一所有者和 canonical import path。"}
|
||||||
|
{"file":".trellis/spec/chrome-extension/frontend/architecture.md","reason":"遵守渠道目录、MAIN world 和 Popup 独立入口边界。"}
|
||||||
|
{"file":".trellis/spec/chrome-extension/frontend/onetalk/page-controls.md","reason":"保留会话复制和状态提示的可见行为与错误契约。"}
|
||||||
|
{"file":".trellis/spec/chrome-extension/frontend/onetalk/page-bridge.md","reason":"保留 bridge identity 刷新和 MAIN IIFE 契约。"}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 实施计划
|
||||||
|
|
||||||
|
## 1. 建立 DOM 适配目录
|
||||||
|
|
||||||
|
- 新建 `src/onetalk/main-page/dom/`,按设计中的五类 DOM 责任迁移实现。
|
||||||
|
- 在移动前,对每个被改动的导出符号执行 GitNexus upstream impact;风险为 HIGH 或 CRITICAL 时先报告并复核范围。
|
||||||
|
- 保持函数名与返回值稳定;只在 `page-context.ts` 和 `image-send.ts` 保留领域编排,改为调用 DOM adapter。
|
||||||
|
|
||||||
|
## 2. 更新组合与测试导入
|
||||||
|
|
||||||
|
- 更新 `page-script-entry.ts`、`page-bridge/main.ts`、`page-context.ts`、`image-send.ts` 的内部导入,确保没有残留的非 observer 直接 `document.querySelector*`、元素创建/挂载或 document click 订阅。
|
||||||
|
- 更新 `onetalk-action-status-tooltip`、`onetalk-conversation-id-copy`、`onetalk-page-context`、`onetalk-send-page`、`onetalk-page-bridge`、`onetalk-image-send` 和 `onetalk-file-send` 的必要导入与 fixture。
|
||||||
|
- 不修改 `main-page/buyer-fact-observer/`、`main-page/contact-observer/` 或其测试。
|
||||||
|
|
||||||
|
## 3. 引入并使用 Testing Library
|
||||||
|
|
||||||
|
- 在 `apps/chrome-extension/package.json` 的 `devDependencies` 添加 `@testing-library/dom`、`@testing-library/user-event` 和 `jsdom`。
|
||||||
|
- 新增最小测试 helper,负责创建/销毁真实 document 和恢复全局状态;不替换所有现有 fixture。
|
||||||
|
- 让复制控件测试通过 Testing Library 按 role/name 定位按钮,并用 `userEvent.click` 验证 Clipboard 调用与成功/失败反馈。
|
||||||
|
|
||||||
|
## 4. 验证与审查
|
||||||
|
|
||||||
|
- 运行受影响的 focused tests,单次 Node test 命令不超过 60 秒。
|
||||||
|
- 运行 `pnpm --filter @trade-message-center/chrome-extension typecheck`、扩展 build 和完整扩展 test。
|
||||||
|
- 搜索非 observer 的 MAIN-page source,确认 DOM API 只在 `main-page/dom/` 和明确保留的 Popup 中出现。
|
||||||
|
- 执行 GitNexus `detect_changes()`,确认只影响 DOM adapter、调用方、测试和依赖清单;审查没有改变 bridge、发送或观察语义。
|
||||||
|
|
||||||
|
## Risk points
|
||||||
|
|
||||||
|
- `readSelectedConversationIds` 供 page bridge、买家事实和联系人资料路径调用;只抽取 DOM 读取,不得改其零/多选结果。
|
||||||
|
- 复制控件自身使用 MutationObserver 来适配 SPA 重绘,但其文件不是被冻结的 observer 目录,仍随控件迁移。
|
||||||
|
- React file uploader 查找依赖宿主内部 fiber,迁移必须保持 null/多候选和发送失败语义。
|
||||||
|
- Node 内置 test runner 没有浏览器 DOM;新测试 runtime 必须局部化,不能污染其它测试的全局对象。
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 集中非观测 DOM 交互
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
把 OneTalk 页面中非观测用途的直接 DOM 访问收敛到明确的 DOM 边界,并使用 `@testing-library/dom` 与 `@testing-library/user-event` 对该边界的用户交互进行验证。这样新增页面动作不再复制手写 fixture 事件序列,同时不改变现有发送、身份判断或桥接行为。
|
||||||
|
|
||||||
|
## Confirmed facts
|
||||||
|
|
||||||
|
- OneTalk 页面脚本在 `MAIN` world、`document_start` 注入,并且必须构建为自包含 IIFE。
|
||||||
|
- 当前没有 `element.click()`、`dispatchEvent()`、`InputEvent` 或 `user-event`;DOM 使用主要是读取页面状态,或注入扩展自有提示和复制控件。
|
||||||
|
- 非 observer 的 OneTalk DOM 触点位于:`action-status-tooltip.ts`、`conversation-id-copy.ts`、`page-context.ts`、`image-send.ts` 和 `page-bridge/main.ts`。
|
||||||
|
- `main-page/buyer-fact-observer/` 与 `main-page/contact-observer/`(包括其中的 `MutationObserver` 和 DOM 读取)不在本任务范围内,保持原路径与行为不变。
|
||||||
|
- 扩展尚未引入 Testing Library;运行时只有浏览器原生 DOM,测试使用 Node 内置 test runner 和手写页面 fixture。
|
||||||
|
- Popup 是扩展自有页面,不纳入本任务;`popup/popup.ts` 保持原路径和行为。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- R1:把本任务范围内 OneTalk 页面 DOM 查询、页面自有控件创建/更新,以及未来动作的公共入口整理到一个专属目录;业务编排、协议解码和页面桥不因目录整理而获得新的 DOM 权限。
|
||||||
|
- R2:引入 `@testing-library/dom` 与 `@testing-library/user-event` 作为 DOM 边界的测试工具,并用真实 DOM fixture 验证受控页面按钮的定位和点击。它们不进入生产 MAIN-world bundle,也不被误当成能生成 trusted event 的运行时自动化方案。
|
||||||
|
- R3:保留当前的 OneTalk SDK 发送和媒体上传策略;本任务不把发送流程改成 DOM 点击,也不引入 `chrome.debugger`、CDP 或任意 selector/任意脚本的远程执行能力。
|
||||||
|
- R4:维持 `channelAccountId + conversationId` 的精确页面身份判断、MAIN/ISOLATED/SW 桥接契约,以及现有页面控件的可见行为。
|
||||||
|
- R5:新增依赖作为扩展测试依赖,并提供 Node test runner 可运行的浏览器 DOM 环境;生产 MAIN-world IIFE 不得引入测试运行时、暴露原始 payload、凭据或开放执行面。
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] AC1:所有纳入范围的 OneTalk 页面直接 DOM 访问均由新目录拥有;调用方只通过其语义 API 使用页面 DOM。
|
||||||
|
- [x] AC2:`buyer-fact-observer/` 与 `contact-observer/` 的生产代码保持未修改。
|
||||||
|
- [x] AC3:至少一项受控页面按钮交互通过 `@testing-library/dom` 和 `@testing-library/user-event` 在真实 DOM fixture 中验证;生产入口不打包这些测试依赖。
|
||||||
|
- [x] AC4:会话 ID 复制控件、动作状态提示、当前会话识别、图片/文件上传器定位、桥接 identity 刷新与重构前行为一致。
|
||||||
|
- [x] AC5:相关 focused tests、`pnpm --filter @trade-message-center/chrome-extension typecheck` 和扩展构建通过;MAIN-world 入口没有产生 code-splitting 或顶层 import 问题。
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- 被动 DOM 观测:`main-page/buyer-fact-observer/`、`main-page/contact-observer/` 及其 `MutationObserver`。
|
||||||
|
- OneTalk 页面实际用户动作的新增业务功能、发送语义、协议、授权、Bridge 合约和服务端改动。
|
||||||
|
- Playwright、`chrome.debugger`、CDP 或 Native Messaging 自动化。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "centralize-dom-interactions",
|
||||||
|
"name": "centralize-dom-interactions",
|
||||||
|
"title": "集中非观测 DOM 交互",
|
||||||
|
"description": "将 Chrome 扩展中 observer 目录以外的页面 DOM 访问收敛到明确边界,并采用 Testing Library。",
|
||||||
|
"status": "in_progress",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "ybf",
|
||||||
|
"assignee": "ybf",
|
||||||
|
"createdAt": "2026-09-14",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": "09-14-centralize-dom-interactions",
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -17,7 +17,10 @@
|
|||||||
"@trade-message-center/onetalk-contract": "workspace:*"
|
"@trade-message-center/onetalk-contract": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@testing-library/user-event": "^14.6.7",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
|
"jsdom": "^30.0.1",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^6.0.7"
|
"vite": "^6.0.7"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,192 +1,7 @@
|
|||||||
// 管理 OneTalk 页面进行中动作提示
|
// 兼容现有控件消费者;DOM 实现在专属适配目录中。
|
||||||
|
export {
|
||||||
import { isObjectRecord } from "../../lib/guards.ts";
|
installOneTalkActionStatusTooltip,
|
||||||
|
readOneTalkActionStatusTooltip,
|
||||||
export type TooltipColor = "neutral" | "info" | "warning" | "error";
|
type OneTalkActionStatusTooltip,
|
||||||
|
type TooltipColor,
|
||||||
export type OneTalkActionStatusTooltip = {
|
} from "./dom/action-status-tooltip.ts";
|
||||||
start: (id: string, text: string, color?: TooltipColor) => void;
|
|
||||||
update: (id: string, text: string, color?: TooltipColor) => boolean;
|
|
||||||
close: (id: string) => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OneTalkActionStatusTooltipWindow = Pick<Window, "document"> & {
|
|
||||||
__tradeMessageCenterOneTalk?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ActionStatus = {
|
|
||||||
color: TooltipColor;
|
|
||||||
row: HTMLDivElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
const STATUS_CONTAINER_ATTRIBUTE = "data-tmc-action-status-tooltip";
|
|
||||||
const STATUS_ROW_ATTRIBUTE = "data-tmc-action-status-row";
|
|
||||||
const STATUS_COLOR_ATTRIBUTE = "data-tmc-action-status-color";
|
|
||||||
const DEFAULT_TOOLTIP_COLOR: TooltipColor = "neutral";
|
|
||||||
const tooltipRefreshes = new WeakMap<OneTalkActionStatusTooltipWindow, () => void>();
|
|
||||||
|
|
||||||
const TOOLTIP_COLORS: Readonly<
|
|
||||||
Record<TooltipColor, { background: string; border: string; text: string }>
|
|
||||||
> = {
|
|
||||||
neutral: { background: "#f5f5f5", border: "#d9d9d9", text: "#595959" },
|
|
||||||
info: { background: "#e6f4ff", border: "#91caff", text: "#1677ff" },
|
|
||||||
warning: { background: "#fffbe6", border: "#ffe58f", text: "#d48806" },
|
|
||||||
error: { background: "#fff2f0", border: "#ffccc7", text: "#cf1322" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const isTooltipColor = (value: unknown): value is TooltipColor => {
|
|
||||||
return typeof value === "string" && Object.hasOwn(TOOLTIP_COLORS, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const requireTooltipColor = (value: unknown): TooltipColor => {
|
|
||||||
if (isTooltipColor(value)) return value;
|
|
||||||
throw new TypeError("onetalk_action_status_tooltip_color_invalid");
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusRowStyles = (color: TooltipColor): string => {
|
|
||||||
const theme = TOOLTIP_COLORS[color];
|
|
||||||
return [
|
|
||||||
"box-sizing: border-box",
|
|
||||||
"width: 100%",
|
|
||||||
"padding: 8px 12px",
|
|
||||||
"border: 1px solid " + theme.border,
|
|
||||||
"border-radius: 6px",
|
|
||||||
"background: " + theme.background,
|
|
||||||
"color: " + theme.text,
|
|
||||||
"font-size: 13px",
|
|
||||||
"line-height: 20px",
|
|
||||||
"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
||||||
"box-shadow: 0 2px 8px rgb(0 0 0 / 12%)",
|
|
||||||
].join(";");
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateStatusRow = (status: ActionStatus, text: string, color: TooltipColor): void => {
|
|
||||||
status.color = color;
|
|
||||||
status.row.textContent = text;
|
|
||||||
status.row.setAttribute(STATUS_COLOR_ATTRIBUTE, color);
|
|
||||||
status.row.style.cssText = statusRowStyles(color);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createStatusRow = (
|
|
||||||
document: Document,
|
|
||||||
id: string,
|
|
||||||
text: string,
|
|
||||||
color: TooltipColor,
|
|
||||||
): ActionStatus => {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.setAttribute(STATUS_ROW_ATTRIBUTE, "");
|
|
||||||
row.dataset.tmcActionStatusId = id;
|
|
||||||
const status = { color, row };
|
|
||||||
updateStatusRow(status, text, color);
|
|
||||||
return status;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createStatusContainer = (document: Document): HTMLDivElement => {
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.setAttribute(STATUS_CONTAINER_ATTRIBUTE, "");
|
|
||||||
container.setAttribute("aria-live", "polite");
|
|
||||||
container.setAttribute("role", "status");
|
|
||||||
container.style.cssText = [
|
|
||||||
"position: fixed",
|
|
||||||
"top: 0",
|
|
||||||
"left: 50%",
|
|
||||||
"transform: translateX(-50%)",
|
|
||||||
"z-index: 2147483647",
|
|
||||||
"display: flex",
|
|
||||||
"flex-direction: column",
|
|
||||||
"gap: 8px",
|
|
||||||
"width: min(600px, calc(100vw - 96px))",
|
|
||||||
"pointer-events: none",
|
|
||||||
].join(";");
|
|
||||||
return container;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findStatusContainer = (document: Document): HTMLDivElement | null => {
|
|
||||||
return document.querySelector<HTMLDivElement>(`div[${STATUS_CONTAINER_ATTRIBUTE}]`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureStatusContainer = (
|
|
||||||
document: Document,
|
|
||||||
statuses: ReadonlyMap<string, ActionStatus>,
|
|
||||||
): HTMLDivElement => {
|
|
||||||
const existing = findStatusContainer(document);
|
|
||||||
const container = existing ?? createStatusContainer(document);
|
|
||||||
if (!existing) {
|
|
||||||
if (!document.body)
|
|
||||||
throw new Error("onetalk_action_status_tooltip_document_body_unavailable");
|
|
||||||
document.body.append(container);
|
|
||||||
}
|
|
||||||
for (const status of statuses.values()) container.append(status.row);
|
|
||||||
return container;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isTooltipFacade = (value: unknown): value is OneTalkActionStatusTooltip => {
|
|
||||||
return (
|
|
||||||
isObjectRecord(value) &&
|
|
||||||
typeof value.start === "function" &&
|
|
||||||
typeof value.update === "function" &&
|
|
||||||
typeof value.close === "function"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createTooltipFacade = (
|
|
||||||
pageWindow: OneTalkActionStatusTooltipWindow,
|
|
||||||
): OneTalkActionStatusTooltip => {
|
|
||||||
const statuses = new Map<string, ActionStatus>();
|
|
||||||
const refresh = (): void => {
|
|
||||||
if (statuses.size > 0) ensureStatusContainer(pageWindow.document, statuses);
|
|
||||||
};
|
|
||||||
tooltipRefreshes.set(pageWindow, refresh);
|
|
||||||
const start = (id: string, text: string, color: TooltipColor = DEFAULT_TOOLTIP_COLOR): void => {
|
|
||||||
if (statuses.has(id)) {
|
|
||||||
refresh();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const resolvedColor = requireTooltipColor(color);
|
|
||||||
const status = createStatusRow(pageWindow.document, id, text, resolvedColor);
|
|
||||||
statuses.set(id, status);
|
|
||||||
const container = ensureStatusContainer(pageWindow.document, statuses);
|
|
||||||
if (status.row.parentElement !== container) container.append(status.row);
|
|
||||||
};
|
|
||||||
const update = (id: string, text: string, color?: TooltipColor): boolean => {
|
|
||||||
const status = statuses.get(id);
|
|
||||||
if (!status) return false;
|
|
||||||
const resolvedColor = color === undefined ? status.color : requireTooltipColor(color);
|
|
||||||
updateStatusRow(status, text, resolvedColor);
|
|
||||||
ensureStatusContainer(pageWindow.document, statuses);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
const close = (id: string): boolean => {
|
|
||||||
const status = statuses.get(id);
|
|
||||||
if (!status) return false;
|
|
||||||
statuses.delete(id);
|
|
||||||
status.row.remove();
|
|
||||||
if (statuses.size === 0) findStatusContainer(pageWindow.document)?.remove();
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
return { close, start, update };
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 读取已安装的页面动作提示 facade。 */
|
|
||||||
export const readOneTalkActionStatusTooltip = (
|
|
||||||
pageWindow: OneTalkActionStatusTooltipWindow,
|
|
||||||
): OneTalkActionStatusTooltip | null => {
|
|
||||||
const namespace = pageWindow.__tradeMessageCenterOneTalk;
|
|
||||||
return isObjectRecord(namespace) && isTooltipFacade(namespace.tooltip)
|
|
||||||
? namespace.tooltip
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 将进行中动作提示注册到 OneTalk 页面命名空间。 */
|
|
||||||
export const installOneTalkActionStatusTooltip = (
|
|
||||||
pageWindow: OneTalkActionStatusTooltipWindow,
|
|
||||||
): void => {
|
|
||||||
const existing = pageWindow.__tradeMessageCenterOneTalk;
|
|
||||||
const namespace: Record<string, unknown> = isObjectRecord(existing) ? existing : {};
|
|
||||||
if (isTooltipFacade(namespace.tooltip)) {
|
|
||||||
tooltipRefreshes.get(pageWindow)?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
namespace.tooltip = createTooltipFacade(pageWindow);
|
|
||||||
pageWindow.__tradeMessageCenterOneTalk = namespace;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,147 +1,2 @@
|
|||||||
// 在当前会话标题旁提供复制会话 ID 控件
|
// 兼容现有控件消费者;DOM 实现在专属适配目录中。
|
||||||
|
export { installOneTalkConversationIdCopyControl } from "./dom/conversation-id-copy.ts";
|
||||||
import { readCurrentConversationId } from "./page-context.ts";
|
|
||||||
import type { OneTalkPageWindow } from "./model.ts";
|
|
||||||
|
|
||||||
type OneTalkConversationIdCopyWindow = OneTalkPageWindow &
|
|
||||||
Pick<Window, "clearTimeout" | "document" | "navigator" | "setTimeout"> & {
|
|
||||||
MutationObserver?: typeof MutationObserver;
|
|
||||||
};
|
|
||||||
|
|
||||||
const COPY_BUTTON_ATTRIBUTE = "data-tmc-conversation-id-copy";
|
|
||||||
const HEADER_MAX_BOTTOM_PX = 72;
|
|
||||||
const BUTTON_LABEL = "复制会话 Id";
|
|
||||||
const COPIED_LABEL = "已复制";
|
|
||||||
const COPY_FAILED_LABEL = "复制失败";
|
|
||||||
const BUTTON_RESTORE_DELAY_MS = 1_500;
|
|
||||||
|
|
||||||
const selectedConversationElement = (document: Document): Element | null => {
|
|
||||||
const selected = document.querySelectorAll(".contact-item-container.selected[data-cid]");
|
|
||||||
return selected.length === 1 ? selected[0] : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const headerTitleElement = (document: Document): Element | null => {
|
|
||||||
const selected = selectedConversationElement(document);
|
|
||||||
if (!selected) return null;
|
|
||||||
const selectedRect = selected.getBoundingClientRect();
|
|
||||||
const maxLeft = document.defaultView?.innerWidth ?? Number.POSITIVE_INFINITY;
|
|
||||||
const candidates = Array.from(document.querySelectorAll("body *")).filter((element) => {
|
|
||||||
if (element.children.length > 0 || !element.textContent?.trim()) return false;
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
return (
|
|
||||||
rect.width > 0 &&
|
|
||||||
rect.height > 0 &&
|
|
||||||
rect.top >= 0 &&
|
|
||||||
rect.bottom <= HEADER_MAX_BOTTOM_PX &&
|
|
||||||
rect.left >= selectedRect.right &&
|
|
||||||
rect.left < maxLeft * 0.75
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
candidates.sort(
|
|
||||||
(left, right) =>
|
|
||||||
left.getBoundingClientRect().left - right.getBoundingClientRect().left ||
|
|
||||||
left.getBoundingClientRect().top - right.getBoundingClientRect().top,
|
|
||||||
)[0] ?? null
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateButtonLabel = (
|
|
||||||
pageWindow: OneTalkConversationIdCopyWindow,
|
|
||||||
button: HTMLButtonElement,
|
|
||||||
label: string,
|
|
||||||
): void => {
|
|
||||||
button.textContent = label;
|
|
||||||
button.setAttribute("aria-label", `${label}会话 Id`);
|
|
||||||
pageWindow.clearTimeout(Number(button.dataset.restoreTimer));
|
|
||||||
button.dataset.restoreTimer = String(
|
|
||||||
pageWindow.setTimeout(() => {
|
|
||||||
button.textContent = BUTTON_LABEL;
|
|
||||||
button.setAttribute("aria-label", BUTTON_LABEL);
|
|
||||||
}, BUTTON_RESTORE_DELAY_MS),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createCopyButton = (
|
|
||||||
pageWindow: OneTalkConversationIdCopyWindow,
|
|
||||||
conversationId: string,
|
|
||||||
): HTMLButtonElement => {
|
|
||||||
const button = pageWindow.document.createElement("button");
|
|
||||||
button.type = "button";
|
|
||||||
button.setAttribute(COPY_BUTTON_ATTRIBUTE, "");
|
|
||||||
button.setAttribute("aria-label", BUTTON_LABEL);
|
|
||||||
button.title = BUTTON_LABEL;
|
|
||||||
button.textContent = BUTTON_LABEL;
|
|
||||||
button.dataset.conversationId = conversationId;
|
|
||||||
button.style.cssText = [
|
|
||||||
"margin-left: 8px",
|
|
||||||
"padding: 2px 7px",
|
|
||||||
"display: inline-flex",
|
|
||||||
"align-items: center",
|
|
||||||
"justify-content: center",
|
|
||||||
"border: 1px solid #d8dce5",
|
|
||||||
"border-radius: 4px",
|
|
||||||
"background: #fff",
|
|
||||||
"color: #315efb",
|
|
||||||
"font-size: 12px",
|
|
||||||
"line-height: 18px",
|
|
||||||
"text-align: center",
|
|
||||||
"cursor: pointer",
|
|
||||||
"vertical-align: middle",
|
|
||||||
].join(";");
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
const currentId = button.dataset.conversationId;
|
|
||||||
if (!currentId) return;
|
|
||||||
const clipboard = pageWindow.navigator.clipboard;
|
|
||||||
if (!clipboard) {
|
|
||||||
updateButtonLabel(pageWindow, button, COPY_FAILED_LABEL);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void clipboard
|
|
||||||
.writeText(currentId)
|
|
||||||
.then(() => updateButtonLabel(pageWindow, button, COPIED_LABEL))
|
|
||||||
.catch(() => updateButtonLabel(pageWindow, button, COPY_FAILED_LABEL));
|
|
||||||
});
|
|
||||||
return button;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCopyControl = (pageWindow: OneTalkConversationIdCopyWindow): void => {
|
|
||||||
const existing = pageWindow.document.querySelector<HTMLButtonElement>(
|
|
||||||
`button[${COPY_BUTTON_ATTRIBUTE}]`,
|
|
||||||
);
|
|
||||||
const conversationId = readCurrentConversationId(pageWindow);
|
|
||||||
if (!conversationId) {
|
|
||||||
existing?.remove();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (existing) {
|
|
||||||
existing.dataset.conversationId = conversationId;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const title = headerTitleElement(pageWindow.document);
|
|
||||||
if (!title) return;
|
|
||||||
title.append(createCopyButton(pageWindow, conversationId));
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 将当前会话 ID 的复制控件安装到 OneTalk 页面标题旁。 */
|
|
||||||
export const installOneTalkConversationIdCopyControl = (
|
|
||||||
pageWindow: OneTalkConversationIdCopyWindow,
|
|
||||||
): void => {
|
|
||||||
let refreshScheduled = false;
|
|
||||||
const refresh = (): void => {
|
|
||||||
refreshScheduled = false;
|
|
||||||
updateCopyControl(pageWindow);
|
|
||||||
};
|
|
||||||
const scheduleRefresh = (): void => {
|
|
||||||
if (refreshScheduled) return;
|
|
||||||
refreshScheduled = true;
|
|
||||||
pageWindow.setTimeout(refresh, 0);
|
|
||||||
};
|
|
||||||
const observer = pageWindow.MutationObserver
|
|
||||||
? new pageWindow.MutationObserver(scheduleRefresh)
|
|
||||||
: null;
|
|
||||||
if (observer && pageWindow.document.documentElement) {
|
|
||||||
observer.observe(pageWindow.document.documentElement, { childList: true, subtree: true });
|
|
||||||
}
|
|
||||||
refresh();
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
// 管理 OneTalk 页面进行中动作提示的 DOM 边界。
|
||||||
|
|
||||||
|
import { isObjectRecord } from "../../../lib/guards.ts";
|
||||||
|
|
||||||
|
export type TooltipColor = "neutral" | "info" | "warning" | "error";
|
||||||
|
|
||||||
|
export type OneTalkActionStatusTooltip = {
|
||||||
|
start: (id: string, text: string, color?: TooltipColor) => void;
|
||||||
|
update: (id: string, text: string, color?: TooltipColor) => boolean;
|
||||||
|
close: (id: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OneTalkActionStatusTooltipWindow = Pick<Window, "document"> & {
|
||||||
|
__tradeMessageCenterOneTalk?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActionStatus = {
|
||||||
|
color: TooltipColor;
|
||||||
|
row: HTMLDivElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_CONTAINER_ATTRIBUTE = "data-tmc-action-status-tooltip";
|
||||||
|
const STATUS_ROW_ATTRIBUTE = "data-tmc-action-status-row";
|
||||||
|
const STATUS_COLOR_ATTRIBUTE = "data-tmc-action-status-color";
|
||||||
|
const DEFAULT_TOOLTIP_COLOR: TooltipColor = "neutral";
|
||||||
|
const tooltipRefreshes = new WeakMap<OneTalkActionStatusTooltipWindow, () => void>();
|
||||||
|
|
||||||
|
const TOOLTIP_COLORS: Readonly<
|
||||||
|
Record<TooltipColor, { background: string; border: string; text: string }>
|
||||||
|
> = {
|
||||||
|
neutral: { background: "#f5f5f5", border: "#d9d9d9", text: "#595959" },
|
||||||
|
info: { background: "#e6f4ff", border: "#91caff", text: "#1677ff" },
|
||||||
|
warning: { background: "#fffbe6", border: "#ffe58f", text: "#d48806" },
|
||||||
|
error: { background: "#fff2f0", border: "#ffccc7", text: "#cf1322" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTooltipColor = (value: unknown): value is TooltipColor => {
|
||||||
|
return typeof value === "string" && Object.hasOwn(TOOLTIP_COLORS, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requireTooltipColor = (value: unknown): TooltipColor => {
|
||||||
|
if (isTooltipColor(value)) return value;
|
||||||
|
throw new TypeError("onetalk_action_status_tooltip_color_invalid");
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusRowStyles = (color: TooltipColor): string => {
|
||||||
|
const theme = TOOLTIP_COLORS[color];
|
||||||
|
return [
|
||||||
|
"box-sizing: border-box",
|
||||||
|
"width: 100%",
|
||||||
|
"padding: 8px 12px",
|
||||||
|
"border: 1px solid " + theme.border,
|
||||||
|
"border-radius: 6px",
|
||||||
|
"background: " + theme.background,
|
||||||
|
"color: " + theme.text,
|
||||||
|
"font-size: 13px",
|
||||||
|
"line-height: 20px",
|
||||||
|
"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||||
|
"box-shadow: 0 2px 8px rgb(0 0 0 / 12%)",
|
||||||
|
].join(";");
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStatusRow = (status: ActionStatus, text: string, color: TooltipColor): void => {
|
||||||
|
status.color = color;
|
||||||
|
status.row.textContent = text;
|
||||||
|
status.row.setAttribute(STATUS_COLOR_ATTRIBUTE, color);
|
||||||
|
status.row.style.cssText = statusRowStyles(color);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStatusRow = (
|
||||||
|
document: Document,
|
||||||
|
id: string,
|
||||||
|
text: string,
|
||||||
|
color: TooltipColor,
|
||||||
|
): ActionStatus => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.setAttribute(STATUS_ROW_ATTRIBUTE, "");
|
||||||
|
row.dataset.tmcActionStatusId = id;
|
||||||
|
const status = { color, row };
|
||||||
|
updateStatusRow(status, text, color);
|
||||||
|
return status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStatusContainer = (document: Document): HTMLDivElement => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.setAttribute(STATUS_CONTAINER_ATTRIBUTE, "");
|
||||||
|
container.setAttribute("aria-live", "polite");
|
||||||
|
container.setAttribute("role", "status");
|
||||||
|
container.style.cssText = [
|
||||||
|
"position: fixed",
|
||||||
|
"top: 0",
|
||||||
|
"left: 50%",
|
||||||
|
"transform: translateX(-50%)",
|
||||||
|
"z-index: 2147483647",
|
||||||
|
"display: flex",
|
||||||
|
"flex-direction: column",
|
||||||
|
"gap: 8px",
|
||||||
|
"width: min(600px, calc(100vw - 96px))",
|
||||||
|
"pointer-events: none",
|
||||||
|
].join(";");
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findStatusContainer = (document: Document): HTMLDivElement | null => {
|
||||||
|
return document.querySelector<HTMLDivElement>(`div[${STATUS_CONTAINER_ATTRIBUTE}]`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureStatusContainer = (
|
||||||
|
document: Document,
|
||||||
|
statuses: ReadonlyMap<string, ActionStatus>,
|
||||||
|
): HTMLDivElement => {
|
||||||
|
const existing = findStatusContainer(document);
|
||||||
|
const container = existing ?? createStatusContainer(document);
|
||||||
|
if (!existing) {
|
||||||
|
if (!document.body)
|
||||||
|
throw new Error("onetalk_action_status_tooltip_document_body_unavailable");
|
||||||
|
document.body.append(container);
|
||||||
|
}
|
||||||
|
for (const status of statuses.values()) container.append(status.row);
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTooltipFacade = (value: unknown): value is OneTalkActionStatusTooltip => {
|
||||||
|
return (
|
||||||
|
isObjectRecord(value) &&
|
||||||
|
typeof value.start === "function" &&
|
||||||
|
typeof value.update === "function" &&
|
||||||
|
typeof value.close === "function"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTooltipFacade = (
|
||||||
|
pageWindow: OneTalkActionStatusTooltipWindow,
|
||||||
|
): OneTalkActionStatusTooltip => {
|
||||||
|
const statuses = new Map<string, ActionStatus>();
|
||||||
|
const refresh = (): void => {
|
||||||
|
if (statuses.size > 0) ensureStatusContainer(pageWindow.document, statuses);
|
||||||
|
};
|
||||||
|
tooltipRefreshes.set(pageWindow, refresh);
|
||||||
|
const start = (id: string, text: string, color: TooltipColor = DEFAULT_TOOLTIP_COLOR): void => {
|
||||||
|
if (statuses.has(id)) {
|
||||||
|
refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resolvedColor = requireTooltipColor(color);
|
||||||
|
const status = createStatusRow(pageWindow.document, id, text, resolvedColor);
|
||||||
|
statuses.set(id, status);
|
||||||
|
const container = ensureStatusContainer(pageWindow.document, statuses);
|
||||||
|
if (status.row.parentElement !== container) container.append(status.row);
|
||||||
|
};
|
||||||
|
const update = (id: string, text: string, color?: TooltipColor): boolean => {
|
||||||
|
const status = statuses.get(id);
|
||||||
|
if (!status) return false;
|
||||||
|
const resolvedColor = color === undefined ? status.color : requireTooltipColor(color);
|
||||||
|
updateStatusRow(status, text, resolvedColor);
|
||||||
|
ensureStatusContainer(pageWindow.document, statuses);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const close = (id: string): boolean => {
|
||||||
|
const status = statuses.get(id);
|
||||||
|
if (!status) return false;
|
||||||
|
statuses.delete(id);
|
||||||
|
status.row.remove();
|
||||||
|
if (statuses.size === 0) findStatusContainer(pageWindow.document)?.remove();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
return { close, start, update };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 读取已安装的页面动作提示 facade。 */
|
||||||
|
export const readOneTalkActionStatusTooltip = (
|
||||||
|
pageWindow: OneTalkActionStatusTooltipWindow,
|
||||||
|
): OneTalkActionStatusTooltip | null => {
|
||||||
|
const namespace = pageWindow.__tradeMessageCenterOneTalk;
|
||||||
|
return isObjectRecord(namespace) && isTooltipFacade(namespace.tooltip)
|
||||||
|
? namespace.tooltip
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 将进行中动作提示注册到 OneTalk 页面命名空间。 */
|
||||||
|
export const installOneTalkActionStatusTooltip = (
|
||||||
|
pageWindow: OneTalkActionStatusTooltipWindow,
|
||||||
|
): void => {
|
||||||
|
const existing = pageWindow.__tradeMessageCenterOneTalk;
|
||||||
|
const namespace: Record<string, unknown> = isObjectRecord(existing) ? existing : {};
|
||||||
|
if (isTooltipFacade(namespace.tooltip)) {
|
||||||
|
tooltipRefreshes.get(pageWindow)?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
namespace.tooltip = createTooltipFacade(pageWindow);
|
||||||
|
pageWindow.__tradeMessageCenterOneTalk = namespace;
|
||||||
|
};
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// 安装 OneTalk 会话 ID 复制控件。
|
||||||
|
|
||||||
|
import { readCurrentConversationId } from "../page-context.ts";
|
||||||
|
import type { OneTalkPageWindow } from "../model.ts";
|
||||||
|
|
||||||
|
type OneTalkConversationIdCopyWindow = OneTalkPageWindow &
|
||||||
|
Pick<Window, "clearTimeout" | "document" | "navigator" | "setTimeout"> & {
|
||||||
|
MutationObserver?: typeof MutationObserver;
|
||||||
|
};
|
||||||
|
|
||||||
|
const COPY_BUTTON_ATTRIBUTE = "data-tmc-conversation-id-copy";
|
||||||
|
const HEADER_MAX_BOTTOM_PX = 72;
|
||||||
|
const BUTTON_LABEL = "复制会话 Id";
|
||||||
|
const COPIED_LABEL = "已复制";
|
||||||
|
const COPY_FAILED_LABEL = "复制失败";
|
||||||
|
const BUTTON_RESTORE_DELAY_MS = 1_500;
|
||||||
|
|
||||||
|
const selectedConversationElement = (document: Document): Element | null => {
|
||||||
|
const selected = document.querySelectorAll(".contact-item-container.selected[data-cid]");
|
||||||
|
return selected.length === 1 ? selected[0] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerTitleElement = (document: Document): Element | null => {
|
||||||
|
const selected = selectedConversationElement(document);
|
||||||
|
if (!selected) return null;
|
||||||
|
const selectedRect = selected.getBoundingClientRect();
|
||||||
|
const maxLeft = document.defaultView?.innerWidth ?? Number.POSITIVE_INFINITY;
|
||||||
|
const candidates = Array.from(document.querySelectorAll("body *")).filter((element) => {
|
||||||
|
if (element.children.length > 0 || !element.textContent?.trim()) return false;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.width > 0 &&
|
||||||
|
rect.height > 0 &&
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.bottom <= HEADER_MAX_BOTTOM_PX &&
|
||||||
|
rect.left >= selectedRect.right &&
|
||||||
|
rect.left < maxLeft * 0.75
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
candidates.sort(
|
||||||
|
(left, right) =>
|
||||||
|
left.getBoundingClientRect().left - right.getBoundingClientRect().left ||
|
||||||
|
left.getBoundingClientRect().top - right.getBoundingClientRect().top,
|
||||||
|
)[0] ?? null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateButtonLabel = (
|
||||||
|
pageWindow: OneTalkConversationIdCopyWindow,
|
||||||
|
button: HTMLButtonElement,
|
||||||
|
label: string,
|
||||||
|
): void => {
|
||||||
|
button.textContent = label;
|
||||||
|
button.setAttribute("aria-label", `${label}会话 Id`);
|
||||||
|
pageWindow.clearTimeout(Number(button.dataset.restoreTimer));
|
||||||
|
button.dataset.restoreTimer = String(
|
||||||
|
pageWindow.setTimeout(() => {
|
||||||
|
button.textContent = BUTTON_LABEL;
|
||||||
|
button.setAttribute("aria-label", BUTTON_LABEL);
|
||||||
|
}, BUTTON_RESTORE_DELAY_MS),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCopyButton = (
|
||||||
|
pageWindow: OneTalkConversationIdCopyWindow,
|
||||||
|
conversationId: string,
|
||||||
|
): HTMLButtonElement => {
|
||||||
|
const button = pageWindow.document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.setAttribute(COPY_BUTTON_ATTRIBUTE, "");
|
||||||
|
button.setAttribute("aria-label", BUTTON_LABEL);
|
||||||
|
button.title = BUTTON_LABEL;
|
||||||
|
button.textContent = BUTTON_LABEL;
|
||||||
|
button.dataset.conversationId = conversationId;
|
||||||
|
button.style.cssText = [
|
||||||
|
"margin-left: 8px",
|
||||||
|
"padding: 2px 7px",
|
||||||
|
"display: inline-flex",
|
||||||
|
"align-items: center",
|
||||||
|
"justify-content: center",
|
||||||
|
"border: 1px solid #d8dce5",
|
||||||
|
"border-radius: 4px",
|
||||||
|
"background: #fff",
|
||||||
|
"color: #315efb",
|
||||||
|
"font-size: 12px",
|
||||||
|
"line-height: 18px",
|
||||||
|
"text-align: center",
|
||||||
|
"cursor: pointer",
|
||||||
|
"vertical-align: middle",
|
||||||
|
].join(";");
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const currentId = button.dataset.conversationId;
|
||||||
|
if (!currentId) return;
|
||||||
|
const clipboard = pageWindow.navigator.clipboard;
|
||||||
|
if (!clipboard) {
|
||||||
|
updateButtonLabel(pageWindow, button, COPY_FAILED_LABEL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void clipboard
|
||||||
|
.writeText(currentId)
|
||||||
|
.then(() => updateButtonLabel(pageWindow, button, COPIED_LABEL))
|
||||||
|
.catch(() => updateButtonLabel(pageWindow, button, COPY_FAILED_LABEL));
|
||||||
|
});
|
||||||
|
return button;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCopyControl = (pageWindow: OneTalkConversationIdCopyWindow): void => {
|
||||||
|
const existing = pageWindow.document.querySelector<HTMLButtonElement>(
|
||||||
|
`button[${COPY_BUTTON_ATTRIBUTE}]`,
|
||||||
|
);
|
||||||
|
const conversationId = readCurrentConversationId(pageWindow);
|
||||||
|
if (!conversationId) {
|
||||||
|
existing?.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existing) {
|
||||||
|
existing.dataset.conversationId = conversationId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const title = headerTitleElement(pageWindow.document);
|
||||||
|
if (!title) return;
|
||||||
|
title.append(createCopyButton(pageWindow, conversationId));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 将当前会话 ID 的复制控件安装到 OneTalk 页面标题旁。 */
|
||||||
|
export const installOneTalkConversationIdCopyControl = (
|
||||||
|
pageWindow: OneTalkConversationIdCopyWindow,
|
||||||
|
): void => {
|
||||||
|
let refreshScheduled = false;
|
||||||
|
const refresh = (): void => {
|
||||||
|
refreshScheduled = false;
|
||||||
|
updateCopyControl(pageWindow);
|
||||||
|
};
|
||||||
|
const scheduleRefresh = (): void => {
|
||||||
|
if (refreshScheduled) return;
|
||||||
|
refreshScheduled = true;
|
||||||
|
pageWindow.setTimeout(refresh, 0);
|
||||||
|
};
|
||||||
|
const observer = pageWindow.MutationObserver
|
||||||
|
? new pageWindow.MutationObserver(scheduleRefresh)
|
||||||
|
: null;
|
||||||
|
if (observer && pageWindow.document.documentElement) {
|
||||||
|
observer.observe(pageWindow.document.documentElement, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// 定位 OneTalk React 文件上传器。
|
||||||
|
|
||||||
|
import { isObjectRecord } from "../../../lib/guards.ts";
|
||||||
|
|
||||||
|
import type { OneTalkPageWindow } from "../model.ts";
|
||||||
|
|
||||||
|
export type OneTalkImageUploader = {
|
||||||
|
owner: Record<string, unknown>;
|
||||||
|
sendFileToOss: (input: Record<string, unknown>) => unknown;
|
||||||
|
sendFile: (...args: unknown[]) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查找页面 React file input 对应的上传器。 */
|
||||||
|
export const findReactFileUploader = (
|
||||||
|
pageWindow: OneTalkPageWindow,
|
||||||
|
): OneTalkImageUploader | null => {
|
||||||
|
const inputs = Array.from(pageWindow.document?.querySelectorAll("input[type=file]") ?? []);
|
||||||
|
for (const input of inputs) {
|
||||||
|
for (
|
||||||
|
let node: (Element & { parentElement: Element | null }) | null = input;
|
||||||
|
node;
|
||||||
|
node = node.parentElement
|
||||||
|
) {
|
||||||
|
const fiberKey = Object.keys(node).find((key) => key.startsWith("__reactFiber"));
|
||||||
|
const fiber = fiberKey ? (node as unknown as Record<string, unknown>)[fiberKey] : null;
|
||||||
|
for (
|
||||||
|
let current: { stateNode?: unknown; return?: unknown } | null = isObjectRecord(
|
||||||
|
fiber,
|
||||||
|
)
|
||||||
|
? fiber
|
||||||
|
: null;
|
||||||
|
current;
|
||||||
|
current = isObjectRecord(current.return) ? current.return : null
|
||||||
|
) {
|
||||||
|
const state = current.stateNode;
|
||||||
|
if (
|
||||||
|
isObjectRecord(state) &&
|
||||||
|
typeof state.sendFileToOss === "function" &&
|
||||||
|
typeof state.sendFile === "function"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
owner: state,
|
||||||
|
sendFileToOss: state.sendFileToOss as (
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
) => unknown,
|
||||||
|
sendFile: state.sendFile as (...args: unknown[]) => unknown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 订阅 OneTalk 当前会话选择点击。
|
||||||
|
|
||||||
|
/** 订阅当前页面的点击事件以刷新会话身份。 */
|
||||||
|
export const subscribeToDocumentClicks = (
|
||||||
|
document: Pick<Document, "addEventListener"> | undefined,
|
||||||
|
onClick: () => void,
|
||||||
|
): void => {
|
||||||
|
document?.addEventListener("click", onClick);
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 读取 OneTalk SPA 当前选中会话的 DOM 边界。
|
||||||
|
|
||||||
|
import type { OneTalkPageWindow } from "../model.ts";
|
||||||
|
|
||||||
|
/** 读取页面中当前选中会话的非空 ID 列表。 */
|
||||||
|
export const readSelectedConversationIdsFromDom = (pageWindow: OneTalkPageWindow): string[] => {
|
||||||
|
if (!pageWindow.document) return [];
|
||||||
|
try {
|
||||||
|
return Array.from(
|
||||||
|
pageWindow.document.querySelectorAll(".contact-item-container.selected[data-cid]"),
|
||||||
|
)
|
||||||
|
.map((element) => element.getAttribute("data-cid")?.trim() ?? "")
|
||||||
|
.filter((value) => value.length > 0);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import type { OneTalkOutboundContent } from "@trade-message-center/onetalk-contr
|
|||||||
import { isObjectRecord } from "../../lib/guards.ts";
|
import { isObjectRecord } from "../../lib/guards.ts";
|
||||||
import { traceOneTalkImageSend } from "../diagnostics/image-send-trace.ts";
|
import { traceOneTalkImageSend } from "../diagnostics/image-send-trace.ts";
|
||||||
import type { PageCommandResult } from "../page-bridge/model.ts";
|
import type { PageCommandResult } from "../page-bridge/model.ts";
|
||||||
|
import { findReactFileUploader, type OneTalkImageUploader } from "./dom/react-file-uploader.ts";
|
||||||
import { readChannelAccountId } from "./page-context.ts";
|
import { readChannelAccountId } from "./page-context.ts";
|
||||||
import type { OneTalkPageWindow } from "./model.ts";
|
import type { OneTalkPageWindow } from "./model.ts";
|
||||||
import {
|
import {
|
||||||
@@ -17,11 +18,7 @@ type MediaSendObservationCorrelator = Pick<
|
|||||||
"executeImage" | "executeFile"
|
"executeImage" | "executeFile"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type OneTalkImageUploader = {
|
export type { OneTalkImageUploader } from "./dom/react-file-uploader.ts";
|
||||||
owner: Record<string, unknown>;
|
|
||||||
sendFileToOss: (input: Record<string, unknown>) => unknown;
|
|
||||||
sendFile: (...args: unknown[]) => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FinalImageMetadata = {
|
type FinalImageMetadata = {
|
||||||
sizeBytes: number;
|
sizeBytes: number;
|
||||||
@@ -149,45 +146,6 @@ const isCurrentImageTarget = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploaderFromReactFileInput = (pageWindow: OneTalkPageWindow): OneTalkImageUploader | null => {
|
|
||||||
const inputs = Array.from(pageWindow.document?.querySelectorAll("input[type=file]") ?? []);
|
|
||||||
for (const input of inputs) {
|
|
||||||
for (
|
|
||||||
let node: (Element & { parentElement: Element | null }) | null = input;
|
|
||||||
node;
|
|
||||||
node = node.parentElement
|
|
||||||
) {
|
|
||||||
const fiberKey = Object.keys(node).find((key) => key.startsWith("__reactFiber"));
|
|
||||||
const fiber = fiberKey ? (node as unknown as Record<string, unknown>)[fiberKey] : null;
|
|
||||||
for (
|
|
||||||
let current: { stateNode?: unknown; return?: unknown } | null = isObjectRecord(
|
|
||||||
fiber,
|
|
||||||
)
|
|
||||||
? fiber
|
|
||||||
: null;
|
|
||||||
current;
|
|
||||||
current = isObjectRecord(current.return) ? current.return : null
|
|
||||||
) {
|
|
||||||
const state = current.stateNode;
|
|
||||||
if (
|
|
||||||
isObjectRecord(state) &&
|
|
||||||
typeof state.sendFileToOss === "function" &&
|
|
||||||
typeof state.sendFile === "function"
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
owner: state,
|
|
||||||
sendFileToOss: state.sendFileToOss as (
|
|
||||||
input: Record<string, unknown>,
|
|
||||||
) => unknown,
|
|
||||||
sendFile: state.sendFile as (...args: unknown[]) => unknown,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const metadataFrom = (value: unknown): FinalImageMetadata | null => {
|
const metadataFrom = (value: unknown): FinalImageMetadata | null => {
|
||||||
if (!isObjectRecord(value)) return null;
|
if (!isObjectRecord(value)) return null;
|
||||||
const sizeBytes = value.nodeSize ?? value.size;
|
const sizeBytes = value.nodeSize ?? value.size;
|
||||||
@@ -415,7 +373,7 @@ const sendOneTalkMedia = async (input: {
|
|||||||
input.runtime?.cancel ?? ((timer: ImageSendTimer) => globalThis.clearTimeout(timer)),
|
input.runtime?.cancel ?? ((timer: ImageSendTimer) => globalThis.clearTimeout(timer)),
|
||||||
};
|
};
|
||||||
const context = imageTargetContext(input.pageWindow, input.conversationId);
|
const context = imageTargetContext(input.pageWindow, input.conversationId);
|
||||||
const uploader = (input.findUploader ?? uploaderFromReactFileInput)(input.pageWindow);
|
const uploader = (input.findUploader ?? findReactFileUploader)(input.pageWindow);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
traceOneTalkImageSend(input.requestId, {
|
traceOneTalkImageSend(input.requestId, {
|
||||||
stage: "target_context_resolved",
|
stage: "target_context_resolved",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { isObjectRecord } from "../../lib/guards.ts";
|
import { isObjectRecord } from "../../lib/guards.ts";
|
||||||
|
|
||||||
|
import { readSelectedConversationIdsFromDom } from "./dom/selection.ts";
|
||||||
import type { OneTalkPageWindow } from "./model.ts";
|
import type { OneTalkPageWindow } from "./model.ts";
|
||||||
|
|
||||||
export type OneTalkConversationSelection =
|
export type OneTalkConversationSelection =
|
||||||
@@ -143,14 +144,5 @@ export const readConversationSelection = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const readSelectedConversationIds = (pageWindow: OneTalkPageWindow): string[] => {
|
export const readSelectedConversationIds = (pageWindow: OneTalkPageWindow): string[] => {
|
||||||
if (!pageWindow.document) return [];
|
return readSelectedConversationIdsFromDom(pageWindow);
|
||||||
try {
|
|
||||||
return Array.from(
|
|
||||||
pageWindow.document.querySelectorAll(".contact-item-container.selected[data-cid]"),
|
|
||||||
)
|
|
||||||
.map((element) => element.getAttribute("data-cid")?.trim() ?? "")
|
|
||||||
.filter((value) => value.length > 0);
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// 启动 OneTalk 页面监听与当前会话同步能力
|
// 启动 OneTalk 页面监听与当前会话同步能力
|
||||||
|
|
||||||
import { installOneTalkConversationIdCopyControl } from "./conversation-id-copy.ts";
|
import { installOneTalkConversationIdCopyControl } from "./dom/conversation-id-copy.ts";
|
||||||
import {
|
import {
|
||||||
installOneTalkActionStatusTooltip,
|
installOneTalkActionStatusTooltip,
|
||||||
readOneTalkActionStatusTooltip,
|
readOneTalkActionStatusTooltip,
|
||||||
} from "./action-status-tooltip.ts";
|
} from "./dom/action-status-tooltip.ts";
|
||||||
import { ConnectionStatusTooltip } from "./connection-status-tooltip.ts";
|
import { ConnectionStatusTooltip } from "./connection-status-tooltip.ts";
|
||||||
import { HistoryBootstrapProgressTooltip } from "./current-conversation-history/bootstrap-progress-tooltip.ts";
|
import { HistoryBootstrapProgressTooltip } from "./current-conversation-history/bootstrap-progress-tooltip.ts";
|
||||||
import { createSendObservationCorrelator } from "./message-observer/send-observation.ts";
|
import { createSendObservationCorrelator } from "./message-observer/send-observation.ts";
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// 连接 OneTalk MAIN 页面与可信桥接契约
|
// 连接 OneTalk MAIN 页面与可信桥接契约
|
||||||
|
|
||||||
import { readChannelAccountId, readConversationSelection } from "../main-page/page-context.ts";
|
import { readChannelAccountId, readConversationSelection } from "../main-page/page-context.ts";
|
||||||
|
import { subscribeToDocumentClicks } from "../main-page/dom/selection-events.ts";
|
||||||
|
import type { OneTalkPageWindow } from "../main-page/model.ts";
|
||||||
import { isPlainRecord } from "@trade-message-center/onetalk-contract";
|
import { isPlainRecord } from "@trade-message-center/onetalk-contract";
|
||||||
import type { HistoryPageProgress } from "../main-page/current-conversation-history/model.ts";
|
import type { HistoryPageProgress } from "../main-page/current-conversation-history/model.ts";
|
||||||
import type { OneTalkObservedMessageSink } from "../main-page/message-observer/model.ts";
|
import type { OneTalkObservedMessageSink } from "../main-page/message-observer/model.ts";
|
||||||
@@ -44,6 +46,8 @@ export type OneTalkPageHelloRetryOptions = {
|
|||||||
maxAttempts?: number;
|
maxAttempts?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type OneTalkMainPageBridgeWindow = OneTalkPageBridgeWindow & OneTalkPageWindow;
|
||||||
|
|
||||||
const PAGE_HELLO_RETRY_DELAY_MS = 250;
|
const PAGE_HELLO_RETRY_DELAY_MS = 250;
|
||||||
const PAGE_HELLO_MAX_ATTEMPTS = 40;
|
const PAGE_HELLO_MAX_ATTEMPTS = 40;
|
||||||
|
|
||||||
@@ -73,7 +77,7 @@ const postPageMessage = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const postPageHello = (pageWindow: OneTalkPageBridgeWindow, origin: string): boolean => {
|
const postPageHello = (pageWindow: OneTalkMainPageBridgeWindow, origin: string): boolean => {
|
||||||
const channelAccountId = readChannelAccountId(pageWindow);
|
const channelAccountId = readChannelAccountId(pageWindow);
|
||||||
if (!channelAccountId) return false;
|
if (!channelAccountId) return false;
|
||||||
const selection = readConversationSelection(pageWindow);
|
const selection = readConversationSelection(pageWindow);
|
||||||
@@ -244,7 +248,7 @@ export const createOneTalkPageBuyerFactsObservedSink = (
|
|||||||
|
|
||||||
/** 安装 MAIN 页面注册与可注入命令消费者。 */
|
/** 安装 MAIN 页面注册与可注入命令消费者。 */
|
||||||
export const installOneTalkMainPageBridge = (
|
export const installOneTalkMainPageBridge = (
|
||||||
pageWindow: OneTalkPageBridgeWindow,
|
pageWindow: OneTalkMainPageBridgeWindow,
|
||||||
onCommand?: OneTalkPageCommandHandler,
|
onCommand?: OneTalkPageCommandHandler,
|
||||||
retryOptions: OneTalkPageHelloRetryOptions = {},
|
retryOptions: OneTalkPageHelloRetryOptions = {},
|
||||||
onConnectionStatus?: OneTalkPageConnectionStatusHandler,
|
onConnectionStatus?: OneTalkPageConnectionStatusHandler,
|
||||||
@@ -299,7 +303,7 @@ export const installOneTalkMainPageBridge = (
|
|||||||
};
|
};
|
||||||
pageWindow.addEventListener("popstate", refreshIdentity);
|
pageWindow.addEventListener("popstate", refreshIdentity);
|
||||||
pageWindow.addEventListener("hashchange", refreshIdentity);
|
pageWindow.addEventListener("hashchange", refreshIdentity);
|
||||||
pageWindow.document?.addEventListener("click", refreshIdentity);
|
subscribeToDocumentClicks(pageWindow.document, refreshIdentity);
|
||||||
attemptHello();
|
attemptHello();
|
||||||
} catch {
|
} catch {
|
||||||
// Bridge setup failures must not affect OneTalk page execution.
|
// Bridge setup failures must not affect OneTalk page execution.
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export type OneTalkPageMessage =
|
|||||||
|
|
||||||
export type OneTalkPageBridgeWindow = {
|
export type OneTalkPageBridgeWindow = {
|
||||||
location: Pick<Location, "href" | "origin">;
|
location: Pick<Location, "href" | "origin">;
|
||||||
document?: Pick<Document, "querySelectorAll" | "addEventListener">;
|
document?: Pick<Document, "addEventListener">;
|
||||||
postMessage(message: OneTalkPageMessage, targetOrigin: string): void;
|
postMessage(message: OneTalkPageMessage, targetOrigin: string): void;
|
||||||
addEventListener(type: "message", listener: (event: MessageEvent<unknown>) => void): void;
|
addEventListener(type: "message", listener: (event: MessageEvent<unknown>) => void): void;
|
||||||
addEventListener(type: "pagehide" | "popstate" | "hashchange", listener: () => void): void;
|
addEventListener(type: "pagehide" | "popstate" | "hashchange", listener: () => void): void;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// 创建可隔离的 OneTalk 页面 DOM 测试夹具。
|
||||||
|
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
|
/** 创建并返回带清理回调的真实 DOM 测试夹具。 */
|
||||||
|
export const createDomFixture = (html = "") => {
|
||||||
|
const dom = new JSDOM(`<!doctype html><html><body>${html}</body></html>`, {
|
||||||
|
url: "https://onetalk.alibaba.com/message/default.htm",
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
document: dom.window.document,
|
||||||
|
window: dom.window,
|
||||||
|
cleanup: () => dom.window.close(),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { installOneTalkActionStatusTooltip } from "../src/onetalk/main-page/action-status-tooltip.ts";
|
import { installOneTalkActionStatusTooltip } from "../src/onetalk/main-page/dom/action-status-tooltip.ts";
|
||||||
import { ConnectionStatusTooltip } from "../src/onetalk/main-page/connection-status-tooltip.ts";
|
import { ConnectionStatusTooltip } from "../src/onetalk/main-page/connection-status-tooltip.ts";
|
||||||
import { HistoryBootstrapProgressTooltip } from "../src/onetalk/main-page/current-conversation-history/bootstrap-progress-tooltip.ts";
|
import { HistoryBootstrapProgressTooltip } from "../src/onetalk/main-page/current-conversation-history/bootstrap-progress-tooltip.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { installOneTalkConversationIdCopyControl } from "../src/onetalk/main-page/conversation-id-copy.ts";
|
|
||||||
|
import { getByRole } from "@testing-library/dom";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
|
import { installOneTalkConversationIdCopyControl } from "../src/onetalk/main-page/dom/conversation-id-copy.ts";
|
||||||
|
import { createDomFixture } from "./dom-fixture.js";
|
||||||
|
|
||||||
const rect = (left, top, width, height) => ({
|
const rect = (left, top, width, height) => ({
|
||||||
left,
|
left,
|
||||||
@@ -11,126 +16,73 @@ const rect = (left, top, width, height) => ({
|
|||||||
bottom: top + height,
|
bottom: top + height,
|
||||||
});
|
});
|
||||||
|
|
||||||
class FakeElement {
|
|
||||||
constructor({ conversationId, elementRect, textContent = "" } = {}) {
|
|
||||||
this.conversationId = conversationId;
|
|
||||||
this.elementRect = elementRect ?? rect(0, 0, 0, 0);
|
|
||||||
this.textContent = textContent;
|
|
||||||
this.children = [];
|
|
||||||
this.dataset = {};
|
|
||||||
this.listeners = new Map();
|
|
||||||
this.style = { cssText: "" };
|
|
||||||
this.attributes = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
getAttribute(name) {
|
|
||||||
return name === "data-cid"
|
|
||||||
? (this.conversationId ?? null)
|
|
||||||
: (this.attributes.get(name) ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
setAttribute(name, value) {
|
|
||||||
this.attributes.set(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
getBoundingClientRect() {
|
|
||||||
return this.elementRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener(type, listener) {
|
|
||||||
this.listeners.set(type, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
append(element) {
|
|
||||||
this.inserted = element;
|
|
||||||
element.isConnected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
this.isConnected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createPage = () => {
|
const createPage = () => {
|
||||||
const selected = new FakeElement({
|
const fixture = createDomFixture(`
|
||||||
conversationId: "conversation-1",
|
<div class="contact-item-container selected" data-cid="conversation-1"></div>
|
||||||
elementRect: rect(80, 80, 340, 60),
|
<span data-testid="conversation-title">Elliot Izzard</span>
|
||||||
});
|
<span>OKKI销售助手</span>
|
||||||
const headerTitle = new FakeElement({
|
`);
|
||||||
elementRect: rect(436, 16, 112, 24),
|
const selected = fixture.document.querySelector(".contact-item-container");
|
||||||
textContent: "Elliot Izzard",
|
const headerTitle = fixture.document.querySelector("[data-testid=conversation-title]");
|
||||||
});
|
const navigationTitle = fixture.document.querySelector("span:last-child");
|
||||||
const navigationTitle = new FakeElement({
|
selected.getBoundingClientRect = () => rect(80, 80, 340, 60);
|
||||||
elementRect: rect(1_500, 16, 160, 24),
|
headerTitle.getBoundingClientRect = () => rect(436, 16, 112, 24);
|
||||||
textContent: "OKKI销售助手",
|
navigationTitle.getBoundingClientRect = () => rect(1_500, 16, 160, 24);
|
||||||
});
|
|
||||||
const document = {
|
|
||||||
documentElement: {},
|
|
||||||
defaultView: { innerWidth: 2_048 },
|
|
||||||
button: null,
|
|
||||||
querySelectorAll(selector) {
|
|
||||||
if (selector === ".contact-item-container.selected[data-cid]") return [selected];
|
|
||||||
if (selector === "body *") return [headerTitle, navigationTitle];
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
querySelector(selector) {
|
|
||||||
return selector === "button[data-tmc-conversation-id-copy]" ? this.button : null;
|
|
||||||
},
|
|
||||||
createElement() {
|
|
||||||
const button = new FakeElement();
|
|
||||||
button.type = "";
|
|
||||||
Object.defineProperty(button, "isConnected", { value: false, writable: true });
|
|
||||||
return button;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const clipboardWrites = [];
|
const clipboardWrites = [];
|
||||||
|
const navigator = { clipboard: { writeText: async (value) => clipboardWrites.push(value) } };
|
||||||
const pageWindow = {
|
const pageWindow = {
|
||||||
document,
|
document: fixture.document,
|
||||||
location: { href: "https://onetalk.alibaba.com/message/default.htm" },
|
location: fixture.window.location,
|
||||||
navigator: { clipboard: { writeText: async (value) => clipboardWrites.push(value) } },
|
navigator,
|
||||||
setTimeout: () => 1,
|
setTimeout: fixture.window.setTimeout.bind(fixture.window),
|
||||||
clearTimeout: () => {},
|
clearTimeout: fixture.window.clearTimeout.bind(fixture.window),
|
||||||
};
|
};
|
||||||
return { clipboardWrites, document, headerTitle, pageWindow, selected };
|
return { ...fixture, clipboardWrites, pageWindow };
|
||||||
};
|
};
|
||||||
|
|
||||||
test("installs a copy control beside the current conversation title", async () => {
|
test("installs a copy control beside the current conversation title and copies through user-event", async (t) => {
|
||||||
const { clipboardWrites, document, headerTitle, pageWindow } = createPage();
|
const { cleanup, clipboardWrites, document, pageWindow } = createPage();
|
||||||
|
t.after(cleanup);
|
||||||
|
|
||||||
installOneTalkConversationIdCopyControl(pageWindow);
|
installOneTalkConversationIdCopyControl(pageWindow);
|
||||||
|
|
||||||
const button = headerTitle.inserted;
|
const button = getByRole(document.body, "button", { name: "复制会话 Id" });
|
||||||
assert.ok(button);
|
|
||||||
assert.equal(button.textContent, "复制会话 Id");
|
|
||||||
assert.equal(button.dataset.conversationId, "conversation-1");
|
assert.equal(button.dataset.conversationId, "conversation-1");
|
||||||
assert.equal(button.getAttribute("aria-label"), "复制会话 Id");
|
|
||||||
assert.match(button.style.cssText, /font-size: 12px/u);
|
assert.match(button.style.cssText, /font-size: 12px/u);
|
||||||
assert.match(button.style.cssText, /justify-content: center/u);
|
assert.match(button.style.cssText, /justify-content: center/u);
|
||||||
button.listeners.get("click")();
|
|
||||||
|
const user = userEvent.setup({ document });
|
||||||
|
await user.click(button);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
assert.deepEqual(clipboardWrites, ["conversation-1"]);
|
assert.deepEqual(clipboardWrites, ["conversation-1"]);
|
||||||
assert.equal(button.textContent, "已复制");
|
assert.equal(button.textContent, "已复制");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does not install a control without one unambiguous selected conversation", () => {
|
test("does not install a control without one unambiguous selected conversation", (t) => {
|
||||||
const { document, pageWindow } = createPage();
|
const { cleanup, document, pageWindow } = createPage();
|
||||||
document.querySelectorAll = () => [];
|
t.after(cleanup);
|
||||||
|
document.querySelector(".contact-item-container").className = "contact-item-container";
|
||||||
|
|
||||||
installOneTalkConversationIdCopyControl(pageWindow);
|
installOneTalkConversationIdCopyControl(pageWindow);
|
||||||
|
|
||||||
assert.equal(document.button, null);
|
assert.equal(document.querySelector("button[data-tmc-conversation-id-copy]"), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shows a failure state when the page cannot write to the clipboard", async () => {
|
test("shows a failure state when the page cannot write to the clipboard", async (t) => {
|
||||||
const { headerTitle, pageWindow } = createPage();
|
const { cleanup, document, pageWindow } = createPage();
|
||||||
|
t.after(cleanup);
|
||||||
pageWindow.navigator.clipboard.writeText = async () => {
|
pageWindow.navigator.clipboard.writeText = async () => {
|
||||||
throw new Error("clipboard_denied");
|
throw new Error("clipboard_denied");
|
||||||
};
|
};
|
||||||
|
|
||||||
installOneTalkConversationIdCopyControl(pageWindow);
|
installOneTalkConversationIdCopyControl(pageWindow);
|
||||||
|
|
||||||
headerTitle.inserted.listeners.get("click")();
|
const button = getByRole(document.body, "button", { name: "复制会话 Id" });
|
||||||
|
const user = userEvent.setup({ document });
|
||||||
|
await user.click(button);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
assert.equal(headerTitle.inserted.textContent, "复制失败");
|
|
||||||
|
assert.equal(button.textContent, "复制失败");
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+428
@@ -27,9 +27,18 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/onetalk-contract
|
version: link:../../packages/onetalk-contract
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@testing-library/dom':
|
||||||
|
specifier: ^10.4.1
|
||||||
|
version: 10.4.1
|
||||||
|
'@testing-library/user-event':
|
||||||
|
specifier: ^14.6.7
|
||||||
|
version: 14.6.7(@testing-library/dom@10.4.1)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.10.2
|
specifier: ^22.10.2
|
||||||
version: 22.20.1
|
version: 22.20.1
|
||||||
|
jsdom:
|
||||||
|
specifier: ^30.0.1
|
||||||
|
version: 30.0.1
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.3
|
specifier: ^5.7.3
|
||||||
version: 5.7.3
|
version: 5.7.3
|
||||||
@@ -89,6 +98,66 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@asamuzakjp/css-color@6.0.7':
|
||||||
|
resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==}
|
||||||
|
engines: {node: ^22.13.0 || >=24.0.0}
|
||||||
|
|
||||||
|
'@asamuzakjp/dom-selector@8.3.2':
|
||||||
|
resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==}
|
||||||
|
engines: {node: ^22.13.0 || >=24.0.0}
|
||||||
|
|
||||||
|
'@babel/code-frame@7.29.7':
|
||||||
|
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.29.7':
|
||||||
|
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.7':
|
||||||
|
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@bramus/specificity@2.4.2':
|
||||||
|
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@csstools/color-helpers@6.1.1':
|
||||||
|
resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
|
'@csstools/css-calc@3.3.0':
|
||||||
|
resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@csstools/css-parser-algorithms': ^4.0.0
|
||||||
|
'@csstools/css-tokenizer': ^4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-color-parser@4.2.2':
|
||||||
|
resolution: {integrity: sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@csstools/css-parser-algorithms': ^4.0.0
|
||||||
|
'@csstools/css-tokenizer': ^4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-parser-algorithms@4.0.0':
|
||||||
|
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@csstools/css-tokenizer': ^4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-syntax-patches-for-csstree@1.1.13':
|
||||||
|
resolution: {integrity: sha512-i9ZylF5QNhmNfPA9l0vHAWK4kPrbIp6g9lKgaiIFsIBz2F/WNB7OLrzlNNcCOm+h42bkaSD2v1PG+IBPHhc3ZA==}
|
||||||
|
peerDependencies:
|
||||||
|
css-tree: ^3.2.1
|
||||||
|
peerDependenciesMeta:
|
||||||
|
css-tree:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@csstools/css-tokenizer@4.0.0':
|
||||||
|
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
'@drizzle-team/brocli@0.10.2':
|
'@drizzle-team/brocli@0.10.2':
|
||||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||||
|
|
||||||
@@ -694,6 +763,15 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@exodus/bytes@1.15.1':
|
||||||
|
resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@noble/hashes': ^1.8.0 || ^2.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@noble/hashes':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@fastify/ajv-compiler@4.0.6':
|
'@fastify/ajv-compiler@4.0.6':
|
||||||
resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==}
|
resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==}
|
||||||
|
|
||||||
@@ -985,6 +1063,19 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@testing-library/dom@10.4.1':
|
||||||
|
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@testing-library/user-event@14.6.7':
|
||||||
|
resolution: {integrity: sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
peerDependencies:
|
||||||
|
'@testing-library/dom': '>=7.21.4'
|
||||||
|
|
||||||
|
'@types/aria-query@5.0.4':
|
||||||
|
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||||
|
|
||||||
'@types/estree@1.0.9':
|
'@types/estree@1.0.9':
|
||||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
@@ -1005,6 +1096,17 @@ packages:
|
|||||||
ajv@8.20.0:
|
ajv@8.20.0:
|
||||||
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@5.2.0:
|
||||||
|
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
aria-query@5.3.0:
|
||||||
|
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||||
|
|
||||||
atomic-sleep@1.0.0:
|
atomic-sleep@1.0.0:
|
||||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -1012,6 +1114,9 @@ packages:
|
|||||||
avvio@9.3.0:
|
avvio@9.3.0:
|
||||||
resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==}
|
resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==}
|
||||||
|
|
||||||
|
bidi-js@1.1.0:
|
||||||
|
resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
@@ -1019,10 +1124,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==}
|
resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
css-tree@3.2.1:
|
||||||
|
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||||
|
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||||
|
|
||||||
|
data-urls@7.0.0:
|
||||||
|
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
|
decimal.js@10.6.0:
|
||||||
|
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||||
|
|
||||||
dequal@2.0.3:
|
dequal@2.0.3:
|
||||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
dom-accessibility-api@0.5.16:
|
||||||
|
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||||
|
|
||||||
drizzle-kit@0.31.10:
|
drizzle-kit@0.31.10:
|
||||||
resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
|
resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1125,6 +1244,10 @@ packages:
|
|||||||
end-of-stream@1.4.5:
|
end-of-stream@1.4.5:
|
||||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||||
|
|
||||||
|
entities@8.1.0:
|
||||||
|
resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
esbuild@0.18.20:
|
esbuild@0.18.20:
|
||||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1193,6 +1316,10 @@ packages:
|
|||||||
get-tsconfig@4.14.3:
|
get-tsconfig@4.14.3:
|
||||||
resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==}
|
resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==}
|
||||||
|
|
||||||
|
html-encoding-sniffer@6.0.0:
|
||||||
|
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
husky@9.1.7:
|
husky@9.1.7:
|
||||||
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1205,6 +1332,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==}
|
resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
is-potential-custom-element-name@1.0.1:
|
||||||
|
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||||
|
|
||||||
|
js-tokens@4.0.0:
|
||||||
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
jsdom@30.0.1:
|
||||||
|
resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==}
|
||||||
|
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
canvas: ^3.2.3
|
||||||
|
peerDependenciesMeta:
|
||||||
|
canvas:
|
||||||
|
optional: true
|
||||||
|
|
||||||
json-schema-ref-resolver@3.0.0:
|
json-schema-ref-resolver@3.0.0:
|
||||||
resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
|
resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
|
||||||
|
|
||||||
@@ -1219,6 +1361,17 @@ packages:
|
|||||||
engines: {node: '>=22.22.1'}
|
engines: {node: '>=22.22.1'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
lru-cache@11.5.2:
|
||||||
|
resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
lz-string@1.5.0:
|
||||||
|
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
mdn-data@2.27.1:
|
||||||
|
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||||
|
|
||||||
nanoid@3.3.18:
|
nanoid@3.3.18:
|
||||||
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -1244,6 +1397,9 @@ packages:
|
|||||||
vite-plus:
|
vite-plus:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
parse5@8.0.1:
|
||||||
|
resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -1269,15 +1425,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==}
|
resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
pretty-format@27.5.1:
|
||||||
|
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||||
|
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||||
|
|
||||||
process-warning@4.0.0:
|
process-warning@4.0.0:
|
||||||
resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==}
|
resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==}
|
||||||
|
|
||||||
process-warning@5.1.0:
|
process-warning@5.1.0:
|
||||||
resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==}
|
resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==}
|
||||||
|
|
||||||
|
punycode@2.3.1:
|
||||||
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4:
|
quick-format-unescaped@4.0.4:
|
||||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||||
|
|
||||||
|
react-is@17.0.2:
|
||||||
|
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -1323,6 +1490,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
saxes@6.0.0:
|
||||||
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
|
engines: {node: '>=v12.22.7'}
|
||||||
|
|
||||||
secure-json-parse@4.1.0:
|
secure-json-parse@4.1.0:
|
||||||
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
|
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
|
||||||
|
|
||||||
@@ -1362,6 +1533,9 @@ packages:
|
|||||||
string_decoder@1.3.0:
|
string_decoder@1.3.0:
|
||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
|
symbol-tree@3.2.4:
|
||||||
|
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||||
|
|
||||||
thread-stream@4.2.0:
|
thread-stream@4.2.0:
|
||||||
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -1378,10 +1552,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
|
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
|
||||||
engines: {node: ^20.0.0 || >=22.0.0}
|
engines: {node: ^20.0.0 || >=22.0.0}
|
||||||
|
|
||||||
|
tldts-core@7.4.12:
|
||||||
|
resolution: {integrity: sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==}
|
||||||
|
|
||||||
|
tldts@7.4.12:
|
||||||
|
resolution: {integrity: sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
toad-cache@3.7.4:
|
toad-cache@3.7.4:
|
||||||
resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==}
|
resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
tough-cookie@6.0.2:
|
||||||
|
resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
tr46@6.0.0:
|
||||||
|
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
tsx@4.23.12:
|
tsx@4.23.12:
|
||||||
resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==}
|
resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -1395,6 +1584,10 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
undici@8.10.2:
|
||||||
|
resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==}
|
||||||
|
engines: {node: '>=22.19.0'}
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@@ -1438,6 +1631,26 @@ packages:
|
|||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
w3c-xmlserializer@5.0.0:
|
||||||
|
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
webidl-conversions@8.0.1:
|
||||||
|
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
whatwg-mimetype@5.0.0:
|
||||||
|
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
whatwg-url@16.0.1:
|
||||||
|
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
|
whatwg-url@17.1.1:
|
||||||
|
resolution: {integrity: sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==}
|
||||||
|
engines: {node: ^22.14.0 || >=24.0.0}
|
||||||
|
|
||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
@@ -1453,6 +1666,13 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
xml-name-validator@5.0.0:
|
||||||
|
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
xmlchars@2.2.0:
|
||||||
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
|
|
||||||
yaml@2.9.0:
|
yaml@2.9.0:
|
||||||
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
||||||
engines: {node: '>= 14.6'}
|
engines: {node: '>= 14.6'}
|
||||||
@@ -1460,6 +1680,59 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@asamuzakjp/css-color@6.0.7':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-color-parser': 4.2.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
lru-cache: 11.5.2
|
||||||
|
|
||||||
|
'@asamuzakjp/dom-selector@8.3.2':
|
||||||
|
dependencies:
|
||||||
|
bidi-js: 1.1.0
|
||||||
|
css-tree: 3.2.1
|
||||||
|
is-potential-custom-element-name: 1.0.1
|
||||||
|
lru-cache: 11.5.2
|
||||||
|
|
||||||
|
'@babel/code-frame@7.29.7':
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
js-tokens: 4.0.0
|
||||||
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.29.7': {}
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.7': {}
|
||||||
|
|
||||||
|
'@bramus/specificity@2.4.2':
|
||||||
|
dependencies:
|
||||||
|
css-tree: 3.2.1
|
||||||
|
|
||||||
|
'@csstools/color-helpers@6.1.1': {}
|
||||||
|
|
||||||
|
'@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-color-parser@4.2.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/color-helpers': 6.1.1
|
||||||
|
'@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-syntax-patches-for-csstree@1.1.13(css-tree@3.2.1)':
|
||||||
|
optionalDependencies:
|
||||||
|
css-tree: 3.2.1
|
||||||
|
|
||||||
|
'@csstools/css-tokenizer@4.0.0': {}
|
||||||
|
|
||||||
'@drizzle-team/brocli@0.10.2': {}
|
'@drizzle-team/brocli@0.10.2': {}
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
@@ -1769,6 +2042,8 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.28.2':
|
'@esbuild/win32-x64@0.28.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@exodus/bytes@1.15.1': {}
|
||||||
|
|
||||||
'@fastify/ajv-compiler@4.0.6':
|
'@fastify/ajv-compiler@4.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 8.20.0
|
ajv: 8.20.0
|
||||||
@@ -1938,6 +2213,23 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.62.5':
|
'@rollup/rollup-win32-x64-msvc@4.62.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@testing-library/dom@10.4.1':
|
||||||
|
dependencies:
|
||||||
|
'@babel/code-frame': 7.29.7
|
||||||
|
'@babel/runtime': 7.29.7
|
||||||
|
'@types/aria-query': 5.0.4
|
||||||
|
aria-query: 5.3.0
|
||||||
|
dom-accessibility-api: 0.5.16
|
||||||
|
lz-string: 1.5.0
|
||||||
|
picocolors: 1.1.1
|
||||||
|
pretty-format: 27.5.1
|
||||||
|
|
||||||
|
'@testing-library/user-event@14.6.7(@testing-library/dom@10.4.1)':
|
||||||
|
dependencies:
|
||||||
|
'@testing-library/dom': 10.4.1
|
||||||
|
|
||||||
|
'@types/aria-query@5.0.4': {}
|
||||||
|
|
||||||
'@types/estree@1.0.9': {}
|
'@types/estree@1.0.9': {}
|
||||||
|
|
||||||
'@types/node@22.20.1':
|
'@types/node@22.20.1':
|
||||||
@@ -1957,6 +2249,14 @@ snapshots:
|
|||||||
json-schema-traverse: 1.0.0
|
json-schema-traverse: 1.0.0
|
||||||
require-from-string: 2.0.2
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-styles@5.2.0: {}
|
||||||
|
|
||||||
|
aria-query@5.3.0:
|
||||||
|
dependencies:
|
||||||
|
dequal: 2.0.3
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
atomic-sleep@1.0.0: {}
|
||||||
|
|
||||||
avvio@9.3.0:
|
avvio@9.3.0:
|
||||||
@@ -1964,12 +2264,32 @@ snapshots:
|
|||||||
'@fastify/error': 4.2.0
|
'@fastify/error': 4.2.0
|
||||||
fastq: 1.20.1
|
fastq: 1.20.1
|
||||||
|
|
||||||
|
bidi-js@1.1.0:
|
||||||
|
dependencies:
|
||||||
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
cookie@1.0.1: {}
|
cookie@1.0.1: {}
|
||||||
|
|
||||||
|
css-tree@3.2.1:
|
||||||
|
dependencies:
|
||||||
|
mdn-data: 2.27.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
data-urls@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
whatwg-mimetype: 5.0.0
|
||||||
|
whatwg-url: 16.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@noble/hashes'
|
||||||
|
|
||||||
|
decimal.js@10.6.0: {}
|
||||||
|
|
||||||
dequal@2.0.3: {}
|
dequal@2.0.3: {}
|
||||||
|
|
||||||
|
dom-accessibility-api@0.5.16: {}
|
||||||
|
|
||||||
drizzle-kit@0.31.10:
|
drizzle-kit@0.31.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@drizzle-team/brocli': 0.10.2
|
'@drizzle-team/brocli': 0.10.2
|
||||||
@@ -1992,6 +2312,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
|
||||||
|
entities@8.1.0: {}
|
||||||
|
|
||||||
esbuild@0.18.20:
|
esbuild@0.18.20:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/android-arm': 0.18.20
|
'@esbuild/android-arm': 0.18.20
|
||||||
@@ -2165,12 +2487,48 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
html-encoding-sniffer@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@exodus/bytes': 1.15.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@noble/hashes'
|
||||||
|
|
||||||
husky@9.1.7: {}
|
husky@9.1.7: {}
|
||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
ipaddr.js@2.5.0: {}
|
ipaddr.js@2.5.0: {}
|
||||||
|
|
||||||
|
is-potential-custom-element-name@1.0.1: {}
|
||||||
|
|
||||||
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
|
jsdom@30.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@asamuzakjp/css-color': 6.0.7
|
||||||
|
'@asamuzakjp/dom-selector': 8.3.2
|
||||||
|
'@bramus/specificity': 2.4.2
|
||||||
|
'@csstools/css-syntax-patches-for-csstree': 1.1.13(css-tree@3.2.1)
|
||||||
|
'@exodus/bytes': 1.15.1
|
||||||
|
css-tree: 3.2.1
|
||||||
|
data-urls: 7.0.0
|
||||||
|
decimal.js: 10.6.0
|
||||||
|
html-encoding-sniffer: 6.0.0
|
||||||
|
is-potential-custom-element-name: 1.0.1
|
||||||
|
lru-cache: 11.5.2
|
||||||
|
parse5: 8.0.1
|
||||||
|
saxes: 6.0.0
|
||||||
|
symbol-tree: 3.2.4
|
||||||
|
tough-cookie: 6.0.2
|
||||||
|
undici: 8.10.2
|
||||||
|
w3c-xmlserializer: 5.0.0
|
||||||
|
webidl-conversions: 8.0.1
|
||||||
|
whatwg-mimetype: 5.0.0
|
||||||
|
whatwg-url: 17.1.1
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@noble/hashes'
|
||||||
|
|
||||||
json-schema-ref-resolver@3.0.0:
|
json-schema-ref-resolver@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
@@ -2191,6 +2549,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
yaml: 2.9.0
|
yaml: 2.9.0
|
||||||
|
|
||||||
|
lru-cache@11.5.2: {}
|
||||||
|
|
||||||
|
lz-string@1.5.0: {}
|
||||||
|
|
||||||
|
mdn-data@2.27.1: {}
|
||||||
|
|
||||||
nanoid@3.3.18: {}
|
nanoid@3.3.18: {}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2: {}
|
on-exit-leak-free@2.1.2: {}
|
||||||
@@ -2223,6 +2587,10 @@ snapshots:
|
|||||||
'@oxfmt/binding-win32-ia32-msvc': 0.64.0
|
'@oxfmt/binding-win32-ia32-msvc': 0.64.0
|
||||||
'@oxfmt/binding-win32-x64-msvc': 0.64.0
|
'@oxfmt/binding-win32-x64-msvc': 0.64.0
|
||||||
|
|
||||||
|
parse5@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
entities: 8.1.0
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.5: {}
|
picomatch@4.0.5: {}
|
||||||
@@ -2255,12 +2623,22 @@ snapshots:
|
|||||||
|
|
||||||
postgres@3.4.9: {}
|
postgres@3.4.9: {}
|
||||||
|
|
||||||
|
pretty-format@27.5.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
ansi-styles: 5.2.0
|
||||||
|
react-is: 17.0.2
|
||||||
|
|
||||||
process-warning@4.0.0: {}
|
process-warning@4.0.0: {}
|
||||||
|
|
||||||
process-warning@5.1.0: {}
|
process-warning@5.1.0: {}
|
||||||
|
|
||||||
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4: {}
|
quick-format-unescaped@4.0.4: {}
|
||||||
|
|
||||||
|
react-is@17.0.2: {}
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
@@ -2321,6 +2699,10 @@ snapshots:
|
|||||||
|
|
||||||
safe-stable-stringify@2.5.0: {}
|
safe-stable-stringify@2.5.0: {}
|
||||||
|
|
||||||
|
saxes@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
secure-json-parse@4.1.0: {}
|
secure-json-parse@4.1.0: {}
|
||||||
|
|
||||||
semver@7.8.5: {}
|
semver@7.8.5: {}
|
||||||
@@ -2350,6 +2732,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
symbol-tree@3.2.4: {}
|
||||||
|
|
||||||
thread-stream@4.2.0:
|
thread-stream@4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
real-require: 1.0.0
|
real-require: 1.0.0
|
||||||
@@ -2363,8 +2747,22 @@ snapshots:
|
|||||||
|
|
||||||
tinypool@2.1.0: {}
|
tinypool@2.1.0: {}
|
||||||
|
|
||||||
|
tldts-core@7.4.12: {}
|
||||||
|
|
||||||
|
tldts@7.4.12:
|
||||||
|
dependencies:
|
||||||
|
tldts-core: 7.4.12
|
||||||
|
|
||||||
toad-cache@3.7.4: {}
|
toad-cache@3.7.4: {}
|
||||||
|
|
||||||
|
tough-cookie@6.0.2:
|
||||||
|
dependencies:
|
||||||
|
tldts: 7.4.12
|
||||||
|
|
||||||
|
tr46@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.1
|
||||||
|
|
||||||
tsx@4.23.12:
|
tsx@4.23.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.28.2
|
esbuild: 0.28.2
|
||||||
@@ -2375,6 +2773,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
|
undici@8.10.2: {}
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vite@6.4.3(@types/node@22.20.1)(tsx@4.23.12)(yaml@2.9.0):
|
vite@6.4.3(@types/node@22.20.1)(tsx@4.23.12)(yaml@2.9.0):
|
||||||
@@ -2391,9 +2791,37 @@ snapshots:
|
|||||||
tsx: 4.23.12
|
tsx: 4.23.12
|
||||||
yaml: 2.9.0
|
yaml: 2.9.0
|
||||||
|
|
||||||
|
w3c-xmlserializer@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
|
||||||
|
webidl-conversions@8.0.1: {}
|
||||||
|
|
||||||
|
whatwg-mimetype@5.0.0: {}
|
||||||
|
|
||||||
|
whatwg-url@16.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@exodus/bytes': 1.15.1
|
||||||
|
tr46: 6.0.0
|
||||||
|
webidl-conversions: 8.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@noble/hashes'
|
||||||
|
|
||||||
|
whatwg-url@17.1.1:
|
||||||
|
dependencies:
|
||||||
|
'@exodus/bytes': 1.15.1
|
||||||
|
tr46: 6.0.0
|
||||||
|
webidl-conversions: 8.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@noble/hashes'
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
ws@8.21.3: {}
|
ws@8.21.3: {}
|
||||||
|
|
||||||
|
xml-name-validator@5.0.0: {}
|
||||||
|
|
||||||
|
xmlchars@2.2.0: {}
|
||||||
|
|
||||||
yaml@2.9.0:
|
yaml@2.9.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|||||||
Reference in New Issue
Block a user