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
|
||||
|
||||
@@ -33,6 +33,7 @@ class ConnectionStatusTooltip {
|
||||
- 按钮直接追加到标题节点,使用 `inline-flex` 和 `justify-content: center` 保持同行及文字水平居中;文字为“复制会话 Id”,字体显式为 `12px`,不能依赖会话标题的继承字体。
|
||||
- 点击处理在用户手势中调用 `navigator.clipboard.writeText(conversationId)`;成功显示“已复制”,不可用或 reject 显示“复制失败”,随后恢复按钮文案。不会以 `execCommand`、隐形文本框或其它方式降级复制。
|
||||
- 页面 DOM 尚未就绪或 SPA 重绘时可由 `MutationObserver` 重新尝试挂载;同一页面只能存在一个该属性的按钮。
|
||||
- 非观测页面 DOM 查询、控件创建和更新仅归 `main-page/dom/` 所有;调用方只能使用其语义 API,不能重新取得同一 DOM 权限。
|
||||
- 动作提示以固定定位、`pointer-events: none` 的 extension-owned `data-tmc-action-status-*` DOM 节点呈现,不依赖 OneTalk 的业务 DOM,也不改变宿主布局。多个活动条目按照首次 `start` 的顺序纵向显示。
|
||||
- `start` 对同一 `id` 幂等,不覆盖已有行;`update` 与 `close` 只作用于已存在的 `id`,找不到时返回 `false`,不得隐式创建行。颜色只接受 `neutral`、`info`、`warning`、`error`,任意 CSS 色值必须显式失败。
|
||||
- 状态 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:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/user-event": "^14.6.7",
|
||||
"@types/node": "^22.10.2",
|
||||
"jsdom": "^30.0.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
|
||||
@@ -1,192 +1,7 @@
|
||||
// 管理 OneTalk 页面进行中动作提示
|
||||
|
||||
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;
|
||||
};
|
||||
// 兼容现有控件消费者;DOM 实现在专属适配目录中。
|
||||
export {
|
||||
installOneTalkActionStatusTooltip,
|
||||
readOneTalkActionStatusTooltip,
|
||||
type OneTalkActionStatusTooltip,
|
||||
type TooltipColor,
|
||||
} from "./dom/action-status-tooltip.ts";
|
||||
|
||||
@@ -1,147 +1,2 @@
|
||||
// 在当前会话标题旁提供复制会话 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();
|
||||
};
|
||||
// 兼容现有控件消费者;DOM 实现在专属适配目录中。
|
||||
export { installOneTalkConversationIdCopyControl } from "./dom/conversation-id-copy.ts";
|
||||
|
||||
@@ -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 { traceOneTalkImageSend } from "../diagnostics/image-send-trace.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 type { OneTalkPageWindow } from "./model.ts";
|
||||
import {
|
||||
@@ -17,11 +18,7 @@ type MediaSendObservationCorrelator = Pick<
|
||||
"executeImage" | "executeFile"
|
||||
>;
|
||||
|
||||
export type OneTalkImageUploader = {
|
||||
owner: Record<string, unknown>;
|
||||
sendFileToOss: (input: Record<string, unknown>) => unknown;
|
||||
sendFile: (...args: unknown[]) => unknown;
|
||||
};
|
||||
export type { OneTalkImageUploader } from "./dom/react-file-uploader.ts";
|
||||
|
||||
type FinalImageMetadata = {
|
||||
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 => {
|
||||
if (!isObjectRecord(value)) return null;
|
||||
const sizeBytes = value.nodeSize ?? value.size;
|
||||
@@ -415,7 +373,7 @@ const sendOneTalkMedia = async (input: {
|
||||
input.runtime?.cancel ?? ((timer: ImageSendTimer) => globalThis.clearTimeout(timer)),
|
||||
};
|
||||
const context = imageTargetContext(input.pageWindow, input.conversationId);
|
||||
const uploader = (input.findUploader ?? uploaderFromReactFileInput)(input.pageWindow);
|
||||
const uploader = (input.findUploader ?? findReactFileUploader)(input.pageWindow);
|
||||
if (!context) {
|
||||
traceOneTalkImageSend(input.requestId, {
|
||||
stage: "target_context_resolved",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { isObjectRecord } from "../../lib/guards.ts";
|
||||
|
||||
import { readSelectedConversationIdsFromDom } from "./dom/selection.ts";
|
||||
import type { OneTalkPageWindow } from "./model.ts";
|
||||
|
||||
export type OneTalkConversationSelection =
|
||||
@@ -143,14 +144,5 @@ export const readConversationSelection = (
|
||||
};
|
||||
|
||||
export const readSelectedConversationIds = (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 [];
|
||||
}
|
||||
return readSelectedConversationIdsFromDom(pageWindow);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// 启动 OneTalk 页面监听与当前会话同步能力
|
||||
|
||||
import { installOneTalkConversationIdCopyControl } from "./conversation-id-copy.ts";
|
||||
import { installOneTalkConversationIdCopyControl } from "./dom/conversation-id-copy.ts";
|
||||
import {
|
||||
installOneTalkActionStatusTooltip,
|
||||
readOneTalkActionStatusTooltip,
|
||||
} from "./action-status-tooltip.ts";
|
||||
} from "./dom/action-status-tooltip.ts";
|
||||
import { ConnectionStatusTooltip } from "./connection-status-tooltip.ts";
|
||||
import { HistoryBootstrapProgressTooltip } from "./current-conversation-history/bootstrap-progress-tooltip.ts";
|
||||
import { createSendObservationCorrelator } from "./message-observer/send-observation.ts";
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 连接 OneTalk MAIN 页面与可信桥接契约
|
||||
|
||||
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 type { HistoryPageProgress } from "../main-page/current-conversation-history/model.ts";
|
||||
import type { OneTalkObservedMessageSink } from "../main-page/message-observer/model.ts";
|
||||
@@ -44,6 +46,8 @@ export type OneTalkPageHelloRetryOptions = {
|
||||
maxAttempts?: number;
|
||||
};
|
||||
|
||||
type OneTalkMainPageBridgeWindow = OneTalkPageBridgeWindow & OneTalkPageWindow;
|
||||
|
||||
const PAGE_HELLO_RETRY_DELAY_MS = 250;
|
||||
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);
|
||||
if (!channelAccountId) return false;
|
||||
const selection = readConversationSelection(pageWindow);
|
||||
@@ -244,7 +248,7 @@ export const createOneTalkPageBuyerFactsObservedSink = (
|
||||
|
||||
/** 安装 MAIN 页面注册与可注入命令消费者。 */
|
||||
export const installOneTalkMainPageBridge = (
|
||||
pageWindow: OneTalkPageBridgeWindow,
|
||||
pageWindow: OneTalkMainPageBridgeWindow,
|
||||
onCommand?: OneTalkPageCommandHandler,
|
||||
retryOptions: OneTalkPageHelloRetryOptions = {},
|
||||
onConnectionStatus?: OneTalkPageConnectionStatusHandler,
|
||||
@@ -299,7 +303,7 @@ export const installOneTalkMainPageBridge = (
|
||||
};
|
||||
pageWindow.addEventListener("popstate", refreshIdentity);
|
||||
pageWindow.addEventListener("hashchange", refreshIdentity);
|
||||
pageWindow.document?.addEventListener("click", refreshIdentity);
|
||||
subscribeToDocumentClicks(pageWindow.document, refreshIdentity);
|
||||
attemptHello();
|
||||
} catch {
|
||||
// Bridge setup failures must not affect OneTalk page execution.
|
||||
|
||||
@@ -123,7 +123,7 @@ export type OneTalkPageMessage =
|
||||
|
||||
export type OneTalkPageBridgeWindow = {
|
||||
location: Pick<Location, "href" | "origin">;
|
||||
document?: Pick<Document, "querySelectorAll" | "addEventListener">;
|
||||
document?: Pick<Document, "addEventListener">;
|
||||
postMessage(message: OneTalkPageMessage, targetOrigin: string): void;
|
||||
addEventListener(type: "message", listener: (event: MessageEvent<unknown>) => 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 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 { HistoryBootstrapProgressTooltip } from "../src/onetalk/main-page/current-conversation-history/bootstrap-progress-tooltip.ts";
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import assert from "node:assert/strict";
|
||||
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) => ({
|
||||
left,
|
||||
@@ -11,126 +16,73 @@ const rect = (left, top, width, 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 selected = new FakeElement({
|
||||
conversationId: "conversation-1",
|
||||
elementRect: rect(80, 80, 340, 60),
|
||||
});
|
||||
const headerTitle = new FakeElement({
|
||||
elementRect: rect(436, 16, 112, 24),
|
||||
textContent: "Elliot Izzard",
|
||||
});
|
||||
const navigationTitle = new FakeElement({
|
||||
elementRect: rect(1_500, 16, 160, 24),
|
||||
textContent: "OKKI销售助手",
|
||||
});
|
||||
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 fixture = createDomFixture(`
|
||||
<div class="contact-item-container selected" data-cid="conversation-1"></div>
|
||||
<span data-testid="conversation-title">Elliot Izzard</span>
|
||||
<span>OKKI销售助手</span>
|
||||
`);
|
||||
const selected = fixture.document.querySelector(".contact-item-container");
|
||||
const headerTitle = fixture.document.querySelector("[data-testid=conversation-title]");
|
||||
const navigationTitle = fixture.document.querySelector("span:last-child");
|
||||
selected.getBoundingClientRect = () => rect(80, 80, 340, 60);
|
||||
headerTitle.getBoundingClientRect = () => rect(436, 16, 112, 24);
|
||||
navigationTitle.getBoundingClientRect = () => rect(1_500, 16, 160, 24);
|
||||
|
||||
const clipboardWrites = [];
|
||||
const navigator = { clipboard: { writeText: async (value) => clipboardWrites.push(value) } };
|
||||
const pageWindow = {
|
||||
document,
|
||||
location: { href: "https://onetalk.alibaba.com/message/default.htm" },
|
||||
navigator: { clipboard: { writeText: async (value) => clipboardWrites.push(value) } },
|
||||
setTimeout: () => 1,
|
||||
clearTimeout: () => {},
|
||||
document: fixture.document,
|
||||
location: fixture.window.location,
|
||||
navigator,
|
||||
setTimeout: fixture.window.setTimeout.bind(fixture.window),
|
||||
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 () => {
|
||||
const { clipboardWrites, document, headerTitle, pageWindow } = createPage();
|
||||
test("installs a copy control beside the current conversation title and copies through user-event", async (t) => {
|
||||
const { cleanup, clipboardWrites, document, pageWindow } = createPage();
|
||||
t.after(cleanup);
|
||||
|
||||
installOneTalkConversationIdCopyControl(pageWindow);
|
||||
|
||||
const button = headerTitle.inserted;
|
||||
assert.ok(button);
|
||||
assert.equal(button.textContent, "复制会话 Id");
|
||||
const button = getByRole(document.body, "button", { name: "复制会话 Id" });
|
||||
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, /justify-content: center/u);
|
||||
button.listeners.get("click")();
|
||||
|
||||
const user = userEvent.setup({ document });
|
||||
await user.click(button);
|
||||
await Promise.resolve();
|
||||
|
||||
assert.deepEqual(clipboardWrites, ["conversation-1"]);
|
||||
assert.equal(button.textContent, "已复制");
|
||||
});
|
||||
|
||||
test("does not install a control without one unambiguous selected conversation", () => {
|
||||
const { document, pageWindow } = createPage();
|
||||
document.querySelectorAll = () => [];
|
||||
test("does not install a control without one unambiguous selected conversation", (t) => {
|
||||
const { cleanup, document, pageWindow } = createPage();
|
||||
t.after(cleanup);
|
||||
document.querySelector(".contact-item-container").className = "contact-item-container";
|
||||
|
||||
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 () => {
|
||||
const { headerTitle, pageWindow } = createPage();
|
||||
test("shows a failure state when the page cannot write to the clipboard", async (t) => {
|
||||
const { cleanup, document, pageWindow } = createPage();
|
||||
t.after(cleanup);
|
||||
pageWindow.navigator.clipboard.writeText = async () => {
|
||||
throw new Error("clipboard_denied");
|
||||
};
|
||||
|
||||
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();
|
||||
assert.equal(headerTitle.inserted.textContent, "复制失败");
|
||||
|
||||
assert.equal(button.textContent, "复制失败");
|
||||
});
|
||||
|
||||
Generated
+428
@@ -27,9 +27,18 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/onetalk-contract
|
||||
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':
|
||||
specifier: ^22.10.2
|
||||
version: 22.20.1
|
||||
jsdom:
|
||||
specifier: ^30.0.1
|
||||
version: 30.0.1
|
||||
typescript:
|
||||
specifier: ^5.7.3
|
||||
version: 5.7.3
|
||||
@@ -89,6 +98,66 @@ importers:
|
||||
|
||||
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':
|
||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||
|
||||
@@ -694,6 +763,15 @@ packages:
|
||||
cpu: [x64]
|
||||
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':
|
||||
resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==}
|
||||
|
||||
@@ -985,6 +1063,19 @@ packages:
|
||||
cpu: [x64]
|
||||
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':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
@@ -1005,6 +1096,17 @@ packages:
|
||||
ajv@8.20.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -1012,6 +1114,9 @@ packages:
|
||||
avvio@9.3.0:
|
||||
resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==}
|
||||
|
||||
bidi-js@1.1.0:
|
||||
resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
@@ -1019,10 +1124,24 @@ packages:
|
||||
resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
dom-accessibility-api@0.5.16:
|
||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||
|
||||
drizzle-kit@0.31.10:
|
||||
resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
|
||||
hasBin: true
|
||||
@@ -1125,6 +1244,10 @@ packages:
|
||||
end-of-stream@1.4.5:
|
||||
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:
|
||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1193,6 +1316,10 @@ packages:
|
||||
get-tsconfig@4.14.3:
|
||||
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:
|
||||
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1205,6 +1332,21 @@ packages:
|
||||
resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==}
|
||||
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:
|
||||
resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
|
||||
|
||||
@@ -1219,6 +1361,17 @@ packages:
|
||||
engines: {node: '>=22.22.1'}
|
||||
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:
|
||||
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -1244,6 +1397,9 @@ packages:
|
||||
vite-plus:
|
||||
optional: true
|
||||
|
||||
parse5@8.0.1:
|
||||
resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1269,15 +1425,26 @@ packages:
|
||||
resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==}
|
||||
|
||||
process-warning@5.1.0:
|
||||
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:
|
||||
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:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1323,6 +1490,10 @@ packages:
|
||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
saxes@6.0.0:
|
||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||
engines: {node: '>=v12.22.7'}
|
||||
|
||||
secure-json-parse@4.1.0:
|
||||
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
|
||||
|
||||
@@ -1362,6 +1533,9 @@ packages:
|
||||
string_decoder@1.3.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -1378,10 +1552,25 @@ packages:
|
||||
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -1395,6 +1584,10 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -1438,6 +1631,26 @@ packages:
|
||||
yaml:
|
||||
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:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
@@ -1453,6 +1666,13 @@ packages:
|
||||
utf-8-validate:
|
||||
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:
|
||||
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
||||
engines: {node: '>= 14.6'}
|
||||
@@ -1460,6 +1680,59 @@ packages:
|
||||
|
||||
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': {}
|
||||
|
||||
'@esbuild-kit/core-utils@3.3.2':
|
||||
@@ -1769,6 +2042,8 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@exodus/bytes@1.15.1': {}
|
||||
|
||||
'@fastify/ajv-compiler@4.0.6':
|
||||
dependencies:
|
||||
ajv: 8.20.0
|
||||
@@ -1938,6 +2213,23 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.62.5':
|
||||
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/node@22.20.1':
|
||||
@@ -1957,6 +2249,14 @@ snapshots:
|
||||
json-schema-traverse: 1.0.0
|
||||
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: {}
|
||||
|
||||
avvio@9.3.0:
|
||||
@@ -1964,12 +2264,32 @@ snapshots:
|
||||
'@fastify/error': 4.2.0
|
||||
fastq: 1.20.1
|
||||
|
||||
bidi-js@1.1.0:
|
||||
dependencies:
|
||||
require-from-string: 2.0.2
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
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: {}
|
||||
|
||||
dom-accessibility-api@0.5.16: {}
|
||||
|
||||
drizzle-kit@0.31.10:
|
||||
dependencies:
|
||||
'@drizzle-team/brocli': 0.10.2
|
||||
@@ -1992,6 +2312,8 @@ snapshots:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
entities@8.1.0: {}
|
||||
|
||||
esbuild@0.18.20:
|
||||
optionalDependencies:
|
||||
'@esbuild/android-arm': 0.18.20
|
||||
@@ -2165,12 +2487,48 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
@@ -2191,6 +2549,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
yaml: 2.9.0
|
||||
|
||||
lru-cache@11.5.2: {}
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
mdn-data@2.27.1: {}
|
||||
|
||||
nanoid@3.3.18: {}
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
@@ -2223,6 +2587,10 @@ snapshots:
|
||||
'@oxfmt/binding-win32-ia32-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: {}
|
||||
|
||||
picomatch@4.0.5: {}
|
||||
@@ -2255,12 +2623,22 @@ snapshots:
|
||||
|
||||
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@5.1.0: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
@@ -2321,6 +2699,10 @@ snapshots:
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
saxes@6.0.0:
|
||||
dependencies:
|
||||
xmlchars: 2.2.0
|
||||
|
||||
secure-json-parse@4.1.0: {}
|
||||
|
||||
semver@7.8.5: {}
|
||||
@@ -2350,6 +2732,8 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
thread-stream@4.2.0:
|
||||
dependencies:
|
||||
real-require: 1.0.0
|
||||
@@ -2363,8 +2747,22 @@ snapshots:
|
||||
|
||||
tinypool@2.1.0: {}
|
||||
|
||||
tldts-core@7.4.12: {}
|
||||
|
||||
tldts@7.4.12:
|
||||
dependencies:
|
||||
tldts-core: 7.4.12
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
esbuild: 0.28.2
|
||||
@@ -2375,6 +2773,8 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici@8.10.2: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
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
|
||||
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: {}
|
||||
|
||||
ws@8.21.3: {}
|
||||
|
||||
xml-name-validator@5.0.0: {}
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
yaml@2.9.0:
|
||||
optional: true
|
||||
|
||||
Reference in New Issue
Block a user