diff --git a/.gitignore b/.gitignore index 427d063..88c351e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ /cmake-build*/ /.playwright-cli/ +/Kernel/Renderive_线程模型说明.md +/Kernel/Renderive_线程问题审计.md +/webapp_gallery/node_modules/ +/webapp_gallery/dist/ diff --git a/b.md b/b.md deleted file mode 100644 index fe9855e..0000000 --- a/b.md +++ /dev/null @@ -1,2338 +0,0 @@ -按这一版代码继续执行,不改 DAG 主体设计。 **后端只处理前面确认的 1~9,原来的第 10 项完全略过;同时重写 `Kernel/threading.md` -;前端整体迁移到 React + Vite + Material UI,DAG 使用 React Flow,但不使用 ELK、Dagre -或其他自动布局库,拓扑解析和布局算法自己实现。** - -前端职责明确分开:Material UI 负责页面、菜单、表单、Tabs、Dialog、Card、Table、Tooltip、Chip、Button、Input、Select、Accordion 等全部普通 -UI;React Flow 只作为 DAG 画布和节点/边交互层。React Flow 本身已经提供节点/边、缩放、平移、选择、自定义节点等基础能力,节点又直接是 -React component,适合和 MUI 组合; **拓扑层级、节点坐标、折叠和时序映射全部由我们自己的 parser 决定**。 ([MUI][1]) - ---- - -# Renderive Frame Publication + Lock Elimination + Web UI 执行计划 - -## 0. 最终目标 - -这轮改造完成以后,后端形成严格边界: - -```text -UI / Control / Producer - │ - ├── 修改 Renderable state - ├── 修改 RTD - ├── 修改 viewport - ├── 修改 visibility - ├── 修改 configuration - ├── rebuild Renderable graph - └── 修改 frame-control - │ - ▼ -┌──────────────────────────────┐ -│ Frame Publication Boundary │ -│ │ -│ 完成所有 mutable → immutable │ -│ snapshot / ownership 转移 │ -└──────────────────────────────┘ - │ -════════════════════════════════════ - │ 以下禁止访问 mutable - ▼ - Render Plan - │ - ▼ - ONE Taskflow - │ - ├── Prepare - ├── Paint - └── Composite - │ - ▼ - Frame Complete -``` - -双线以下必须满足: - -```text -不获取 Plot mutex -不获取 Renderable configuration mutex -不获取 Renderable graph mutex -不重新读取 Frame Control mutable state -不修改 RTD -不访问 Scene mutable API -不读取 live visibility -不持有 renderer mutex 跑完整帧 -``` - -允许的只有: - -```text -immutable Frame Snapshot -immutable Renderable Graph Snapshot -Prepare Buffer -Paint Buffer -Frame-local execution slots -Frame ownership -``` - ---- - -# 第一阶段:建立统一 `Frame_Render_Snapshot` - -这是 1~9 所有修改的基础。 - -不要针对每一个 mutex 单独打补丁。 - -新增一个明确的 Frame 级只读上下文,名称按当前项目命名规范确定,语义固定为: - -```cpp -Frame_Render_Snapshot -``` - -至少承载: - -```text -frame_id -render_sequence -scene_state_revision -viewport -frame_control_state -capture state/ticket - -per-renderable: - Renderable_Id - visibility - configuration snapshot - prepare revision - paint revision - immutable render graph -``` - -注意: - -```text -Frame_Render_Snapshot -``` - -不是把所有 Renderable 业务数据复制一次。 - -Renderable 的大块 State / RTD 继续使用已有: - -```text -published state -render state -immutable snapshot -``` - -Frame Snapshot 保存的是: - -```text -这一帧到底引用哪个 published version -``` - -以及渲染过程需要的小型 immutable metadata。 - ---- - -# 1. 删除 Paint 中的 viewport mutex - -## 当前问题 - -当前路径: - -```text -Paint Task - ↓ -Renderable::viewport_size() - ↓ -Plot_Core::viewport_size() - ↓ -Plot_Core::Impl::mutex -``` - -多个 Paint worker 会共同碰这个 mutex。 - -这是第一处直接从 worker 热路径删除的锁。 - -## 修改 - -Frame 发布时: - -```text -Plot viewport - ↓ -Frame_Render_Snapshot::viewport -``` - -Paint Context: - -```cpp -struct Paint_Render_Context { - ... - Size viewport; -}; -``` - -实际代码不要重复保存时,则引用: - -```cpp -const Frame_Render_Snapshot* frame; -``` - -Paint: - -```text -Painter - ↓ -context.frame.viewport -``` - -禁止再: - -```text -Painter - ↓ -Renderable::viewport_size() - ↓ -Plot_Core -``` - -## 旧接口语义 - -`Plot_Core::viewport_size()` 对外 API 不删除。 - -调用方普通控制线程仍然可以调用它。 - -但是: - -> Render DAG 内部不得调用它。 - -所以不是破坏旧 API,而是把渲染内部依赖切断。 - -## 测试 - -新增: - -```text -Frame snapshot 后修改 viewport -``` - -验证: - -```text -当前 Frame → 使用旧 viewport -下一 Frame → 使用新 viewport -``` - -同时保证多个并行 Paint 不进入 `Plot_Core::mutex`。 - ---- - -# 2. 拆分 Prepare / Paint / Composite Context - -这是这轮最重要的架构约束之一。 - -当前: - -```cpp -Scene_Render_Context -``` - -同时给 Prepare 和 Paint 使用,而且带: - -```cpp -Scene_Base* scene; -``` - -必须拆。 - -## 新结构 - -```text -Prepare_Render_Context -Paint_Render_Context -Composite_Render_Context -``` - -### Prepare 可以拥有 - -```text -Frame snapshot -自身 Renderable identity -prepare buffer -必要的 dependency published views -metrics -``` - -Prepare 可以解析已声明 dependency。 - ---- - -### Paint 只能拥有 - -```text -Frame immutable snapshot -自身 Prepare Buffer -自身 Paint Buffer / Color Cache -自身 paint-only immutable config -metrics -``` - -Paint Context **禁止存在**: - -```cpp -Scene_Base* scene; -Renderable_Base* dependency; -Plot_Core* plot; -Frame_Control_Strategy_Base* strategy; -``` - -这样用户就算想在 Paint 里面: - -```cpp -context.scene->xxx(); -``` - -也根本写不出来。 - ---- - -### Composite 只能拥有 - -```text -Frame snapshot -已完成 Paint Buffer -final target -layer/composite metadata -metrics -``` - -Composite 不允许重新访问 Renderable 原始状态。 - -## 执行 - -逐个修改: - -```text -Renderable -Waterfall -Spectrum -Afterglow -Axis -Overlay -其他内置 Renderable -``` - -所有: - -```text -prepare callback -paint callback -composite callback -``` - -签名。 - -旧的统一 `Scene_Render_Context` 实现直接删除。 - -不保留兼容 overload。 - ---- - -# 3. visibility 正式进入 Frame Snapshot - -当前: - -```cpp -visible_.load(...) -``` - -虽然无 mutex,但是仍然是 live state。 - -这个要改。 - -## 新语义 - -控制侧: - -```text -set_visible(false) -``` - -修改下一次 publication 使用的状态。 - -Frame 发布: - -```text -Renderable A visible = true -Renderable B visible = false -``` - -这一帧之后固定。 - -DAG 编译也直接基于这份 snapshot。 - -最好可以进一步: - -```text -visible == false -``` - -直接裁掉: - -```text -prepare -paint -composite -``` - -或者按现有缓存/图层语义裁剪所需节点。 - -## 禁止 - -Taskflow worker: - -```cpp -is_visible() -``` - -不要再 live load。 - -## 测试 - -```text -Frame A publish: visible=true - -Taskflow blocked - -control thread: -set_visible(false) - -release Taskflow -``` - -验证: - -```text -Frame A 仍绘制 -Frame B 不绘制 -``` - ---- - -# 4. `discard_real_time_data()` 移出 Prepare Worker - -这是现在 Prepare 热路径里最不应该存在的一类操作。 - -当前: - -```text -Prepare - ↓ -discard_real_time_data() - ↓ -Frame Control query - ↓ -RTD state - ↓ -RTD mutation/discard -``` - -必须取消。 - -## 正确边界 - -RTD 的: - -```text -update -discard -history maintenance -retention -``` - -属于: - -```text -producer/control/publication side -``` - -不属于 Render DAG。 - -Frame 建立之前: - -```text -RTD mutable storage - ↓ -maintenance / discard - ↓ -publish render snapshot -════════════════════════ - ↓ -Prepare 只读 -``` - -## Render DAG 内 - -Prepare 只允许: - -```text -read RTD render snapshot -``` - -禁止: - -```text -clear -discard -update -lock update storage -``` - -## Frequency - -如果 discard policy 需要: - -```text -frequency_hz -``` - -直接使用 publication 时捕获的: - -```text -frame_control_state.frequency -``` - -不要从 RTD Prepare 中再返回 Scene 查询。 - -## 测试 - -必须覆盖: - -```text -RTD update 与 Frame publication 并发 -RTD discard 与 publish -Frame publish 后 RTD 再变化 -``` - -当前 Frame 只能看到 publication 时确定的 snapshot。 - ---- - -# 5. `configuration_mutex_` 移出每帧 Render Plan 编译 - -当前 Render Plan 编译为了: - -```text -cache_enabled -prepare validity -paint validity -... -``` - -会读取: - -```cpp -configuration() -``` - -然后拿: - -```text -configuration_mutex_ -``` - -这要取消。 - -## 新设计 - -区分: - -```text -mutable/control configuration -``` - -和: - -```text -published render configuration -``` - -Scene 配置修改仍然通过当前: - -```text -set_renderable_configuration() -lock_render_idle() -``` - -保持原有外部线程安全语义。 - -Frame publication 时: - -```text -configuration - ↓ -Renderable_Frame_State.configuration -``` - -Render Plan compiler 之后只读: - -```text -snapshot.configuration -``` - -## `configuration_mutex_` - -如果控制 API 自身仍然需要 mutex,可以保留。 - -但它不能再进入: - -```text -compile_render_plan() -Taskflow -``` - -## 测试 - -同样使用 Frame A / Frame B 语义: - -```text -Frame A publication -改变 cache/config -Frame A 执行 -Frame B publication -``` - -Frame A 不允许中途改变。 - ---- - -# 6. Render Graph 改为 immutable published graph - -当前: - -```cpp -renderable->render_graph() -``` - -每帧经过: - -```text -render_graph_mutex_ -``` - -这个也清掉。 - -但不能直接删 mutex,因为当前允许: - -```text -render -|| -rebuild_render_graph() -``` - -并发。 - -因此要改 ownership,不是硬删锁。 - -## 新结构 - -Renderable 控制侧: - -```text -mutable graph builder state - │ - ▼ -rebuild - │ - ▼ -shared_ptr - │ - ▼ -atomic/publication -``` - -Frame publication: - -```text -Frame -└── shared_ptr -``` - -当前 Frame 一旦拿到: - -```text -Graph v17 -``` - -哪怕控制线程马上 rebuild: - -```text -Graph v18 -``` - -当前 Frame 仍继续读: - -```text -Graph v17 -``` - -不加锁。 - -下一 Frame 获得: - -```text -Graph v18 -``` - -## 生命周期 - -必须依靠 immutable graph ownership: - -```text -shared_ptr -``` - -或者现有项目已有等价 ownership。 - -不要重新做 generation pointer hack。 - -## `rebuild_render_graph()` - -锁只允许保护: - -```text -build/publish 新 Graph -``` - -绝不保护: - -```text -当前 Frame 读取旧 Graph -``` - -## 测试 - -保留并强化当前: - -```text -Scene render -|| -rebuild_render_graph() -``` - -测试。 - -新增验证: - -```text -Frame A = graph version 10 -rebuild → 11 -Frame A 仍然完整执行 version 10 -Frame B 使用 version 11 -``` - ---- - -# 7. Frame Control `swap + read` 合并成一次 publication - -现在类似: - -```text -swap() - ↓ -unlock - ↓ -frame_control_state() - ↓ -lock -``` - -第二次锁没有意义。 - -## 改造 - -Frame Control publication API 必须能够直接返回: - -```text -published State snapshot -``` - -语义类似: - -```cpp -State publish(); -``` - -或者符合现有命名: - -```text -swap_and_acquire_render_state() -``` - -具体函数名按现有接口风格确定。 - -但语义必须是: - -```text -lock -├── publish/swap -├── 得到当前 immutable State -└── unlock -``` - -一次完成。 - -Frame 以后只持: - -```text -State value -``` - -## 不做 - -不要为兼容保留: - -```text -swap() -+ -swap_and_get() -``` - -两套内部实现。 - -如果原接口属于公开旧语义,需要保留 public API 时: - -```text -public swap() -``` - -仍可存在。 - -但是 Renderive 内部新执行路径只使用单 publication operation。 - ---- - -# 8. `Render_Plan_History` 从 unchanged-frame 热路径移出 - -Plan History 是: - -```text -历史/分析冷路径 -``` - -不能每帧: - -```text -mutex -compare -unlock -``` - -## 新状态 - -Scene/Frame compiler 持有: - -```text -current_render_plan -``` - -同时维护能够判断 effective topology 是否变化的: - -```text -plan signature -``` - -这个 signature 是明确的结构字段组合。 - -不要使用字符串序列化。 - -例如覆盖: - -```text -active node IDs -node kind -dependency edges -composite edges -``` - -按照 canonical 顺序比较。 - -## 没变化 - -```text -reuse current shared_ptr -``` - -完全不进入 History mutex。 - -## 有变化 - -才: - -```text -new version - ↓ -new Render_Plan - ↓ -publish current - ↓ -append Render_Plan_History -``` - -History lock 只发生在: - -```text -plan version changed -``` - -这条冷路径。 - -## 注意 - -继续保持当前 `a.md` 的语义: - -> cache pruning 导致 active topology 改变,可以生成新的 Render Plan version。 - -不要因为这次锁优化重新改掉。 - ---- - -# 9. Render Lease 从“整帧持 mutex”改成短 ownership claim - -这个要非常谨慎,必须保留旧函数语义: - -> 多个 renderer caller 并发调用时,同一个 Frame 最多只能被一个 renderer 消费。 - -不能因为去锁导致重复消费。 - -## 当前 - -```text -acquire_renderer - ↓ -render_mutex LOCK - ↓ -Render Lease 整个生命周期 - ↓ -Taskflow - ↓ -wait - ↓ -Render Lease destructor - ↓ -render_mutex UNLOCK -``` - -要改成: - -```text -acquire_renderer - ↓ -短同步 ownership claim - ↓ -获得独占 Frame -════════════════════════ -下面不再持 Frame-Control mutex - ↓ -Taskflow - ↓ -complete -``` - -## ownership 状态 - -Frame 必须具有明确状态机,例如语义: - -```text -pending - ↓ -claimed - ↓ -rendering - ↓ -completed -``` - -ownership claim 可以: - -```text -短 mutex -``` - -或者符合当前数据结构时使用: - -```text -CAS -``` - -不要为了“lock-free”强行上复杂 atomic 状态机。 - -目标不是: - -```text -acquire_renderer 本身绝对无锁 -``` - -目标是: - -```text -ownership 确定以后不再持 mutex 执行整帧 -``` - -## Flow / Manual / Low Latency - -分别处理,但统一遵守: - -```text -Frame pointer/queue ownership transition -允许短同步 - -Frame 已被 renderer 独占后 -不持 mutex -``` - -不能破坏各 strategy 原来的语义。 - -## 测试 - -必须继续覆盖: - -```text -4/8 renderer callers -同一个 Frame 恰好消费一次 -无重复 -无撕裂 -无 ownership 丢失 -``` - -同时增加: - -```text -一个 renderer 正在执行长 Taskflow -另外 renderer 调用 acquire_renderer -``` - -验证: - -```text -不会因为第一个 Render Lease 持整帧 mutex -导致无关 Frame Control API 被长时间锁死 -``` - ---- - -# 原第 10 项 - -**完全略过。** - -不做: - -```text -TSan 执行计划 -性能阈值验收计划 -``` - -这轮不写进去。 - ---- - -# 11. 最终 Render Worker 的锁规则 - -完成 1~9 后,建立一条可以机械检查的规则。 - -进入: - -```text -Render Plan execution -``` - -以后,下列函数不得出现: - -```cpp -std::mutex::lock -std::recursive_mutex::lock -std::shared_mutex::lock -std::lock_guard -std::unique_lock -``` - -指的是 Kernel 自己的执行数据路径。 - -下面这种 Frame-local worker slot: - -```text -execution_slots[index] -``` - -继续直接独占写。 - -不增加锁。 - -最终应该是: - -```text -Taskflow worker -├── immutable Frame Snapshot readonly -├── immutable Render Graph readonly -├── Prepare Buffer owner-defined -├── Paint Buffer exclusive by DAG -├── final composite dependency DAG controlled -└── execution slot[index] exclusive index -``` - ---- - -# 12. 同步重写 `Kernel/threading.md` - -这次不要在现在的 `threading.md` 上简单补几行。 - -当前里面很多描述会因为此次改造失效,例如: - -```text -Double/Triple State render snapshot 都在 mutex 下读取 -render_graph_mutex_ 串行当前 render graph 访问 -Low Latency render_mutex_ 整个 Render Lease 生命周期持有 -``` - -这些都要按新模型重写。 - ---- - -# `threading.md` 必须采用下面的结构 - -## 1. 核心线程模型 - -第一段直接规定: - -```text -Renderive 的同步边界分为: - -1. Mutable Control Domain -2. Publication Domain -3. Immutable Frame Execution Domain -4. Cold Analysis Domain -``` - ---- - -## 2. Mutable Control Domain - -明确这里可以使用锁。 - -包括: - -```text -Renderable 属性修改 -Scene attach/detach -dependency/layer 修改 -RTD update -viewport 修改 -configuration 修改 -graph rebuild -Frame producer queue 操作 -``` - -这里的原则: - -```text -锁保护 mutable ownership -锁不泄漏到 Render DAG -``` - ---- - -## 3. Publication Domain - -专门说明: - -```text -publication 是 mutable → immutable 的唯一边界 -``` - -允许: - -```text -mutex -short critical section -pointer swap -shared_ptr publication -atomic ownership change -``` - -禁止: - -```text -持锁进入 Taskflow -``` - -描述: - -```text -Frame publication 完成以后, -当前 Frame 所需的数据版本必须全部固定。 -``` - ---- - -## 4. Immutable Frame Execution Domain - -这一节要写成最严格的规则。 - -明确: - -```text -Prepare/Paint/Composite worker 不获得 Kernel mutex。 -``` - -Worker 只允许: - -```text -读取 Frame Snapshot -读取 published Render State -读取 immutable Renderable Graph -访问 dependency snapshot -写自己的 Prepare Buffer -写自己的 Paint Buffer -写自己的 execution slot -``` - -禁止: - -```text -Plot_Core live getter -Scene control API -Frame Control mutable API -RTD mutation -configuration getter requiring lock -render_graph rebuild/getter requiring lock -live visibility -``` - ---- - -# 13. threading.md 增加“锁分类表” - -必须直接写表。 - -类似: - -| 锁/同步 | 所属域 | 是否允许跨 Frame Execution | 用途 | -|---------------------------|---------------------|---------------------------:|---------------------------------| -| Scene topology mutex | Control | 否 | attach/detach/topology mutation | -| State publish mutex | Publication | 否 | mutable→render state | -| RTD mutation mutex | Control | 否 | update + observer 顺序 | -| RTD publish mutex | Publication | 否 | render snapshot | -| Graph rebuild mutex | Control/Publication | 否 | 创建新 immutable graph | -| Frame queue mutex | Ownership | 否 | claim/publish frame | -| Render Plan History mutex | Cold | 不进入 worker | 保存历史版本 | -| Capture repository mutex | Cold | 不进入 worker | 保存成功 Snapshot | -| Taskflow worker data | Execution | 无锁 | immutable/exclusive data | - -这样以后审代码的时候直接对表查。 - ---- - -# 14. threading.md 增加锁顺序 - -虽然 Frame execution 不拿锁,控制侧仍然需要锁顺序。 - -必须明确规定: - -```text -Scene control - ↓ -Renderable control - ↓ -Frame control - ↓ -RTD / observer -``` - -但实际顺序需要按代码最后改完后的真实调用链重新核对后写。 - -**不能提前凭猜测写死。** - -最终 threading.md 必须列: - -```text -允许的 nested locking -禁止的 nested locking -callback 前必须释放哪些锁 -``` - -尤其强调: - -```text -用户 observer callback -用户 Renderable callback -Taskflow callback -``` - -调用前不得持有可能被 callback 重入的控制锁。 - ---- - -# 15. threading.md 增加 callback 规则 - -写明: - -```text -Observer callback 是同步 callback -``` - -但: - -```text -不得在持有 Frame ownership mutex 时调用 -不得在持有 Scene topology mutation mutex 时调用会重入 Scene 的 callback -不得在 publication mutex 下执行未知用户代码 -``` - -需要 state observation 时: - -```text -lock -生成 Observation value -unlock -callback(observation) -``` - -保持当前已经采用的正确模式。 - ---- - -# 16. threading.md 增加 Frame Ownership 章节 - -Flow / Manual / Low Latency 分别说明: - -```text -producer ownership -pending ownership -renderer claim -render ownership -completed ownership -recycle ownership -``` - -核心规则: - -> Frame ownership 转移可以同步,Frame ownership 一旦确定,Frame 内容执行不依赖互斥锁。 - -这句话作为 Frame Strategy 的总规则。 - ---- - -# 17. threading.md 增加 Snapshot Lifetime 章节 - -写清: - -```text -Frame 持有什么 shared ownership -什么时候释放 -graph version 生命周期 -RTD snapshot 生命周期 -renderable snapshot 生命周期 -``` - -特别说明: - -```text -Renderable rebuild Graph -``` - -不会使正在执行 Frame 的 Graph 失效。 - ---- - -# 18. threading.md 增加 Prepare/Paint 并发规则 - -保留现在正确的: - -```text -不同 Renderable 无依赖即可并发 -Renderable 内无 dependency edge 即可并发 -Prepare A 可以与 Paint B 并发 -``` - -并新增: - -```text -并发安全不是依赖 mutex 达成 -而是依赖: -immutable input -exclusive output -DAG dependency -``` - -这个很重要。 - ---- - -# 19. threading.md 增加“不应该看到的锁” - -直接写一个禁止清单。 - -在: - -```text -prepare callback -paint callback -composite callback -Taskflow wrapper -``` - -里面如果 Kernel 自己出现: - -```text -configuration_mutex_ -render_graph_mutex_ -Plot_Core::mutex -Frame Control state mutex -RTD mutable mutex -Render Lease mutex -``` - -视为架构错误。 - -不是“性能优化建议”。 - -是违反线程模型。 - ---- - -# 20. 前端整体废除当前手写 DOM 架构 - -当前: - -```text -webapp_gallery/ -├── index.html -├── app.js -└── styles.css -``` - -其中 `app.js` 自己维护: - -```text -GalleryCard class -DOM clone -querySelector -菜单状态 -Tabs -表单 -DAG SVG -Timeline SVG -选择状态 -``` - -这一版直接替换。 - -旧实现删除。 - -不保留: - -```text -legacy app.js -legacy page -React page -``` - -双实现。 - ---- - -# 21. 前端新技术栈 - -固定: - -```text -React -TypeScript -Vite -Material UI -@mui/icons-material -@xyflow/react -``` - -**不使用:** - -```text -ELK -elkjs -Dagre -D3 layout -Graphviz -Cytoscape -AMIS -手写 DOM UI -手写完整 DAG SVG -``` - -Material UI 是 React component library,可以把普通 UI 全部收敛为一致的组件模型。 ([MUI][1]) - -React Flow 只承担图画布能力: - -```text -node rendering -edge rendering -zoom -pan -selection -fit view -viewport -``` - -这些都是其现成能力。 ([React Flow][2]) - ---- - -# 22. DAG 布局自己解析 - -这里严格按你刚才的要求: - -**不要 ELK。** - -新增自己的纯 TypeScript parser/layout: - -```text -renderPlanToDagModel() - ↓ -layoutRenderDag() - ↓ -React Flow nodes/edges -``` - ---- - -# 23. DAG parser 输入 - -直接使用当前后端: - -```text -render_plan -├── version -├── nodes -└── edges -``` - -不要让 React component 自己理解后端 JSON。 - -首先解析成前端自己的强类型模型: - -```ts -RenderPlan -RenderNode -RenderEdge -RenderNodeKind -RenderDagModel -RenderDagLevel -``` - -所有后端 JSON 检查集中在: - -```text -protocol/ -``` - -React component 不做: - -```ts -data?.foo?.bar ?? -... -``` - -满页面 defensive parsing。 - ---- - -# 24. 自己实现 DAG level parser - -布局算法按 DAG 本身语义做。 - -第一步: - -```text -node indegree -``` - -然后 Kahn topological traversal。 - -计算: - -```text -level[node] = -max(level[parent] + 1) -``` - -无父节点: - -```text -level = 0 -``` - -得到: - -```text -Level 0 -├── A.prepare -├── B.prepare -└── C.prepare - -Level 1 -├── A.paint -└── C.paint - -Level 2 -├── ... -``` - ---- - -# 25. 同 level 节点排序 - -不能随机。 - -排序规则固定,保证同一 plan 刷新后节点不乱跳。 - -依次: - -```text -owner/renderable order -kind -logical execution order -node_id -``` - -如果前一 version 有相同 Node ID: - -```text -优先保持上一个位置 -``` - -这样: - -```text -Plan v17 -→ -Plan v18 -``` - -只新增一个 node 时,不应该全图重排。 - ---- - -# 26. DAG 布局增加 barycenter pass - -基础 level 完成之后,自己实现简单交叉减少。 - -两遍: - -```text -left → right -right → left -``` - -对每层节点根据邻接节点平均位置排序。 - -不引入第三方 layout engine。 - -复杂度保持可控。 - -如果数据量增长,最多增加几轮 deterministic pass。 - -不做无限迭代优化。 - ---- - -# 27. DAG Node 使用 MUI 组件 - -React Flow custom node 内部直接用 MUI: - -```text -Paper -Stack -Typography -Chip -Tooltip -Box -``` - -例如: - -```text -┌────────────────────────────┐ -│ Waterfall │ -│ chunk.paint.4 │ -│ │ -│ PAINT 1.82ms 23.1% │ -└────────────────────────────┘ -``` - -Node 数据: - -```text -owner -name -kind -duration -critical -cache state -worker -``` - -React Flow custom node 本身就是普通 React component,因此这种组合是自然的。 ([React Flow][3]) - ---- - -# 28. DAG Edge 不自己写 SVG Path 算法 - -这个和“自己写 parser”区分开。 - -自己写的是: - -```text -DAG topology parser -level layout -position -selection mapping -``` - -不是重新实现图形库。 - -Edge 使用 React Flow 自带: - -```text -straight -step -smoothstep -``` - -当前更适合执行 DAG 的默认使用: - -```text -smoothstep -``` - -dependency / composite 用不同: - -```text -edge data / style -``` - -表达。 - -React Flow 已经提供这些 edge 类型以及自定义 edge 能力,不需要我们重新维护点击命中、SVG 路径等基础设施。 ([React Flow][4]) - ---- - -# 29. 前端目录重构 - -改成: - -```text -webapp_gallery/ -├── index.html -├── package.json -├── tsconfig.json -├── vite.config.ts -└── src/ - ├── main.tsx - ├── App.tsx - ├── theme.ts - ├── protocol/ - │ ├── gallery.ts - │ ├── renderPlan.ts - │ └── capture.ts - ├── websocket/ - │ └── GallerySocket.ts - ├── state/ - │ └── galleryState.ts - ├── components/ - │ ├── GalleryCard.tsx - │ ├── GalleryToolbar.tsx - │ ├── ControlPanel.tsx - │ ├── ObserverPanel.tsx - │ ├── PerformancePanel.tsx - │ └── capture/ - │ ├── CapturePanel.tsx - │ ├── CaptureControls.tsx - │ ├── FrameBrowser.tsx - │ ├── RenderDag.tsx - │ ├── RenderDagNode.tsx - │ ├── WorkerTimeline.tsx - │ ├── NodeDetail.tsx - │ └── PlanComparison.tsx - └── dag/ - ├── model.ts - ├── parseRenderPlan.ts - └── layoutRenderDag.ts -``` - -不要搞一个: - -```text -utils.ts -``` - -塞几千行。 - ---- - -# 30. Gallery 主页面 Material UI 化 - -现在的: - -```text -topbar -hero -mode tabs -category filters -cards -context menu -toast -``` - -分别改成 MUI: - -```text -AppBar -Toolbar -Tabs -ToggleButtonGroup / Chip -Grid -Card -Drawer / Dialog -Snackbar -``` - -所有按钮和 input 都不再自己手写 HTML styling。 - ---- - -# 31. 原右键菜单改成 MUI Drawer - -当前性能控制区内容很多。 - -不要继续使用自己计算定位的 context menu。 - -右键 Renderable 后打开: - -```text -Drawer -``` - -PC 端右侧。 - -里面: - -```text -Tabs -├── 属性 -├── 专属 API -├── Observer -├── Performance -├── Performance Capture -└── Render DAG -``` - -保持现有功能语义。 - ---- - -# 32. 表单全部 MUI controlled component - -后端字段类型解析成: - -```text -boolean → Switch -enum → Select -number → TextField type=number -action → Button -readonly → Typography/TextField readonly -``` - -解决之前手写表单容易: - -```text -后端刷新覆盖正在输入的值 -Enter 后值恢复 -dirty state 不明确 -``` - -的问题。 - -每个字段维护: - -```text -serverValue -draftValue -dirty -submitting -error -``` - -**后端 telemetry refresh 不允许覆盖 dirty draft。** - -只有: - -```text -submit 成功 -manual reset -``` - -才同步。 - ---- - -# 33. Performance Capture 页面重新组件化 - -布局: - -```text -┌────────────────────────────────────────────────────┐ -│ Capture Controls │ -├──────────────┬─────────────────────────────────────┤ -│ Frame List │ DAG │ -│ │ │ -│ ├─────────────────────────────────────┤ -│ │ Worker Timeline │ -├──────────────┴─────────────────────────────────────┤ -│ Selected Node Detail │ -├────────────────────────────────────────────────────┤ -│ Statistics / Plan Comparison │ -└────────────────────────────────────────────────────┘ -``` - -使用 MUI: - -```text -Paper -Stack -Grid -List -ListItemButton -Tabs -Table -Chip -Tooltip -Divider -``` - ---- - -# 34. Timeline 不再手写 SVG - -Timeline 不需要另外一个图框架。 - -自己解析: - -```text -worker_id -start_offset_ns -duration_ns -``` - -然后使用: - -```text -MUI Box -``` - -做 CSS absolute positioning。 - -每一个 execution block: - -```text -Box -``` - -位置: - -```text -left = start / frame_duration -width = duration / frame_duration -``` - -Worker 一行一个: - -```text -Stack / Box -``` - -这样: - -```text -selection -hover -tooltip -critical path -``` - -全部是标准 React DOM,而不是手工 `createElementNS()`。 - ---- - -# 35. DAG 和 Timeline 共享 selection store - -当前: - -```text -selectedCaptureNodeId -``` - -保留这个概念,但放到 React state。 - -```text -selectedNodeId -selectedFrameId -selectedPlanVersion -``` - -点击 DAG: - -```text -selectedNodeId = X -``` - -Timeline 对应 block 高亮。 - -点击 Timeline: - -```text -selectedNodeId = X -``` - -DAG 对应 Node 高亮。 - -Detail 自动更新。 - -不要两套独立选择状态。 - ---- - -# 36. DAG parser 不关心 Capture - -建立: - -```ts -parseRenderPlan(plan) -``` - -只处理 topology。 - -Capture overlay 单独: - -```ts -applyFrameExecution(dag, frame) -``` - -这样同一套 DAG 可以用于: - -```text -Current Render Plan -Capture Frame -Plan Comparison -``` - -避免现在 SVG renderer 里: - -```text -plan + frame + statistics -``` - -全部搅在一起。 - ---- - -# 37. Plan version 变化的前端处理 - -利用稳定: - -```text -node_id -``` - -做增量视觉稳定。 - -当: - -```text -v17 → v18 -``` - -parser 输出: - -```text -same nodes -added nodes -removed nodes -changed edges -``` - -UI 可以: - -```text -新增节点标记 -删除节点列表 -边变化 -``` - -并尽可能保持原 Node 位置。 - -这非常适合你后面看: - -```text -Waterfall partition -cache pruning -``` - -到底怎么改变 DAG。 - ---- - -# 38. 前端不要修改后端协议语义 - -这一轮: - -```text -前端技术实现替换 -``` - -不是重新设计 Gallery protocol。 - -当前已有: - -```text -render_plan -performance_capture -node_statistics -plan statistics -frame snapshots -``` - -继续消费。 - -只有在 React 强类型化时发现: - -```text -数据本身缺关键 identity -``` - -才修改协议。 - -不要为了前端组件化随便改 C++ JSON 字段。 - ---- - -# 39. Vite 集成方式 - -`webapp_gallery` 自己作为 frontend source。 - -Vite build 输出固定到现有 Web Server 使用的静态目录。 - -路径计算全部基于: - -```text -vite.config.ts 自身目录 -``` - -不做随机输出目录。 - -不改变现有构建库的文件路径语义。 - -CMake 只负责: - -```text -调用 frontend build -复制/嵌入确定的 dist -``` - -不把前端源文件逻辑塞进 CMake。 - ---- - -# 40. 前端迁移顺序 - -严格按: - -```text -1. Vite + React + TS 能构建 -2. WebSocket protocol 强类型化 -3. Gallery 首页 -4. Pixel Canvas -5. Controls -6. Actions -7. Observer -8. Performance -9. Performance Capture -10. Render DAG -11. Worker Timeline -12. Plan Comparison -13. 删除 app.js -14. 删除旧 DOM templates -15. 删除不再使用的 styles.css -``` - -不要边迁移边留旧页面 fallback。 - -最后直接一套实现。 - ---- - -# 41. 后端实际执行顺序 - -这部分严格按依赖执行: - -```text -A. Frame_Render_Snapshot 基础结构 - ↓ -B. viewport snapshot - ↓ -C. Prepare/Paint/Composite Context 拆分 - ↓ -D. visibility snapshot - ↓ -E. RTD discard 移出 DAG - ↓ -F. configuration snapshot - ↓ -G. immutable Render Graph publication - ↓ -H. Frame Control publish/read 合并 - ↓ -I. Render Plan current/history 分离 - ↓ -J. Render Lease 短 ownership claim - ↓ -K. threading.md 重写 -``` - -原因是后面的锁删除都依赖前面的 snapshot ownership。 - -不要反过来先删 mutex。 - ---- - -# 42. 每一步测试要求 - -每一个步骤都采用: - -```text -先补对应测试 -↓ -修改实现 -↓ -跑对应测试 -↓ -跑 Kernel/render_2D/web_server 全量相关测试 -↓ -进入下一项 -``` - -不是全部改完最后才测。 - -尤其不能出现: - -```text -先把 mutex 全删掉 -然后看看有没有 crash -``` - ---- - -# 43. 最终锁审计标准 - -改完以后重新全局搜索: - -```text -mutex -lock_guard -unique_lock -shared_lock -recursive_mutex -``` - -逐个给它分类: - -```text -Control -Publication -Ownership -Cold -``` - -**不存在第五类。** - -凡是属于: - -```text -Frame Execution -``` - -的 Kernel mutex,继续修改,直到为 0。 - ---- - -# 44. 最终架构 - -最终形成: - -```text - MUTABLE DOMAIN - │ - ┌──────────────────────┼───────────────────────┐ - │ │ │ -Renderable State RTD / Config Graph Builder - │ │ │ - └──────────────────────┼───────────────────────┘ - ▼ - FRAME PUBLICATION - │ - ┌────────────┴────────────┐ - │ Frame Render Snapshot │ - │ Immutable Graphs │ - │ Published RTD Views │ - │ Render Plan │ - └────────────┬────────────┘ - │ -═══════════════════════════╪════════════════════════════ - NO KERNEL LOCKS - │ - ▼ - ONE TASKFLOW - ┌────────────┼────────────┐ - ▼ ▼ ▼ - Prepare Paint Composite - │ │ │ - └────────────┼────────────┘ - ▼ - Frame Snapshot - │ - COLD ANALYSIS DOMAIN - │ - Repository / Statistics / API - │ - ▼ - React + Material UI - │ - React Flow Render DAG - │ - Own DAG Layout Parser -``` - -## 最终验收只有一句话 - -> **控制侧允许锁,publication 允许短锁,Frame ownership 转移允许短同步;一旦 Frame 的 immutable execution snapshot 发布完成,直到 -Prepare/Paint/Composite 完成,Kernel 不再通过 mutex 获取任何渲染输入,也不持有 Frame-Control mutex 执行整帧。** - -前端则执行: - -> **所有常规 UI 使用 Material UI;DAG 使用 React Flow 的画布/节点/边能力;DAG 拓扑解析、分层、排序、交叉减少、版本位置稳定全部由 -Renderive 自己的 TypeScript parser 完成;不引入 ELK/Dagre。** ([React Flow][2]) - -这版计划不会动你已经完成的全局 DAG 核心,主要是在它外面补上最后一道真正严格的 **Frame immutable publication boundary** -。完成后,“交换以后渲染执行无锁”就不再是一种实现倾向,而会成为整个 Kernel 明文规定的线程契约。 - -[1]: https://mui.com/material-ui/?utm_source=chatgpt.com "React components that implement Material Design" -[2]: https://reactflow.dev/api-reference/react-flow?utm_source=chatgpt.com "The ReactFlow component" -[3]: https://reactflow.dev/learn/customization/custom-nodes?utm_source=chatgpt.com "Custom Nodes - React Flow" -[4]: https://reactflow.dev/examples/edges/custom-edges?utm_source=chatgpt.com "Custom Edges - React Flow" diff --git a/renderive_package.zip b/renderive_package.zip new file mode 100644 index 0000000..e39eac3 Binary files /dev/null and b/renderive_package.zip differ diff --git a/renderive_package_走偏的一版.zip b/renderive_package_走偏的一版.zip new file mode 100644 index 0000000..5adb94a Binary files /dev/null and b/renderive_package_走偏的一版.zip differ diff --git a/web_server/CMakeLists.txt b/web_server/CMakeLists.txt index d7039bd..2802de3 100644 --- a/web_server/CMakeLists.txt +++ b/web_server/CMakeLists.txt @@ -4,6 +4,76 @@ if (RENDERIVE_BUILD_TESTS) endif () rcl_add_dependency_action_targets(Renderive_Web_env ${Renderive_Web_dependencies}) set_target_properties(Renderive_Web_env PROPERTIES FOLDER Renderive_Web) +if (CMAKE_CONFIGURATION_TYPES) + set(Renderive_Web_assets_dir "${CMAKE_CURRENT_BINARY_DIR}/$/webapp_gallery") +else () + set(Renderive_Web_assets_dir "${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery") +endif () +set(Renderive_Web_node_search_paths) +if (WIN32 AND DEFINED ENV{APPDATA}) + file(GLOB Renderive_Web_clion_node_versions LIST_DIRECTORIES true + "$ENV{APPDATA}/JetBrains/CLion*/node/versions/*") + list(SORT Renderive_Web_clion_node_versions COMPARE NATURAL ORDER DESCENDING) + foreach (Renderive_Web_clion_node_version IN LISTS Renderive_Web_clion_node_versions) + if (EXISTS "${Renderive_Web_clion_node_version}/node.exe" + AND EXISTS "${Renderive_Web_clion_node_version}/npm.cmd") + list(APPEND Renderive_Web_node_search_paths "${Renderive_Web_clion_node_version}") + endif () + endforeach () +endif () +find_program(Renderive_Web_node_executable NAMES node node.exe + HINTS ${Renderive_Web_node_search_paths} REQUIRED) +if (WIN32) + find_program(Renderive_Web_npm_executable NAMES npm.cmd + HINTS ${Renderive_Web_node_search_paths} REQUIRED) +else () + find_program(Renderive_Web_npm_executable NAMES npm REQUIRED) +endif () +set(Renderive_Web_frontend_dir "${CMAKE_CURRENT_LIST_DIR}/../webapp_gallery") +set(Renderive_Web_frontend_install_stamp "${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery_npm_ci.stamp") +set(Renderive_Web_frontend_build_stamp "${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery_vite_build.stamp") +file(GLOB_RECURSE Renderive_Web_frontend_sources CONFIGURE_DEPENDS + "${Renderive_Web_frontend_dir}/src/*" +) +set(Renderive_Web_frontend_configuration + "${Renderive_Web_frontend_dir}/package.json" + "${Renderive_Web_frontend_dir}/package-lock.json" + "${Renderive_Web_frontend_dir}/vite.config.ts" + "${Renderive_Web_frontend_dir}/tsconfig.json" + "${Renderive_Web_frontend_dir}/tsconfig.app.json" + "${Renderive_Web_frontend_dir}/index.html" +) +add_custom_command( + OUTPUT "${Renderive_Web_frontend_install_stamp}" + COMMAND "${Renderive_Web_npm_executable}" ci + COMMAND "${CMAKE_COMMAND}" -E touch "${Renderive_Web_frontend_install_stamp}" + DEPENDS + "${Renderive_Web_frontend_dir}/package.json" + "${Renderive_Web_frontend_dir}/package-lock.json" + WORKING_DIRECTORY "${Renderive_Web_frontend_dir}" + COMMENT "Installing Renderive Gallery frontend dependencies" + VERBATIM +) +add_custom_command( + OUTPUT "${Renderive_Web_frontend_build_stamp}" + COMMAND "${Renderive_Web_npm_executable}" run build + COMMAND "${CMAKE_COMMAND}" -E touch "${Renderive_Web_frontend_build_stamp}" + DEPENDS "${Renderive_Web_frontend_install_stamp}" + ${Renderive_Web_frontend_configuration} + ${Renderive_Web_frontend_sources} + WORKING_DIRECTORY "${Renderive_Web_frontend_dir}" + COMMENT "Building Renderive Gallery frontend" + VERBATIM +) +add_custom_target(Renderive_Web_Assets + COMMAND "${CMAKE_COMMAND}" -E remove_directory "${Renderive_Web_assets_dir}" + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${Renderive_Web_frontend_dir}/dist" + "${Renderive_Web_assets_dir}" + DEPENDS "${Renderive_Web_frontend_build_stamp}" + COMMENT "Synchronizing Renderive Gallery frontend assets" + VERBATIM +) library_is_installed_with_rely(Renderive_Web_dependencies_installed ${Renderive_Web_dependencies}) if (NOT Renderive_Web_dependencies_installed) rcl_log_append("[FATAL_ERROR] Renderive_Web dependencies are not installed. Build Renderive_Web_env first") @@ -54,17 +124,6 @@ append_glob_source(Renderive_Web_Server_sources "${Renderive_Web_Server_source_d add_executable(Renderive_Web_Server ${Renderive_Web_Server_sources}) target_compile_features(Renderive_Web_Server PRIVATE cxx_std_20) target_link_libraries(Renderive_Web_Server PRIVATE Renderive_Web) -if (CMAKE_CONFIGURATION_TYPES) - set(Renderive_Web_assets_dir "${CMAKE_CURRENT_BINARY_DIR}/$/webapp_gallery") -else () - set(Renderive_Web_assets_dir "${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery") -endif () -add_custom_target(Renderive_Web_Assets - COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${CMAKE_CURRENT_LIST_DIR}/../webapp_gallery" - "${Renderive_Web_assets_dir}" - COMMENT "Synchronizing Renderive Web assets" -) add_dependencies(Renderive_Web_Server Renderive_Web_Assets) if (MSVC) target_compile_options(Renderive_Web PRIVATE /utf-8) diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js deleted file mode 100644 index a580ab5..0000000 --- a/webapp_gallery/app.js +++ /dev/null @@ -1,1460 +0,0 @@ -"use strict"; - -const $ = id => document.getElementById(id); -const elements = { - pages: $("pages"), pageTemplate: $("page-template"), cardTemplate: $("card-template"), - connection: $("connection-status"), filters: $("category-filter"), modeTabs: $("mode-tabs"), - streamToggle: $("toggle-streams"), pageCount: $("page-count"), caseCount: $("case-count"), - canvasCount: $("canvas-count"), apiCount: $("api-count"), modeDescription: $("mode-description"), - heroEyebrow: $("hero-eyebrow"), heroTitle: $("hero-title"), - menu: $("context-menu"), menuTitle: $("menu-title"), menuComponent: $("menu-component"), - menuDescription: $("menu-description"), menuBody: $("menu-body"), menuStatus: $("menu-status"), - menuClose: $("menu-close"), menuReset: $("menu-reset"), menuRefresh: $("menu-refresh"), - menuTabs: [...document.querySelectorAll(".menu-tabs button")], toast: $("toast") -}; - -const query = new URLSearchParams(location.search); -const hosted = location.protocol !== "file:" && ["/", "/index.html", "/gallery", "/gallery/"].includes(location.pathname); -const jetBrainsPreview = location.port === "63342"; -const socketPort = query.get("port") || (hosted && !jetBrainsPreview ? location.port : "8848") || "8848"; -const socketHost = query.get("host") || (hosted ? location.hostname : "127.0.0.1") || "127.0.0.1"; -const socketUrl = `${location.protocol === "https:" ? "wss" : "ws"}://${socketHost}:${socketPort}/renderive/gallery`; - -const pages = new Map(); -let definitions = []; -let modes = []; -let navigation = {}; -let dashboard = {}; -let activeMode = ""; -let activeCategory = ""; -let activeCard = null; -let activeTab = "controls"; -let streamsPaused = false; -let toastTimer = 0; -let lastAnimationFrameAt = 0; -const displayIntervalSamples = []; - -const message = (type, payload = {}) => JSON.stringify({category: "event", type, ...payload}); -const setConnection = (state, text) => { - elements.connection.dataset.state = state; - elements.connection.querySelector("span").textContent = text; -}; -function toast(text, error = false) { - clearTimeout(toastTimer); - elements.toast.textContent = text; - elements.toast.dataset.error = String(error); - elements.toast.hidden = false; - toastTimer = setTimeout(() => { elements.toast.hidden = true; }, 2600); -} -function grouped(items) { - const groups = new Map(); - for (const item of items || []) { - const group = item.group || "其他"; - if (!groups.has(group)) groups.set(group, []); - groups.get(group).push(item); - } - return groups; -} -function adminiveFieldVisible(expression, data) { - if (!expression) return true; - const equality = expression.match(/^\$\{\$self\.([A-Za-z_][A-Za-z0-9_]*) == '([^']*)'}$/); - return equality ? String(data?.[equality[1]]) === equality[2] : true; -} -function adminiveControls(resource) { - const resources = resource?.resources || (resource ? [resource] : []); - const controls = []; - const visit = (fields, data, target, group, path = [], labels = []) => { - for (const field of fields || []) { - const presentation = field.presentation || {}; - if (!adminiveFieldVisible(presentation.visible_on, data)) continue; - const fieldPath = [...path, field.name]; - const fieldLabels = [...labels, presentation.label || field.name]; - if (field.children?.length) { - visit(field.children, data?.[field.name], target, group, fieldPath, fieldLabels); - continue; - } - if (!field.editable) continue; - controls.push({ - id: fieldPath.join("."), - target, - path: fieldPath, - label: fieldLabels.join(" / "), - api: fieldPath.join("."), - description: presentation.description || "", - group, - input: presentation.control === "automatic" ? "text" : presentation.control || "text", - minimum: field.minimum, - maximum: field.maximum, - step: field.multiple_of, - options: presentation.options || [], - value: data?.[field.name] - }); - } - }; - for (const item of resources) { - const group = item?.view?.title || item?.descriptor?.label || "控件属性"; - visit(item?.descriptor?.fields, item?.data || {}, item?.target, group); - } - return controls; -} -function nestedPatch(path, value) { - return [...path].reverse().reduce((result, key) => ({[key]: result}), value); -} -function descriptorValue(field, value) { - const options = field.presentation?.options || []; - const option = options.find(item => String(item.value) === String(value)); - if (option) return option.label; - if (typeof value === "boolean") return value ? "启用" : "关闭"; - if (value === null || value === undefined) return "—"; - if (field.name?.endsWith("_ns")) return formatNanoseconds(value); - if (typeof value === "number") return value.toLocaleString(); - if (typeof value === "object") return JSON.stringify(value); - return String(value); -} - -function descriptorRows(descriptor, data, fieldNames = null) { - const selected = fieldNames ? new Set(fieldNames) : null; - const rows = []; - const visit = (fields, value, labels = []) => { - for (const field of fields || []) { - const presentation = field.presentation || {}; - if (!adminiveFieldVisible(presentation.visible_on, value)) continue; - const fieldLabels = [...labels, presentation.label || field.name]; - if (field.children?.length) { - visit(field.children, value?.[field.name], fieldLabels); - continue; - } - if (!selected || selected.has(field.name)) - rows.push({label: fieldLabels.join(" / "), value: descriptorValue(field, value?.[field.name])}); - } - }; - visit(descriptor?.fields, data || {}); - return rows; -} -function formatNanoseconds(value) { - const nanoseconds = Math.max(0, Number(value) || 0); - if (nanoseconds < 1_000) return `${Math.round(nanoseconds)} ns`; - if (nanoseconds < 1_000_000) return `${(nanoseconds / 1_000).toFixed(2)} µs`; - return `${(nanoseconds / 1_000_000).toFixed(3)} ms`; -} -function valueAtPath(root, path) { - return path.split(".").reduce((value, key) => value?.[key], root); -} -function formatDashboardField(field, telemetry) { - const format = field.format || "text"; - const raw = valueAtPath(telemetry, field.source || "") ?? field.default ?? (format === "text" ? "" : 0); - const number = Number(raw) || 0; - const digits = field.digits ?? 0; - const valueMap = dashboard.value_maps?.[field.value_map] || {}; - if (format === "fixed") return {text: number.toFixed(digits), title: String(number)}; - if (format === "integer") return {text: number.toLocaleString(), title: String(number)}; - if (format === "milliseconds") return {text: `${number.toFixed(digits)} ms`, title: `${number} ms`}; - if (format === "fps") return {text: `${number.toFixed(digits)} FPS`, title: `${number} FPS`}; - if (format === "bytes") return {text: `${number.toLocaleString()} B`, title: `${number} B`}; - if (format === "nanoseconds") return {text: formatNanoseconds(number), title: `${number.toLocaleString()} ns`}; - if (format === "frequency") { - if (valueAtPath(telemetry, field.enabled_source) === false) - return {text: field.disabled_label, title: field.disabled_label}; - return {text: `${number.toLocaleString()} Hz`, title: `${number} Hz`}; - } - if (format === "inverse_fps") { - const fps = number > 0 ? 1e9 / number : 0; - return {text: `${fps.toFixed(digits)} FPS`, title: `${fps} FPS`}; - } - if (format === "pair") { - const values = field.sources.map(source => Number(valueAtPath(telemetry, source) || 0).toLocaleString()); - return {text: values.join(field.separator || " / "), title: values.join(field.separator || " / ")}; - } - if (format === "enum" || format === "duration_enum") { - const key = String(raw); - const label = valueMap[key] || key; - const durationSource = field.duration_sources?.[key]; - const duration = durationSource === undefined ? undefined : valueAtPath(telemetry, durationSource); - return duration === undefined ? {text: label, title: key} : - {text: `${label} · ${formatNanoseconds(duration)}`, title: `${key} · ${Number(duration || 0).toLocaleString()} ns`}; - } - if (format === "flags") { - const text = String(raw).split(field.separator || "+").map(value => valueMap[value] || value).join(field.joiner || " + "); - return {text, title: String(raw)}; - } - return {text: String(raw), title: String(raw)}; -} -function rollingStatistics(values) { - if (!values.length) return {average: 0, deviation: 0, p50: 0, p95: 0, p99: 0}; - const sorted = [...values].sort((left, right) => left - right); - const average = sorted.reduce((sum, value) => sum + value, 0) / sorted.length; - const deviation = Math.sqrt(sorted.reduce((sum, value) => { - const difference = value - average; - return sum + difference * difference; - }, 0) / sorted.length); - const percentile = ratio => sorted[Math.max(0, Math.ceil(ratio * sorted.length) - 1)]; - return {average, deviation, p50: percentile(0.5), p95: percentile(0.95), p99: percentile(0.99)}; -} -function resetDisplayTiming() { - lastAnimationFrameAt = 0; - displayIntervalSamples.length = 0; -} -function updateDisplayTiming(time) { - if (lastAnimationFrameAt > 0) { - displayIntervalSamples.push({ - time, - interval: Math.max(0, time - lastAnimationFrameAt) - }); - } - lastAnimationFrameAt = time; - while (displayIntervalSamples.length && displayIntervalSamples[0].time < time - 10_000) displayIntervalSamples.shift(); -} - -class GalleryCard { - constructor(definition, mode) { - this.definition = definition; - this.mode = mode; - this.controls = []; - this.actions = []; - this.observers = []; - this.renderPlan = null; - this.performanceCapture = {controller: {}, sessions: [], plans: []}; - this.selectedCaptureSessionId = null; - this.selectedCaptureFrameId = null; - this.selectedCaptureNodeId = null; - this.menuGroupState = new Map(); - this.menuRequestPending = false; - this.telemetry = {}; - this.frameCount = 0; - this.framePending = false; - this.frameTimeout = null; - this.frameTimeoutCount = 0; - this.latestPixelBuffer = null; - this.transportTimes = []; - this.presentationTimes = []; - this.lastPixelSignature = null; - this.changedPixelFrames = 0; - this.duplicatePixelFrames = 0; - this.lastPixelReceivedAt = 0; - this.lastPixelChangeAt = 0; - this.motionState = "waiting"; - this.reconnectTimer = null; - this.disposed = false; - this.ready = false; - this.intersecting = false; - this.backendActive = null; - this.frameRequestStartedAt = 0; - this.frameRoundTripSamples = []; - this.lastTelemetryRequest = 0; - this.overwrittenPixelFrames = 0; - this.node = elements.cardTemplate.content.firstElementChild.cloneNode(true); - this.node.dataset.category = definition.category; - this.node.dataset.mode = mode.id; - this.canvas = this.node.querySelector("canvas"); - this.shell = this.node.querySelector(".canvas-shell"); - this.context = this.canvas.getContext("2d", {alpha: false}); - this.socketState = this.node.querySelector(".card-socket"); - this.motionStatus = this.node.querySelector(".motion-status"); - this.dashboardBindings = []; - this.limitBindings = []; - this.frameLabel = this.node.querySelector(".card-frames"); - this.node.dataset.observerVisible = String(mode.observer_visible); - this.node.style.setProperty("--mode-accent", mode.accent); - this.node.querySelector(".card-category").textContent = definition.category; - this.node.querySelector(".card-title").textContent = definition.title; - this.node.querySelector(".card-description").textContent = definition.description; - this.node.querySelector(".card-component").textContent = definition.component; - this.node.querySelector(".card-controls").textContent = definition.control_count_by_mode?.[mode.id] ?? "—"; - this.node.querySelector(".card-actions").textContent = definition.action_count_by_mode?.[mode.id] ?? "—"; - const frameButton = this.node.querySelector(".frame-button"); - frameButton.textContent = mode.frame_button_label; - frameButton.addEventListener("click", () => this.requestFrame(performance.now(), true)); - this.node.querySelector(".open-menu").addEventListener("click", event => { - const rect = event.currentTarget.getBoundingClientRect(); - openMenu(this, rect.right, rect.bottom); - }); - this.node.addEventListener("contextmenu", event => { - event.preventDefault(); - openMenu(this, event.clientX, event.clientY); - }); - this.installCanvasEvents(); - this.resizeObserver = new ResizeObserver(() => this.resize()); - this.resizeObserver.observe(this.shell); - this.intersectionObserver = new IntersectionObserver(entries => { - const intersecting = entries[0]?.isIntersecting === true; - if (this.intersecting === intersecting) return; - this.intersecting = intersecting; - this.syncActivity(); - }, {rootMargin: "160px"}); - this.intersectionObserver.observe(this.node); - this.installDashboard(); - } - createDashboardCell(field, valueTag, labelTag, valueFirst) { - const cell = document.createElement("div"); - if (field.cell_class) cell.className = field.cell_class; - cell.dataset.format = field.format; - const value = document.createElement(valueTag); - const label = document.createElement(labelTag); - label.textContent = field.label; - cell.append(...(valueFirst ? [value, label] : [label, value])); - this.dashboardBindings.push({field, node: value}); - return cell; - } - createDashboardValue(field, tagName) { - const node = document.createElement(tagName); - this.dashboardBindings.push({field, node}); - return node; - } - installDashboard() { - const performanceStrip = this.node.querySelector(".performance-strip"); - performanceStrip.replaceChildren(...dashboard.performance.fields.map(field => - this.createDashboardCell(field, "dt", "dd", true))); - const limitFlags = this.node.querySelector(".limit-flags"); - limitFlags.ariaLabel = dashboard.limits.aria_label; - const limitTitle = document.createElement("strong"); - limitTitle.textContent = dashboard.limits.title; - const limitNodes = dashboard.limits.fields.map(field => { - const node = document.createElement("span"); - const value = document.createElement("b"); - node.append(`${field.label}:`, value); - this.limitBindings.push({field, node, value}); - return node; - }); - limitFlags.replaceChildren(limitTitle, ...limitNodes); - const observerPanel = this.node.querySelector(".kernel-observer-panel"); - observerPanel.ariaLabel = dashboard.observer.aria_label; - const headerContract = dashboard.observer.header; - const header = document.createElement("header"); - const identity = document.createElement("div"); - const name = document.createElement("span"); - name.append(`${headerContract.prefix} `, this.createDashboardValue(headerContract.mode, "b"), ` ${headerContract.suffix}`); - identity.append(name, this.createDashboardValue(headerContract.limit, "strong")); - const event = document.createElement("div"); - event.append(`${headerContract.event_label} `, this.createDashboardValue(headerContract.event, "b")); - header.append(identity, event); - const sections = dashboard.observer.sections.map(sectionContract => { - const section = document.createElement("div"); - section.className = sectionContract.class_name; - if (sectionContract.aria_label) section.ariaLabel = sectionContract.aria_label; - section.replaceChildren(...sectionContract.fields.map(field => - this.createDashboardCell(field, "b", "small", false))); - return section; - }); - observerPanel.replaceChildren(header, ...sections); - } - setSocketState(state, text) { - this.socketState.dataset.state = state; - this.socketState.querySelector("span").textContent = text; - } - categoryVisible() { - return activeCategory === navigation.all_categories_label || this.definition.category === activeCategory; - } - displayVisible() { - return !document.hidden && this.mode.id === activeMode && this.categoryVisible() && this.intersecting; - } - streamActive() { - return !streamsPaused && this.displayVisible(); - } - syncActivity(force = false) { - const visible = this.displayVisible(); - if (visible && ![WebSocket.OPEN, WebSocket.CONNECTING].includes(this.socket?.readyState)) { - this.connect(); - return; - } - const active = !streamsPaused && visible; - if (this.ready && (force || this.backendActive !== active)) { - this.send(active ? "show" : "hide"); - this.backendActive = active; - } - if (active) this.requestFrame(performance.now()); - } - connect() { - if (this.disposed || [WebSocket.OPEN, WebSocket.CONNECTING].includes(this.socket?.readyState)) return; - clearTimeout(this.reconnectTimer); - const socket = new WebSocket(socketUrl); - this.socket = socket; - socket.binaryType = "arraybuffer"; - socket.addEventListener("open", () => { - if (this.socket !== socket) return; - this.setSocketState("ready", "WS 已连接"); - this.send("gallery_open", {case: this.definition.id, frame_mode: this.mode.id}); - this.resize(); - }); - socket.addEventListener("message", event => { - if (this.socket === socket) - typeof event.data === "string" ? this.receiveJson(event.data) : this.receivePixels(event.data); - }); - socket.addEventListener("close", () => { - if (this.socket !== socket) return; - this.ready = false; - this.backendActive = null; - this.framePending = false; - this.menuRequestPending = false; - if (activeCard === this) setMenuRequestState(false); - clearTimeout(this.frameTimeout); this.frameTimeout = null; - this.setSocketState("error", this.displayVisible() ? "连接关闭 · 自动重连" : "连接关闭 · 等待可见"); - this.updateMotionStatus(); - if (!this.disposed && this.displayVisible()) this.reconnectTimer = setTimeout(() => this.connect(), 1000); - }); - socket.addEventListener("error", () => { - if (this.socket === socket) this.setSocketState("error", "连接错误 · 自动重连"); - }); - } - send(type, payload = {}) { - if (this.socket?.readyState === WebSocket.OPEN) this.socket.send(message(type, payload)); - } - receiveJson(raw) { - let data; - try { data = JSON.parse(raw); } catch { toast(`${this.definition.title} 返回无效 JSON`, true); return; } - if (data.type === "error") { - const detail = Object.values(data.field_errors || {})[0] || data.message; - this.menuRequestPending = false; - if (activeCard === this) setMenuRequestState(false); - toast(detail || "后端拒绝操作", true); - if (activeCard === this) elements.menuStatus.textContent = detail; - return; - } - if (data.type === "observer_state") { - this.telemetry = data.telemetry || {}; - this.observers = this.telemetry.renderable_observers || this.observers; - this.performanceCapture = this.telemetry.performance_capture || - this.performanceCapture; - this.updateDashboard(); - if (activeCard === this && activeTab === "performance_capture") - renderPerformanceCaptureMenu(); - return; - } - const manualRefresh = data.type === "refresh_state"; - if (data.type !== "case_state" && !manualRefresh) return; - this.controls = adminiveControls(data.controls); - this.actions = data.actions?.data || []; - this.telemetry = data.telemetry || {}; - this.observers = this.telemetry.renderable_observers || data.controls?.observers || []; - this.renderPlan = data.controls?.render_plan || null; - this.performanceCapture = data.controls?.performance_capture || - this.telemetry.performance_capture || this.performanceCapture; - this.ready = true; - this.backendActive = null; - this.node.dataset.ready = "true"; - this.syncActivity(); - this.node.querySelector(".card-controls").textContent = this.controls.length; - this.node.querySelector(".card-actions").textContent = this.actions.length; - this.setSocketState("ready", `${data.frame_mode?.strategy || "Core2"} 在线`); - this.updateDashboard(); - if (data.notice && !data.notice.includes("已创建")) toast(`${this.definition.title}:${data.notice}`); - if (manualRefresh) { - this.menuRequestPending = false; - if (activeCard === this) { - rememberMenuGroups(); - renderMenuBody(); - setMenuRequestState(false); - elements.menuStatus.textContent = data.notice || "当前页已手动刷新"; - } - } else if (activeCard === this && data.notice && !data.notice.includes("已创建")) { - elements.menuStatus.textContent = `${data.notice};点击刷新读取当前页`; - } - } - updateDashboard() { - for (const binding of this.dashboardBindings) { - const formatted = formatDashboardField(binding.field, this.telemetry); - binding.node.textContent = formatted.text; - binding.node.title = formatted.title; - } - const currentLimit = valueAtPath(this.telemetry, dashboard.limits.current_source); - for (const binding of this.limitBindings) { - const enabled = binding.field.enabled_source === undefined || - Boolean(valueAtPath(this.telemetry, binding.field.enabled_source)); - const active = currentLimit === binding.field.active_value; - const status = enabled ? (active ? dashboard.limits.active_label : dashboard.limits.inactive_label) : - dashboard.limits.disabled_label; - const duration = valueAtPath(this.telemetry, binding.field.duration_source) || 0; - binding.node.dataset.active = String(active); - binding.value.textContent = `${status} · ${formatNanoseconds(duration)}`; - } - this.updateMotionStatus(); - } - pixelSignature(buffer, width, height, stride) { - const bytes = new Uint8Array(buffer, 16, stride * height); - let hash = (2166136261 ^ width ^ (height << 16)) >>> 0; - const sampleCount = Math.min(4096, bytes.length); - if (sampleCount <= 1) return Math.imul(hash ^ (bytes[0] || 0), 16777619) >>> 0; - for (let index = 0; index < sampleCount; index++) { - const offset = Math.floor(index * (bytes.length - 1) / (sampleCount - 1)); - hash = Math.imul(hash ^ bytes[offset], 16777619) >>> 0; - } - return hash; - } - updateMotionStatus(now = performance.now()) { - let state = "waiting", label = "等待动态帧"; - if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) { - state = "stalled"; label = "像素流断开"; - } else if (!this.streamActive()) { - state = "waiting"; label = streamsPaused && this.displayVisible() ? "像素流已暂停" : "非活动视图"; - } else if (this.lastPixelChangeAt && now - this.lastPixelChangeAt < 1500) { - state = "moving"; label = `画面变化 ${this.changedPixelFrames.toLocaleString()}`; - } else if (this.lastPixelReceivedAt && now - this.lastPixelReceivedAt < 1500) { - state = "duplicate"; label = `重复像素帧 ${this.duplicatePixelFrames.toLocaleString()}`; - } else if (this.lastPixelReceivedAt) { - state = "stalled"; label = "像素帧已停滞"; - } - if (state !== this.motionState || this.motionStatus.querySelector("span").textContent !== label) { - this.motionState = state; - this.motionStatus.dataset.state = state; - this.motionStatus.querySelector("span").textContent = label; - } - } - receivePixels(buffer) { - this.framePending = false; - if (this.frameTimeout !== null) { clearTimeout(this.frameTimeout); this.frameTimeout = null; } - if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16) { - this.setSocketState("error", "像素帧无效 · 自动重连"); - this.socket?.close(1003, "invalid pixel frame"); - return; - } - const header = new DataView(buffer, 0, 16); - const magic = String.fromCharCode(...new Uint8Array(buffer, 0, 4)); - const width = header.getUint32(4, true), height = header.getUint32(8, true), stride = header.getUint32(12, true); - if (magic !== "RVP1" || !width || !height || stride < width * 4 || buffer.byteLength < 16 + stride * height) { - this.setSocketState("error", "像素帧协议错误 · 自动重连"); - this.socket?.close(1003, "invalid pixel frame"); - return; - } - const now = performance.now(); - const signature = this.pixelSignature(buffer, width, height, stride); - if (this.lastPixelSignature === null || signature !== this.lastPixelSignature) { - this.changedPixelFrames++; - this.lastPixelChangeAt = now; - } else this.duplicatePixelFrames++; - this.lastPixelSignature = signature; - this.lastPixelReceivedAt = now; - if (this.frameRequestStartedAt > 0) { - this.frameRoundTripSamples.push({ - time: now, - value: Math.max(0, now - this.frameRequestStartedAt) - }); - this.frameRequestStartedAt = 0; - } - if (this.latestPixelBuffer !== null) this.overwrittenPixelFrames++; - this.latestPixelBuffer = buffer; - this.setSocketState("ready", `${this.mode.strategy || "Core2"} 在线`); - this.recordRate(this.transportTimes, now); - this.frameCount++; - this.frameLabel.textContent = this.frameCount.toLocaleString(); - this.updateMotionStatus(now); - if (this.mode.request_after_response) this.requestFrame(now); - } - presentLatest(time) { - const buffer = this.latestPixelBuffer; - if (!buffer) return; - this.latestPixelBuffer = null; - const header = new DataView(buffer, 0, 16); - const width = header.getUint32(4, true), height = header.getUint32(8, true), stride = header.getUint32(12, true); - if (this.canvas.width !== width || this.canvas.height !== height) { - this.canvas.width = width; - this.canvas.height = height; - } - if (stride === width * 4) { - this.context.putImageData(new ImageData(new Uint8ClampedArray(buffer, 16, width * height * 4), width, height), 0, 0); - } else { - const packed = new Uint8ClampedArray(width * height * 4), source = new Uint8Array(buffer, 16); - for (let row = 0; row < height; row++) packed.set(source.subarray(row * stride, row * stride + width * 4), row * width * 4); - this.context.putImageData(new ImageData(packed, width, height), 0, 0); - } - this.recordRate(this.presentationTimes, time); - } - currentRate(history, now) { - while (history.length && history[0] < now - 1000) history.shift(); - if (history.length < 2) return 0; - return (history.length - 1) * 1000 / Math.max(1, history[history.length - 1] - history[0]); - } - recordRate(history, now) { - history.push(now); - return this.currentRate(history, now); - } - resize() { - if (this.socket?.readyState !== WebSocket.OPEN) return; - const rect = this.shell.getBoundingClientRect(); - this.send("resize", {width: Math.max(240, Math.round(rect.width)), height: Math.max(180, Math.round(rect.height))}); - } - requestFrame(time, explicit = false) { - if ((!explicit && !this.streamActive()) || (explicit && !this.displayVisible()) || !this.ready || this.framePending || this.socket?.readyState !== WebSocket.OPEN) return; - if (!explicit && !this.mode.automatic) return; - this.frameRequestStartedAt = time; - this.framePending = true; - this.send("frame"); - this.frameTimeout = setTimeout(() => { - this.framePending = false; - this.frameRequestStartedAt = 0; - this.frameTimeout = null; - this.frameTimeoutCount++; - if (!this.streamActive() || this.socket?.readyState !== WebSocket.OPEN) return; - this.setSocketState("error", "像素响应超时 · 正在恢复"); - this.syncActivity(true); - }, 1500); - } - clientMetrics(time) { - while (this.frameRoundTripSamples.length && - this.frameRoundTripSamples[0].time < time - 10_000) - this.frameRoundTripSamples.shift(); - const roundTrip = rollingStatistics(this.frameRoundTripSamples.map(sample => sample.value)); - const display = rollingStatistics(displayIntervalSamples.map(sample => sample.interval)); - return { - transport_fps: this.currentRate(this.transportTimes, time), - presentation_fps: this.currentRate(this.presentationTimes, time), - websocket_buffered_bytes: this.socket.bufferedAmount || 0, - changed_pixel_frames: this.changedPixelFrames, - duplicate_pixel_frames: this.duplicatePixelFrames, - frame_request_timeout_count: this.frameTimeoutCount, - frame_round_trip_ms: this.frameRoundTripSamples.at(-1)?.value || 0, - frame_round_trip_average_ms: roundTrip.average, - frame_round_trip_deviation_ms: roundTrip.deviation, - frame_round_trip_p95_ms: roundTrip.p95, - frame_round_trip_p99_ms: roundTrip.p99, - display_interval_ms: display.p50, - display_interval_average_ms: display.average, - display_interval_latest_ms: displayIntervalSamples.at(-1)?.interval || 0, - display_interval_p95_ms: display.p95, - display_interval_p99_ms: display.p99, - display_interval_deviation_ms: display.deviation, - overwritten_pixel_frames: this.overwrittenPixelFrames, - last_pixel_receive_age_ms: this.lastPixelReceivedAt ? Math.max(0, time - this.lastPixelReceivedAt) : 0, - last_pixel_change_age_ms: this.lastPixelChangeAt ? Math.max(0, time - this.lastPixelChangeAt) : 0 - }; - } - refreshMenu() { - if (!this.ready || this.menuRequestPending || - this.socket?.readyState !== WebSocket.OPEN) return; - this.menuRequestPending = true; - setMenuRequestState(true); - elements.menuStatus.textContent = "正在读取当前页数据"; - this.send("gallery_refresh", {client_metrics: this.clientMetrics(performance.now())}); - } - resetMonitoring() { - if (!this.ready || this.menuRequestPending || - this.socket?.readyState !== WebSocket.OPEN) return; - this.resetClientMonitoring(); - this.menuRequestPending = true; - setMenuRequestState(true); - elements.menuStatus.textContent = "正在重置监测滑动窗口"; - this.send("gallery_reset_monitoring"); - } - resetClientMonitoring() { - this.transportTimes.length = 0; - this.presentationTimes.length = 0; - this.changedPixelFrames = 0; - this.duplicatePixelFrames = 0; - this.frameTimeoutCount = 0; - this.frameRoundTripSamples.length = 0; - this.overwrittenPixelFrames = 0; - this.frameCount = 0; - this.frameLabel.textContent = "0"; - this.lastTelemetryRequest = 0; - resetDisplayTiming(); - } - observeTelemetry(time) { - if (!this.ready || !this.displayVisible() || - this.socket?.readyState !== WebSocket.OPEN || - time - this.lastTelemetryRequest < 650) return; - this.lastTelemetryRequest = time; - this.send("gallery_observe", {client_metrics: this.clientMetrics(time)}); - } - position(event) { - const rect = this.canvas.getBoundingClientRect(); - return {x: (event.clientX - rect.left) * this.canvas.width / Math.max(1, rect.width), y: (event.clientY - rect.top) * this.canvas.height / Math.max(1, rect.height)}; - } - modifiers(event) { return (event.shiftKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.altKey ? 4 : 0) | (event.metaKey ? 8 : 0); } - installCanvasEvents() { - const pointer = (type, event) => this.send(type, {...this.position(event), button: ["left", "middle", "right"][event.button] || "none", buttons: event.buttons, modifiers: this.modifiers(event)}); - this.canvas.addEventListener("pointermove", event => pointer("pointer_move", event)); - this.canvas.addEventListener("pointerdown", event => { if (event.button !== 2) { this.shell.focus(); this.canvas.setPointerCapture(event.pointerId); pointer("pointer_press", event); } }); - this.canvas.addEventListener("pointerup", event => { if (event.button !== 2) pointer("pointer_release", event); }); - this.canvas.addEventListener("pointerleave", () => this.send("leave")); - this.canvas.addEventListener("wheel", event => { event.preventDefault(); this.send("wheel", {...this.position(event), pixelDeltaX: event.deltaX, pixelDeltaY: event.deltaY, angleDeltaX: -event.deltaX * 8, angleDeltaY: -event.deltaY * 8, buttons: event.buttons, modifiers: this.modifiers(event)}); }, {passive: false}); - this.shell.addEventListener("keydown", event => this.send("key_press", {key: event.key, nativeKey: event.keyCode, repeat: event.repeat, modifiers: this.modifiers(event)})); - this.shell.addEventListener("keyup", event => this.send("key_release", {key: event.key, nativeKey: event.keyCode, repeat: false, modifiers: this.modifiers(event)})); - } -} - -function createPage(mode) { - const page = elements.pageTemplate.content.firstElementChild.cloneNode(true); - page.dataset.mode = mode.id; - page.querySelector(".page-strategy").textContent = mode.strategy; - page.querySelector(".page-title").textContent = `${mode.title} · 全控件页`; - page.querySelector(".page-description").textContent = mode.description; - const gallery = page.querySelector(".gallery"); - const cards = definitions.map(definition => new GalleryCard(definition, mode)); - cards.forEach(card => gallery.append(card.node)); - elements.pages.append(page); - pages.set(mode.id, {mode, page, cards}); - return pages.get(mode.id); -} -function syncCardActivity() { - for (const page of pages.values()) for (const card of page.cards) card.syncActivity(); -} -function selectMode(id) { - activeMode = id; - let selected = pages.get(id); - if (!selected) selected = createPage(modes.find(mode => mode.id === id)); - for (const [modeId, page] of pages) page.page.hidden = modeId !== id; - [...elements.modeTabs.children].forEach(button => button.classList.toggle("active", button.dataset.mode === id)); - elements.modeDescription.textContent = selected.mode.description; - applyCategory(); - syncCardActivity(); - closeMenu(); -} -function applyCategory() { - const page = pages.get(activeMode); - if (!page) return; - for (const card of page.cards) { - card.node.hidden = !card.categoryVisible(); - card.syncActivity(); - } -} -function installNavigation() { - elements.modeTabs.replaceChildren(...modes.map(mode => { - const button = document.createElement("button"); - button.type = "button"; button.dataset.mode = mode.id; - button.innerHTML = `${mode.title}${mode.strategy}`; - button.addEventListener("click", () => selectMode(mode.id)); - return button; - })); - const categories = [navigation.all_categories_label, ...new Set(definitions.map(item => item.category))]; - elements.filters.replaceChildren(...categories.map(category => { - const button = document.createElement("button"); - button.type = "button"; button.textContent = category; - button.classList.toggle("active", category === activeCategory); - button.addEventListener("click", () => { - activeCategory = category; - [...elements.filters.children].forEach(child => child.classList.toggle("active", child === button)); - applyCategory(); - }); - return button; - })); -} - -function openMenu(card, x, y) { - if (!card.ready) { toast("该控件仍在等待后端描述", true); return; } - activeCard = card; activeTab = "controls"; - elements.menuComponent.textContent = `${card.mode.strategy} / ${card.definition.component}`; - elements.menuTitle.textContent = card.definition.title; - elements.menuDescription.textContent = card.definition.description; - elements.menuStatus.textContent = "菜单仅包含当前控件和当前帧策略可调用的 API"; - setMenuRequestState(card.menuRequestPending); - elements.menuTabs.forEach(button => button.classList.toggle("active", button.dataset.tab === activeTab)); - renderMenuBody(); - elements.menu.hidden = false; - const rect = elements.menu.getBoundingClientRect(), gap = 10; - elements.menu.style.left = `${Math.max(gap, Math.min(x, innerWidth - rect.width - gap))}px`; - elements.menu.style.top = `${Math.max(gap, Math.min(y, innerHeight - rect.height - gap))}px`; -} -function closeMenu() { - rememberMenuGroups(); - elements.menu.hidden = true; - activeCard = null; -} -function setMenuRequestState(pending) { - elements.menuReset.disabled = pending; - elements.menuRefresh.disabled = pending; - elements.menuReset.textContent = pending ? "处理中" : "↺ 重置监测"; - elements.menuRefresh.textContent = pending ? "读取中" : "↻ 刷新当前页"; -} -function groupStateKey(key) { return `${activeTab}:${key}`; } -function prepareGroup(section, key, defaultOpen = false) { - const stateKey = groupStateKey(key); - section.dataset.groupKey = stateKey; - section.open = activeCard?.menuGroupState.has(stateKey) - ? activeCard.menuGroupState.get(stateKey) - : defaultOpen; - section.addEventListener("toggle", () => { - activeCard?.menuGroupState.set(stateKey, section.open); - }); -} -function rememberMenuGroups() { - if (!activeCard) return; - for (const section of elements.menuBody.querySelectorAll("details[data-group-key]")) - activeCard.menuGroupState.set(section.dataset.groupKey, section.open); -} -function renderControl(item) { - const card = activeCard; - const row = document.createElement("div"); row.className = "control-row"; - const copy = document.createElement("div"); copy.className = "control-copy"; - const label = document.createElement("label"); label.textContent = item.label; - copy.append(label); - let input; - if (item.input === "select") { - input = document.createElement("select"); - for (const entry of item.options || []) { const option = document.createElement("option"); option.value = entry.value; option.textContent = entry.label; input.append(option); } - input.value = String(item.value); - } else { - input = document.createElement("input"); input.type = item.input === "boolean" ? "checkbox" : item.input; - if (item.input === "boolean") input.checked = Boolean(item.value); else input.value = item.value; - if (item.input === "number") { input.min = item.minimum; input.max = item.maximum; input.step = item.step || "any"; } - } - input.className = "control-input"; - input.setAttribute("aria-label", item.label); - input.title = item.description || item.label; - const submit = value => { - card.send("gallery_patch", {target: item.target, patch: nestedPatch(item.path, value)}); - elements.menuStatus.textContent = `提交“${item.label}”并等待后端回读`; - }; - if (item.input === "number") { - let committedValue = input.value; - const commit = () => { - const value = input.valueAsNumber; - const minimum = Number(item.minimum); - const maximum = Number(item.maximum); - if (input.value === "" || !Number.isFinite(value) || - (Number.isFinite(minimum) && value < minimum) || - (Number.isFinite(maximum) && value > maximum)) { - input.value = committedValue; - return; - } - if (value === Number(committedValue)) { - input.value = committedValue; - return; - } - committedValue = input.value; - item.value = value; - submit(value); - }; - input.addEventListener("blur", commit); - input.addEventListener("keydown", event => { - if (event.key === "Enter") { - event.preventDefault(); - commit(); - } else if (event.key === "Escape") { - event.preventDefault(); - event.stopPropagation(); - input.value = committedValue; - } - }); - } else input.addEventListener("change", () => submit(item.input === "boolean" ? input.checked : input.value)); - row.append(copy, input); return row; -} -function renderAction(item) { - const row = document.createElement("div"); row.className = "action-row"; - const copy = document.createElement("div"); copy.className = "action-copy"; - const label = document.createElement("strong"); label.textContent = item.label; - const api = document.createElement("code"); api.textContent = item.api; api.title = item.api; copy.append(label, api); - const controls = document.createElement("div"); controls.className = "action-controls"; - let argument = null; - if (item.argument_input) { argument = document.createElement("input"); argument.className = "control-input"; argument.type = item.argument_input; argument.value = item.argument_default; controls.append(argument); } - const button = document.createElement("button"); button.className = "action-button"; button.type = "button"; button.textContent = "执行"; - button.addEventListener("click", () => { - const card = activeCard; - const payload = {action: item.id}; - if (argument) payload.argument = item.argument_input === "number" ? Number(argument.value) : argument.value; - card.send("gallery_action", payload); - elements.menuStatus.textContent = `执行 ${item.api}`; - if (item.request_frame) setTimeout(() => card.requestFrame(performance.now(), true), 40); - }); - controls.append(button); row.append(copy, controls); return row; -} -function renderGroups(items, renderer) { - const fragment = document.createDocumentFragment(); - let index = 0; - for (const [name, children] of grouped(items)) { - const section = document.createElement("details"); section.className = "control-group"; - prepareGroup(section, name, index++ === 0); - const title = document.createElement("summary"); - const label = document.createElement("span"); label.textContent = name; - const count = document.createElement("small"); count.textContent = `${children.length} 项`; - title.append(label, count); - section.append(title, ...children.map(renderer)); fragment.append(section); - } - elements.menuBody.replaceChildren(fragment); -} - -function descriptorGroup(resource, fieldNames, key, open = false) { - const rows = descriptorRows(resource.descriptor, resource.data, fieldNames); - const section = document.createElement("details"); section.className = "control-group descriptor-group"; - prepareGroup(section, key, open); - const summary = document.createElement("summary"); - const title = document.createElement("span"); - title.textContent = resource.title || resource.descriptor?.label || "观察数据"; - const count = document.createElement("small"); count.textContent = `${rows.length} 项`; - summary.append(title, count); - const list = document.createElement("dl"); list.className = "telemetry-grid"; - for (const row of rows) { - const dt = document.createElement("dt"), dd = document.createElement("dd"); - dt.textContent = row.label; dd.textContent = row.value; list.append(dt, dd); - } - section.append(summary, list); - return section; -} - -function renderObserverMenu() { - const view = dashboard.menu_views.observer; - const kernel = { - ...view.kernel, - data: valueAtPath(activeCard.telemetry, view.kernel.source) || {} - }; - const groups = [descriptorGroup(kernel, null, "kernel", true)]; - activeCard.observers.forEach(resource => groups.push(descriptorGroup( - resource, view.renderable_fields, `renderable:${resource.target}`))); - elements.menuBody.replaceChildren(...groups); -} - -function renderPerformanceMenu() { - const view = dashboard.menu_views.performance; - const groups = activeCard.observers.map((resource, index) => descriptorGroup( - resource, view.renderable_fields, `renderable:${resource.target}`, index === 0)); - const resources = view.resources.map(resource => ({ - ...resource, - data: valueAtPath(activeCard.telemetry, resource.source) || {} - })); - resources.forEach(resource => groups.push(descriptorGroup( - resource, null, `aggregate:${resource.source}`))); - elements.menuBody.replaceChildren(...groups); -} - -function captureMilliseconds(value) { - return `${(Number(value || 0) / 1e6).toFixed(3)} ms`; -} - -function captureSection(title, className = "") { - const section = document.createElement("section"); - section.className = `capture-section ${className}`.trim(); - const heading = document.createElement("h3"); - heading.textContent = title; - section.append(heading); - return section; -} - -function captureMetricGrid(values) { - const grid = document.createElement("dl"); - grid.className = "capture-metrics"; - for (const [label, value] of values) { - const term = document.createElement("dt"); - const detail = document.createElement("dd"); - term.textContent = label; - detail.textContent = value; - grid.append(term, detail); - } - return grid; -} - -function selectedCaptureContext() { - const capture = activeCard.performanceCapture || {controller: {}, sessions: [], plans: []}; - const sessions = capture.sessions || []; - let session = sessions.find(item => item.session_id === activeCard.selectedCaptureSessionId); - if (!session) session = sessions.at(-1) || null; - if (session) activeCard.selectedCaptureSessionId = session.session_id; - let frame = session?.frames?.find(item => item.frame_id === activeCard.selectedCaptureFrameId); - if (!frame) frame = session?.frames?.at(-1) || null; - if (frame) activeCard.selectedCaptureFrameId = frame.frame_id; - const plan = frame - ? (capture.plans || []).find(item => item.version === frame.render_plan_version) || null - : activeCard.renderPlan; - return {capture, sessions, session, frame, plan}; -} - -function captureDagSvg(plan, frame, statistics) { - const namespace = "http://www.w3.org/2000/svg"; - const nodes = [...(plan?.nodes || [])]; - const kinds = ["prepare", "paint", "composite"]; - const byKind = new Map(kinds.map(kind => [kind, nodes.filter(node => node.kind === kind)])); - const nodeWidth = 244, nodeHeight = 88, columnGap = 42, rowGap = 24; - const positions = new Map(); - kinds.forEach((kind, column) => byKind.get(kind).forEach((node, row) => { - positions.set(String(node.id), { - x: 20 + column * (nodeWidth + columnGap), - y: 48 + row * (nodeHeight + rowGap) - }); - })); - const maximumRows = Math.max(1, ...kinds.map(kind => byKind.get(kind).length)); - const width = 20 + kinds.length * nodeWidth + (kinds.length - 1) * columnGap + 20; - const height = 58 + maximumRows * (nodeHeight + rowGap); - const svg = document.createElementNS(namespace, "svg"); - svg.classList.add("capture-dag"); - svg.setAttribute("viewBox", `0 0 ${width} ${height}`); - const executionByNode = new Map((frame?.node_executions || []).map(item => [Number(item.node_id), item])); - const analysisByNode = new Map((frame?.analysis?.nodes || []).map(item => [Number(item.node_id), item])); - const statisticsByNode = new Map((statistics || []).map(item => [Number(item.node_id), item])); - kinds.forEach((kind, column) => { - const heading = document.createElementNS(namespace, "text"); - heading.classList.add("capture-dag-heading"); - heading.setAttribute("x", 20 + column * (nodeWidth + columnGap)); - heading.setAttribute("y", 25); - heading.textContent = kind.toUpperCase(); - svg.append(heading); - }); - for (const edge of plan?.edges || []) { - const from = positions.get(String(edge.from)); - const to = positions.get(String(edge.to)); - if (!from || !to) continue; - const path = document.createElementNS(namespace, "path"); - const startX = from.x + nodeWidth; - const startY = from.y + nodeHeight / 2; - const endX = to.x; - const endY = to.y + nodeHeight / 2; - const bend = Math.max(22, Math.abs(endX - startX) * .45); - path.setAttribute("d", `M ${startX} ${startY} C ${startX + bend} ${startY}, ${endX - bend} ${endY}, ${endX} ${endY}`); - path.classList.add("capture-dag-edge"); - svg.append(path); - } - for (const node of nodes) { - const position = positions.get(String(node.id)); - const execution = executionByNode.get(Number(node.node_id)); - const analysis = analysisByNode.get(Number(node.node_id)); - const historical = statisticsByNode.get(Number(node.node_id)); - const group = document.createElementNS(namespace, "g"); - group.classList.add("capture-dag-node"); - group.dataset.kind = node.kind; - group.dataset.selected = String(Number(node.node_id) === Number(activeCard.selectedCaptureNodeId)); - group.dataset.critical = String(Boolean(analysis?.on_critical_path)); - group.setAttribute("tabindex", "0"); - const select = () => { - activeCard.selectedCaptureNodeId = Number(node.node_id); - renderPerformanceCaptureMenu(); - }; - group.addEventListener("click", select); - group.addEventListener("keydown", event => { - if (event.key === "Enter" || event.key === " ") select(); - }); - const rect = document.createElementNS(namespace, "rect"); - rect.setAttribute("x", position.x); - rect.setAttribute("y", position.y); - rect.setAttribute("width", nodeWidth); - rect.setAttribute("height", nodeHeight); - group.append(rect); - const lines = [ - node.name || node.label, - execution ? `${captureMilliseconds(execution.duration_ns)} · worker ${execution.worker_id}` : "not executed", - analysis ? `wait ${captureMilliseconds(analysis.scheduler_wait_ns)} · critical ${(analysis.critical_path_contribution * 100).toFixed(1)}%` : "wait — · critical —", - historical ? `historical p95 ${captureMilliseconds(historical.p95_ns)}` : "historical p95 —" - ]; - lines.forEach((line, index) => { - const text = document.createElementNS(namespace, "text"); - text.setAttribute("x", position.x + 10); - text.setAttribute("y", position.y + 20 + index * 19); - text.textContent = line.length > 34 ? `${line.slice(0, 33)}…` : line; - group.append(text); - }); - svg.append(group); - } - return svg; -} - -function captureTimeline(frame, plan) { - const section = captureSection("Worker Timeline", "capture-timeline-section"); - if (!frame) { - section.append("Select a captured frame to inspect worker execution."); - return section; - } - const namespace = "http://www.w3.org/2000/svg"; - const executions = (frame.node_executions || []).filter(item => item.end_offset_ns >= item.start_offset_ns); - const workers = [...new Set(executions.map(item => item.worker_id))].sort((a, b) => a - b); - const names = new Map((plan?.nodes || []).map(item => [Number(item.node_id), item.name])); - const left = 88, timelineWidth = 820, rowHeight = 42; - const duration = Math.max(1, Number(frame.render_duration_ns || 0)); - const svg = document.createElementNS(namespace, "svg"); - svg.classList.add("capture-timeline"); - svg.setAttribute("viewBox", `0 0 ${left + timelineWidth + 20} ${42 + workers.length * rowHeight}`); - for (let tick = 0; tick <= 5; ++tick) { - const x = left + timelineWidth * tick / 5; - const line = document.createElementNS(namespace, "line"); - line.setAttribute("x1", x); line.setAttribute("x2", x); - line.setAttribute("y1", 28); line.setAttribute("y2", 40 + workers.length * rowHeight); - line.classList.add("capture-time-grid"); - const label = document.createElementNS(namespace, "text"); - label.setAttribute("x", x); label.setAttribute("y", 18); - label.classList.add("capture-time-label"); - label.textContent = `${(duration * tick / 5 / 1e6).toFixed(2)} ms`; - svg.append(line, label); - } - workers.forEach((worker, row) => { - const y = 36 + row * rowHeight; - const label = document.createElementNS(namespace, "text"); - label.setAttribute("x", 6); label.setAttribute("y", y + 21); - label.classList.add("capture-worker-label"); - label.textContent = `Worker ${worker}`; - svg.append(label); - for (const execution of executions.filter(item => item.worker_id === worker)) { - const x = left + timelineWidth * Number(execution.start_offset_ns) / duration; - const width = Math.max(2, timelineWidth * Number(execution.duration_ns) / duration); - const group = document.createElementNS(namespace, "g"); - group.classList.add("capture-time-block"); - group.dataset.selected = String(Number(execution.node_id) === Number(activeCard.selectedCaptureNodeId)); - const rect = document.createElementNS(namespace, "rect"); - rect.setAttribute("x", x); rect.setAttribute("y", y); - rect.setAttribute("width", width); rect.setAttribute("height", 27); - const title = document.createElementNS(namespace, "title"); - title.textContent = `${names.get(Number(execution.node_id)) || execution.node_id} · ${captureMilliseconds(execution.duration_ns)}`; - group.append(rect, title); - group.addEventListener("click", () => { - activeCard.selectedCaptureNodeId = Number(execution.node_id); - renderPerformanceCaptureMenu(); - }); - svg.append(group); - } - }); - section.append(svg); - return section; -} - -function captureNodeDetail(frame, plan, session) { - const section = captureSection("Node Detail", "capture-node-detail"); - const nodeId = Number(activeCard.selectedCaptureNodeId); - const node = (plan?.nodes || []).find(item => Number(item.node_id) === nodeId); - const execution = (frame?.node_executions || []).find(item => Number(item.node_id) === nodeId); - const analysis = (frame?.analysis?.nodes || []).find(item => Number(item.node_id) === nodeId); - const historical = (session?.node_statistics || []).find(item => Number(item.node_id) === nodeId); - if (!node || !execution) { - section.append("Select a DAG node or timeline interval."); - return section; - } - const criticalFrequency = `${historical?.critical_path_frequency || 0} / ${session?.captured_count || 0}`; - section.append(captureMetricGrid([ - ["Node", `${node.owner} / ${node.name}`], - ["Kind", node.kind], - ["Current duration", captureMilliseconds(execution.duration_ns)], - ["Scheduler wait", captureMilliseconds(analysis?.scheduler_wait_ns)], - ["Worker", String(execution.worker_id)], - ["Critical contribution", `${((analysis?.critical_path_contribution || 0) * 100).toFixed(2)}%`], - ["Moving average", captureMilliseconds(historical?.moving_average_ns)], - ["P95", captureMilliseconds(historical?.p95_ns)], - ["P99", captureMilliseconds(historical?.p99_ns)], - ["Critical frequency", criticalFrequency] - ])); - const metrics = Object.entries(execution.metrics || {}); - if (metrics.length) { - const heading = document.createElement("h4"); - heading.textContent = "Renderable metrics"; - section.append(heading, captureMetricGrid(metrics)); - } - return section; -} - -function captureSessionStatistics(session) { - const section = captureSection("Multi-frame Statistics"); - if (!session?.captured_count) { - section.append("Statistics appear after captured frames complete."); - return section; - } - const summary = session.summary || {}; - section.append(captureMetricGrid([ - ["Frames", `${session.captured_count} / ${session.requested_count}`], - ["Total render avg", captureMilliseconds(summary.render_average_ns)], - ["Total render p50", captureMilliseconds(summary.render_p50_ns)], - ["Total render p95", captureMilliseconds(summary.render_p95_ns)], - ["Total render max", captureMilliseconds(summary.render_maximum_ns)], - ["Average parallelism", Number(summary.average_parallelism || 0).toFixed(2)], - ["Peak parallelism", String(summary.peak_parallelism || 0)], - ["Scheduler wait avg", captureMilliseconds(summary.scheduler_wait_average_ns)], - ["Scheduler wait p95", captureMilliseconds(summary.scheduler_wait_p95_ns)] - ])); - const frequency = document.createElement("div"); - frequency.className = "capture-frequency-list"; - frequency.textContent = (summary.critical_path_frequency || []) - .map(item => `#${item.node_id}: ${item.frequency}`).join(" · ") || "No critical-path samples"; - section.append(frequency); - return section; -} - -function planTopologyDifference(first, second) { - const firstNodes = new Set((first?.nodes || []).map(item => Number(item.node_id))); - const secondNodes = new Set((second?.nodes || []).map(item => Number(item.node_id))); - const edgeKey = edge => `${edge.from}>${edge.to}`; - const firstEdges = new Set((first?.edges || []).map(edgeKey)); - const secondEdges = new Set((second?.edges || []).map(edgeKey)); - return { - addedNodes: [...secondNodes].filter(value => !firstNodes.has(value)), - removedNodes: [...firstNodes].filter(value => !secondNodes.has(value)), - addedEdges: [...secondEdges].filter(value => !firstEdges.has(value)), - removedEdges: [...firstEdges].filter(value => !secondEdges.has(value)) - }; -} - -function planNodeList(ids, plan) { - const nodes = new Map((plan?.nodes || []).map(node => [Number(node.node_id), node])); - return ids.map(id => { - const node = nodes.get(Number(id)); - return node ? `#${id} ${node.owner} / ${node.name}` : `#${id}`; - }).join(", ") || "none"; -} - -function planCacheChanges(first, second) { - const before = new Map((first?.renderables || []).map(item => [Number(item.owner_id), item])); - const after = new Map((second?.renderables || []).map(item => [Number(item.owner_id), item])); - const owners = new Set([...before.keys(), ...after.keys()]); - const changes = []; - for (const owner of owners) { - const left = before.get(owner); - const right = after.get(owner); - const leftState = left ? `${left.prepare_cache}/${left.paint_cache}` : "absent"; - const rightState = right ? `${right.prepare_cache}/${right.paint_cache}` : "absent"; - if (leftState !== rightState) - changes.push(`${right?.name || left?.name || `Renderable ${owner}`}: ${leftState} -> ${rightState}`); - } - return changes.join("; ") || "unchanged"; -} - -function criticalNodeList(statistics, plan) { - const names = new Map((plan?.nodes || []).map(node => [Number(node.node_id), node.name])); - return [...(statistics?.nodes || [])] - .filter(node => Number(node.critical_path_frequency) > 0) - .sort((left, right) => right.critical_path_frequency - left.critical_path_frequency) - .map(node => `#${node.node_id} ${names.get(Number(node.node_id)) || "retired"} (${node.critical_path_frequency})`) - .join(", ") || "none"; -} - -function capturePlanComparison(context) { - const section = captureSection("Plan Version Comparison"); - const statistics = context.session?.plan_statistics || []; - if (!statistics.length) { - section.append("Capture frames to compare render plan versions."); - return section; - } - const row = document.createElement("div"); - row.className = "capture-plan-selectors"; - const makeSelect = value => { - const select = document.createElement("select"); - for (const item of statistics) { - const option = document.createElement("option"); - option.value = item.render_plan_version; - option.textContent = `Plan v${item.render_plan_version}`; - option.selected = Number(value) === Number(item.render_plan_version); - select.append(option); - } - return select; - }; - const firstDefault = activeCard.captureCompareFirst || statistics[0].render_plan_version; - const secondDefault = activeCard.captureCompareSecond || statistics.at(-1).render_plan_version; - const firstSelect = makeSelect(firstDefault), secondSelect = makeSelect(secondDefault); - const update = () => { - activeCard.captureCompareFirst = Number(firstSelect.value); - activeCard.captureCompareSecond = Number(secondSelect.value); - renderPerformanceCaptureMenu(); - }; - firstSelect.addEventListener("change", update); - secondSelect.addEventListener("change", update); - row.append(firstSelect, document.createTextNode(" versus "), secondSelect); - section.append(row); - const first = statistics.find(item => Number(item.render_plan_version) === Number(firstSelect.value)); - const second = statistics.find(item => Number(item.render_plan_version) === Number(secondSelect.value)); - const firstPlan = context.capture.plans.find(item => Number(item.version) === Number(firstSelect.value)); - const secondPlan = context.capture.plans.find(item => Number(item.version) === Number(secondSelect.value)); - const difference = planTopologyDifference(firstPlan, secondPlan); - section.append(captureMetricGrid([ - ["Frame count", `${first?.frame_count || 0} → ${second?.frame_count || 0}`], - ["Render average", `${captureMilliseconds(first?.render_average_ns)} → ${captureMilliseconds(second?.render_average_ns)}`], - ["Render p95", `${captureMilliseconds(first?.render_p95_ns)} → ${captureMilliseconds(second?.render_p95_ns)}`], - ["Average parallelism", `${Number(first?.average_parallelism || 0).toFixed(2)} → ${Number(second?.average_parallelism || 0).toFixed(2)}`], - ["Peak parallelism", `${first?.peak_parallelism || 0} → ${second?.peak_parallelism || 0}`], - ["Scheduler wait", `${captureMilliseconds(first?.scheduler_wait_average_ns)} → ${captureMilliseconds(second?.scheduler_wait_average_ns)}`], - ["First critical nodes", criticalNodeList(first, firstPlan)], - ["Second critical nodes", criticalNodeList(second, secondPlan)], - ["Added nodes", planNodeList(difference.addedNodes, secondPlan)], - ["Removed nodes", planNodeList(difference.removedNodes, firstPlan)], - ["Added edges", difference.addedEdges.join(", ") || "none"], - ["Removed edges", difference.removedEdges.join(", ") || "none"], - ["Cache pruning", planCacheChanges(firstPlan, secondPlan)] - ])); - return section; -} - -function renderPerformanceCaptureMenu() { - if (!activeCard) return; - const context = selectedCaptureContext(); - const view = document.createElement("div"); - view.className = "performance-capture-view"; - const controls = captureSection("Capture Control", "capture-controls"); - const buttons = document.createElement("div"); - buttons.className = "capture-control-row"; - const next = document.createElement("button"); - next.type = "button"; next.textContent = "Capture next frame"; - const count = document.createElement("input"); - count.type = "number"; count.min = "1"; count.max = "1000"; count.value = "20"; - const many = document.createElement("button"); - many.type = "button"; many.textContent = "Capture N frames"; - const request = (id, argument) => { - const payload = {action: id}; - if (argument !== undefined) payload.argument = argument; - activeCard.send("gallery_action", payload); - setTimeout(() => activeCard?.requestFrame(performance.now(), true), 40); - }; - next.addEventListener("click", () => request("capture_next_frame")); - many.addEventListener("click", () => request("capture_frames", Math.max(1, Number(count.value) || 20))); - const controller = context.capture.controller || {}; - next.disabled = many.disabled = Boolean(controller.enabled); - buttons.append(next, count, many); - const activeSession = context.sessions.find(item => item.session_id === controller.session_id) || - context.sessions.find(item => item.active); - const progress = document.createElement("div"); - progress.className = "capture-progress"; - const captured = activeSession?.captured_count || 0; - const requested = activeSession?.requested_count || 0; - progress.textContent = requested ? `captured ${captured} / ${requested}` : "Capture disabled"; - progress.dataset.active = String(Boolean(controller.enabled || activeSession?.active)); - controls.append(buttons, progress); - view.append(controls); - const browser = captureSection("Captured Frames", "capture-browser"); - const selectors = document.createElement("div"); - selectors.className = "capture-browser-selectors"; - const sessionSelect = document.createElement("select"); - for (const session of context.sessions) { - const option = document.createElement("option"); - option.value = session.session_id; - option.textContent = `Session #${session.session_id} · ${session.captured_count}/${session.requested_count}`; - option.selected = session === context.session; - sessionSelect.append(option); - } - sessionSelect.addEventListener("change", () => { - activeCard.selectedCaptureSessionId = Number(sessionSelect.value); - activeCard.selectedCaptureFrameId = null; - activeCard.selectedCaptureNodeId = null; - renderPerformanceCaptureMenu(); - }); - selectors.append(sessionSelect); - const frames = document.createElement("div"); - frames.className = "capture-frame-list"; - for (const frame of context.session?.frames || []) { - const button = document.createElement("button"); - button.type = "button"; - button.dataset.selected = String(frame === context.frame); - button.textContent = `Frame #${frame.frame_id} Plan v${frame.render_plan_version} ${captureMilliseconds(frame.render_duration_ns)}`; - button.addEventListener("click", () => { - activeCard.selectedCaptureFrameId = frame.frame_id; - activeCard.selectedCaptureNodeId = null; - renderPerformanceCaptureMenu(); - }); - frames.append(button); - } - if (!context.sessions.length) selectors.append("No capture sessions yet."); - browser.append(selectors, frames); - view.append(browser); - if (context.plan) { - const cache = captureSection(`Renderable Cache · Plan v${context.plan.version}`, "capture-cache"); - for (const renderable of context.plan.renderables || []) { - const badge = document.createElement("div"); - badge.className = "capture-cache-card"; - const name = document.createElement("strong"); - const prepare = document.createElement("span"); - const paint = document.createElement("span"); - name.textContent = renderable.name; - prepare.textContent = `prepare cache: ${renderable.prepare_cache}`; - paint.textContent = `paint cache: ${renderable.paint_cache}`; - badge.append(name, prepare, paint); - cache.append(badge); - } - view.append(cache); - const dag = captureSection(`Render DAG · Plan v${context.plan.version}`, "capture-dag-section"); - dag.append(captureDagSvg(context.plan, context.frame, context.session?.node_statistics)); - view.append(dag, captureTimeline(context.frame, context.plan), - captureNodeDetail(context.frame, context.plan, context.session)); - } - view.append(captureSessionStatistics(context.session), capturePlanComparison(context)); - elements.menuBody.replaceChildren(view); -} - -function renderRenderPlanMenu() { - if (!activeCard.renderPlan) { - elements.menuBody.textContent = "The scene has not compiled a render plan yet."; - return; - } - const view = document.createElement("div"); - view.className = "render-plan-view"; - const cache = captureSection(`Renderable Cache · Plan v${activeCard.renderPlan.version || 0}`, "capture-cache"); - for (const renderable of activeCard.renderPlan.renderables || []) { - const card = document.createElement("div"); - card.className = "capture-cache-card"; - const name = document.createElement("strong"); - const prepare = document.createElement("span"); - const paint = document.createElement("span"); - name.textContent = renderable.name; - prepare.textContent = `prepare cache: ${renderable.prepare_cache}`; - paint.textContent = `paint cache: ${renderable.paint_cache}`; - card.append(name, prepare, paint); - cache.append(card); - } - const dag = captureSection(`Current Render DAG · Plan v${activeCard.renderPlan.version || 0}`, "capture-dag-section"); - dag.append(captureDagSvg(activeCard.renderPlan, null, [])); - view.append(cache, dag); - elements.menuBody.replaceChildren(view); -} -function renderMenuBody() { - if (!activeCard) return; - if (activeTab === "actions") renderGroups(activeCard.actions, renderAction); - else if (activeTab === "controls") renderGroups(activeCard.controls, renderControl); - else if (activeTab === "observer") renderObserverMenu(); - else if (activeTab === "performance") renderPerformanceMenu(); - else if (activeTab === "performance_capture") renderPerformanceCaptureMenu(); - else if (activeTab === "render_plan") renderRenderPlanMenu(); -} - -function buildCatalog(data) { - definitions = [...(data.cases || [])].sort((a, b) => a.order - b.order); - modes = [...(data.frame_modes || [])].sort((a, b) => a.order - b.order); - navigation = data.navigation; - dashboard = data.dashboard; - activeMode = navigation.default_mode; - activeCategory = navigation.all_categories_label; - if (!modes.length) throw new Error("后端没有返回帧策略目录"); - elements.pages.replaceChildren(); - elements.heroEyebrow.textContent = navigation.hero_eyebrow; - elements.heroTitle.textContent = navigation.hero_title; - elements.pageCount.textContent = data.coverage?.page_count ?? modes.length; - elements.caseCount.textContent = data.coverage?.case_count ?? definitions.length; - elements.canvasCount.textContent = data.coverage?.canvas_count ?? definitions.length * modes.length; - elements.apiCount.textContent = (data.coverage?.manual_control_count || 0) + (data.coverage?.manual_action_count || 0); - installNavigation(); - if (!modes.some(mode => mode.id === activeMode)) activeMode = modes[0].id; - selectMode(activeMode); - setConnection("ready", navigation.catalog_loaded_text); -} -function connectCatalog() { - const socket = new WebSocket(socketUrl); - socket.addEventListener("open", () => socket.send(message("gallery_catalog"))); - socket.addEventListener("message", event => { - if (typeof event.data !== "string") return; - try { - const data = JSON.parse(event.data); - if (data.type === "catalog") { - buildCatalog(data); - socket.close(); - } else if (data.type === "error") { - setConnection("error", "目录读取失败"); - toast(data.message, true); - } - } catch (error) { - setConnection("error", "目录解析失败"); - toast(error.message, true); - } - }); - socket.addEventListener("error", () => { setConnection("error", `无法连接 ${socketUrl} · 自动重连`); }); - socket.addEventListener("close", () => { - if (!definitions.length) setTimeout(connectCatalog, 1000); - }); -} -function loop(time) { - updateDisplayTiming(time); - const page = pages.get(activeMode); - for (const card of page?.cards || []) { - card.presentLatest(time); - card.updateMotionStatus(time); - if (card.mode.request_on_animation_frame) card.requestFrame(time); - card.observeTelemetry(time); - } - requestAnimationFrame(loop); -} - -elements.streamToggle.addEventListener("click", () => { - streamsPaused = !streamsPaused; - elements.streamToggle.textContent = streamsPaused ? "恢复自动像素流" : "暂停自动像素流"; - syncCardActivity(); -}); -elements.menuClose.addEventListener("click", closeMenu); -elements.menuReset.addEventListener("click", () => activeCard?.resetMonitoring()); -elements.menuRefresh.addEventListener("click", () => activeCard?.refreshMenu()); -elements.menuTabs.forEach(button => button.addEventListener("click", () => { - rememberMenuGroups(); - activeTab = button.dataset.tab; - elements.menuTabs.forEach(item => item.classList.toggle("active", item === button)); - renderMenuBody(); -})); -document.addEventListener("keydown", event => { if (event.key === "Escape" && !elements.menu.hidden) closeMenu(); }); -document.addEventListener("pointerdown", event => { if (!elements.menu.hidden && !elements.menu.contains(event.target) && !event.target.closest(".open-menu")) closeMenu(); }); -document.addEventListener("visibilitychange", () => { - resetDisplayTiming(); - syncCardActivity(); -}); -window.addEventListener("beforeunload", () => { for (const page of pages.values()) for (const card of page.cards) { card.disposed = true; clearTimeout(card.frameTimeout); clearTimeout(card.reconnectTimer); card.send("hide"); card.socket?.close(); } }); - -connectCatalog(); -requestAnimationFrame(loop); diff --git a/webapp_gallery/index.html b/webapp_gallery/index.html index 6a8184c..f016955 100644 --- a/webapp_gallery/index.html +++ b/webapp_gallery/index.html @@ -3,101 +3,12 @@ - Renderive Core2 全控件性能画廊 + + Renderive Core2 性能画廊 - -
-
- -

CORE2 · KERNEL · WEBSOCKET

全控件 API 与帧策略性能画廊

-
-
- 读取后端目录 - -
-
- -
-
-

-

-

所有属性、动作、观察者和性能数据均由后端通过 WebSocket 返回。

-
-
-
帧策略页
-
每页控件
-
独立场景
-
手测入口
-
-
- - - - -
-
等待后端返回控件与帧策略目录
-
- - - - - - - - - - +
+ diff --git a/webapp_gallery/package-lock.json b/webapp_gallery/package-lock.json new file mode 100644 index 0000000..1cdfc7c --- /dev/null +++ b/webapp_gallery/package-lock.json @@ -0,0 +1,4165 @@ +{ + "name": "renderive-gallery", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "renderive-gallery", + "version": "1.0.0", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^6.4.8", + "@mui/material": "^6.4.8", + "@xyflow/react": "^12.4.4", + "elkjs": "^0.9.3", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@playwright/test": "^1.51.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^26.0.0", + "typescript": "^5.7.3", + "vite": "^6.1.0", + "vitest": "^3.0.8" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz", + "integrity": "sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.5.0.tgz", + "integrity": "sha512-VPuPqXqbBPlcVSA0BmnoE4knW4/xG6Thazo8vCLWkOKusko6DtwFV6B665MMWJ9j0KFohTIf3yx2zYtYacvG1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^6.5.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz", + "integrity": "sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/core-downloads-tracker": "^6.5.0", + "@mui/system": "^6.5.0", + "@mui/types": "~7.2.24", + "@mui/utils": "^6.4.9", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.0.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^6.5.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz", + "integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/utils": "^6.4.9", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.5.0.tgz", + "integrity": "sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.5.0.tgz", + "integrity": "sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/private-theming": "^6.4.9", + "@mui/styled-engine": "^6.5.0", + "@mui/types": "~7.2.24", + "@mui/utils": "^6.4.9", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz", + "integrity": "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/types": "~7.2.24", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xyflow/react": { + "version": "12.11.3", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.3.tgz", + "integrity": "sha512-G3jogHz2GWUtIOkhavUGno2YzY9u6fILIJBttfsBendb0/HWB90JG+sOTAvlIMEwyvq9zgy9V9ZQSwyQjR5QzQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.80", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.80", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.80.tgz", + "integrity": "sha512-ywc3ZqG91brzWrH1WlwMdIX4goOfrpBy6AbLdVSaof/Xx9l138ijIKRExM6EkMro2F+OImGmSiA/WKcXvKVcfA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.405", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", + "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "extraneous": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/webapp_gallery/package.json b/webapp_gallery/package.json new file mode 100644 index 0000000..bf2f500 --- /dev/null +++ b/webapp_gallery/package.json @@ -0,0 +1,35 @@ +{ + "name": "renderive-gallery", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "tsc -b && vite build", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^6.4.8", + "@mui/material": "^6.4.8", + "@xyflow/react": "^12.4.4", + "elkjs": "^0.9.3", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@playwright/test": "^1.51.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^26.0.0", + "typescript": "^5.7.3", + "vite": "^6.1.0", + "vitest": "^3.0.8" + } +} diff --git a/webapp_gallery/playwright.config.ts b/webapp_gallery/playwright.config.ts new file mode 100644 index 0000000..46b4ac2 --- /dev/null +++ b/webapp_gallery/playwright.config.ts @@ -0,0 +1,2 @@ +import {defineConfig,devices} from "@playwright/test"; +export default defineConfig({testDir:"./tests/e2e",use:{baseURL:"http://127.0.0.1:4173",trace:"retain-on-failure"},webServer:{command:"npm run dev -- --port 4173",url:"http://127.0.0.1:4173",reuseExistingServer:true},projects:[{name:"chromium",use:{...devices["Desktop Chrome"]}}]}); diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx new file mode 100644 index 0000000..6b04a80 --- /dev/null +++ b/webapp_gallery/src/app.tsx @@ -0,0 +1,17 @@ +import {Alert,Box,CircularProgress,Container,Stack,Typography} from "@mui/material"; +import {useEffect,useMemo,useState} from "react"; +import {use_gallery_catalog} from "./hooks/use_gallery_catalog"; +import {Gallery_Toolbar} from "./gallery/gallery_toolbar";import {Gallery_Summary} from "./gallery/gallery_summary";import {Frame_Mode_Tabs} from "./gallery/frame_mode_tabs";import {Category_Filter} from "./gallery/category_filter";import {Gallery_Page} from "./gallery/gallery_page";import type {Selected_Plot} from "./gallery/plot_card";import {Inspector_Drawer} from "./inspector/inspector_drawer"; +export function App() { + const {catalog,state:socket_state,message:socket_message}=use_gallery_catalog(); + const [active_mode,set_active_mode]=useState(""),[active_category,set_active_category]=useState(""),[streams_paused,set_streams_paused]=useState(false),[selected_plot,set_selected_plot]=useState(null),[selected_inspector_tab,set_selected_inspector_tab]=useState("controls"); + const [,set_visibility_epoch]=useState(0); + useEffect(()=>{const change=()=>set_visibility_epoch(value=>value+1);document.addEventListener("visibilitychange",change);return()=>document.removeEventListener("visibilitychange",change);},[]); + useEffect(()=>{if(catalog){set_active_mode(catalog.frame_modes.some(mode=>mode.id===catalog.navigation.default_mode)?catalog.navigation.default_mode:catalog.frame_modes[0]?.id??"");set_active_category(catalog.navigation.all_categories_label);}},[catalog]); + const modes=useMemo(()=>[...(catalog?.frame_modes??[])].sort((a,b)=>a.order-b.order),[catalog]); + const cases=useMemo(()=>[...(catalog?.cases??[])].sort((a,b)=>a.order-b.order),[catalog]); + const frame_mode=modes.find(mode=>mode.id===active_mode); + const categories=catalog?[catalog.navigation.all_categories_label,...new Set(cases.map(item=>item.category))]:[]; + const visible_cases=catalog?cases.filter(item=>active_category===catalog.navigation.all_categories_label||item.category===active_category):[]; + return <>set_streams_paused(value=>!value)}/>{!catalog&&socket_state!=="error"&&等待后端返回控件与帧策略目录}{!catalog&&socket_state==="error"&&{socket_message}}{catalog&&<>{set_active_mode(id);set_selected_plot(null);}}/>{frame_mode&&<>{frame_mode.description}}}set_selected_plot(null)}/>; +} diff --git a/webapp_gallery/src/capture/capture_frames.tsx b/webapp_gallery/src/capture/capture_frames.tsx new file mode 100644 index 0000000..1a463aa --- /dev/null +++ b/webapp_gallery/src/capture/capture_frames.tsx @@ -0,0 +1,2 @@ +import {List,ListItemButton,ListItemText} from "@mui/material";import type {Gallery_Captured_Frame} from "../protocol/gallery_types";import {format_nanoseconds} from "../protocol/format"; +export function Capture_Frames({frames,selected_frame_id,on_select}:{frames:Gallery_Captured_Frame[];selected_frame_id:number|null;on_select:(id:number)=>void}) {return {frames.map(frame=>on_select(frame.frame_id)}>)};} diff --git a/webapp_gallery/src/capture/capture_panel.tsx b/webapp_gallery/src/capture/capture_panel.tsx new file mode 100644 index 0000000..a367dd2 --- /dev/null +++ b/webapp_gallery/src/capture/capture_panel.tsx @@ -0,0 +1,14 @@ +import {Divider,Paper,Stack,Typography} from "@mui/material"; +import {useState} from "react"; +import type {Gallery_Performance_Capture} from "../protocol/gallery_types"; +import {Capture_Toolbar} from "./capture_toolbar";import {Capture_Sessions} from "./capture_sessions";import {Capture_Frames} from "./capture_frames";import {Frame_Summary} from "./frame_summary";import {Worker_Timeline} from "./worker_timeline";import {Node_Detail} from "./node_detail";import {Plan_Comparison} from "./plan_comparison";import {Render_Dag} from "../dag/render_dag"; +export function Capture_Panel({capture,on_capture}:{capture:Gallery_Performance_Capture;on_capture:(action:string,count?:number)=>void}) { + const latest_session=capture.sessions.at(-1)??null; + const [selected_capture_session,set_selected_capture_session]=useState(latest_session?.session_id??null); + const session=capture.sessions.find(item=>item.session_id===selected_capture_session)??latest_session; + const [selected_capture_frame,set_selected_capture_frame]=useState(session?.frames.at(-1)?.frame_id??null); + const frame=session?.frames.find(item=>item.frame_id===selected_capture_frame)??session?.frames.at(-1); + const plan=frame?capture.plans.find(item=>item.version===frame.render_plan_version):undefined; + const [selected_node_id,set_selected_node_id]=useState(null); + return {session&&<>{set_selected_capture_session(id);set_selected_capture_frame(null);set_selected_node_id(null);}}/>{set_selected_capture_frame(id);set_selected_node_id(null);}}/>}{session&&frame&&plan&&<>Frame SummaryRender DAGWorker TimelineNode DetailPlan Comparison}; +} diff --git a/webapp_gallery/src/capture/capture_sessions.tsx b/webapp_gallery/src/capture/capture_sessions.tsx new file mode 100644 index 0000000..4fcd665 --- /dev/null +++ b/webapp_gallery/src/capture/capture_sessions.tsx @@ -0,0 +1,2 @@ +import {MenuItem,TextField} from "@mui/material";import type {Gallery_Capture_Session} from "../protocol/gallery_types"; +export function Capture_Sessions({sessions,selected_session_id,on_select}:{sessions:Gallery_Capture_Session[];selected_session_id:number|null;on_select:(id:number)=>void}) {return on_select(Number(event.target.value))}>{sessions.map(session=>Session #{session.session_id} · {session.captured_count}/{session.requested_count})};} diff --git a/webapp_gallery/src/capture/capture_toolbar.tsx b/webapp_gallery/src/capture/capture_toolbar.tsx new file mode 100644 index 0000000..e93123f --- /dev/null +++ b/webapp_gallery/src/capture/capture_toolbar.tsx @@ -0,0 +1,2 @@ +import {Button,LinearProgress,Stack,TextField,Typography} from "@mui/material";import {useState} from "react";import type {Gallery_Performance_Capture} from "../protocol/gallery_types"; +export function Capture_Toolbar({capture,on_capture}:{capture:Gallery_Performance_Capture;on_capture:(action:string,count?:number)=>void}) {const [capture_count_draft,set_capture_count_draft]=useState("20");const active=capture.sessions.find(session=>session.session_id===capture.controller.session_id)||capture.sessions.find(session=>session.active);const requested=active?.requested_count??0,captured=active?.captured_count??0;return set_capture_count_draft(event.target.value)} sx={{width:110}}/>{requested?`captured ${captured} / ${requested}`:"Capture disabled"}{requested>0&&};} diff --git a/webapp_gallery/src/capture/frame_summary.tsx b/webapp_gallery/src/capture/frame_summary.tsx new file mode 100644 index 0000000..a97b7ab --- /dev/null +++ b/webapp_gallery/src/capture/frame_summary.tsx @@ -0,0 +1,2 @@ +import {Metric_Grid} from "../common/metric_grid";import type {Gallery_Captured_Frame} from "../protocol/gallery_types";import {format_nanoseconds} from "../protocol/format"; +export function Frame_Summary({frame}:{frame:Gallery_Captured_Frame}) {const workers=new Set(frame.node_executions.map(item=>item.worker_id));return ;} diff --git a/webapp_gallery/src/capture/node_detail.tsx b/webapp_gallery/src/capture/node_detail.tsx new file mode 100644 index 0000000..ec08593 --- /dev/null +++ b/webapp_gallery/src/capture/node_detail.tsx @@ -0,0 +1,2 @@ +import {Stack,Typography} from "@mui/material";import type {Gallery_Capture_Session,Gallery_Captured_Frame,Gallery_Render_Plan} from "../protocol/gallery_types";import {Metric_Grid} from "../common/metric_grid";import {Json_Viewer} from "../common/json_viewer";import {format_nanoseconds} from "../protocol/format"; +export function Node_Detail({frame,plan,session,selected_node_id}:{frame:Gallery_Captured_Frame;plan:Gallery_Render_Plan;session:Gallery_Capture_Session;selected_node_id:number|null}) {const node=plan.nodes.find(item=>item.node_id===selected_node_id),execution=frame.node_executions.find(item=>item.node_id===selected_node_id),analysis=frame.analysis?.nodes.find(item=>item.node_id===selected_node_id),history=session.node_statistics.find(item=>item.node_id===selected_node_id);if(!node||!execution)return 选择 DAG 节点或时间线区间查看详情。;return {execution.metrics&&};} diff --git a/webapp_gallery/src/capture/plan_comparison.tsx b/webapp_gallery/src/capture/plan_comparison.tsx new file mode 100644 index 0000000..4f86327 --- /dev/null +++ b/webapp_gallery/src/capture/plan_comparison.tsx @@ -0,0 +1,2 @@ +import {Stack,Typography} from "@mui/material";import type {Gallery_Capture_Session,Gallery_Performance_Capture} from "../protocol/gallery_types";import {Json_Viewer} from "../common/json_viewer"; +export function Plan_Comparison({capture,session}:{capture:Gallery_Performance_Capture;session:Gallery_Capture_Session}) {if(session.plan_statistics.length<2)return 捕获不同 Render Plan 版本后可比较拓扑与性能。;const first=session.plan_statistics[0],second=session.plan_statistics.at(-1)!;const first_plan=capture.plans.find(plan=>plan.version===first.render_plan_version),second_plan=capture.plans.find(plan=>plan.version===second.render_plan_version);const first_nodes=new Set(first_plan?.nodes.map(node=>node.node_id)),second_nodes=new Set(second_plan?.nodes.map(node=>node.node_id));return Plan v{first.render_plan_version} → v{second.render_plan_version}!first_nodes.has(id)),removed_nodes:[...first_nodes].filter(id=>!second_nodes.has(id))}}/>;} diff --git a/webapp_gallery/src/capture/worker_timeline.tsx b/webapp_gallery/src/capture/worker_timeline.tsx new file mode 100644 index 0000000..4115bc0 --- /dev/null +++ b/webapp_gallery/src/capture/worker_timeline.tsx @@ -0,0 +1,2 @@ +import {Box,Tooltip,Typography} from "@mui/material";import type {Gallery_Captured_Frame,Gallery_Render_Plan} from "../protocol/gallery_types";import {format_nanoseconds} from "../protocol/format"; +export function Worker_Timeline({frame,plan,selected_node_id,on_select_node}:{frame:Gallery_Captured_Frame;plan:Gallery_Render_Plan;selected_node_id:number|null;on_select_node:(id:number)=>void}) {const duration=Math.max(1,frame.render_duration_ns);const workers=[...new Set(frame.node_executions.map(item=>item.worker_id))].toSorted((a,b)=>a-b);const names=new Map(plan.nodes.map(node=>[node.node_id,node.name]));return {workers.map(worker=>Worker {worker}{frame.node_executions.filter(item=>item.worker_id===worker).map(item=>on_select_node(item.node_id)} sx={{position:"absolute",left:`${item.start_offset_ns/duration*100}%`,width:`${Math.max(.4,item.duration_ns/duration*100)}%`,top:4,bottom:4,borderRadius:.5,cursor:"pointer",bgcolor:item.node_id===selected_node_id?"secondary.main":"primary.main"}}/>)})};} diff --git a/webapp_gallery/src/common/copy_button.tsx b/webapp_gallery/src/common/copy_button.tsx new file mode 100644 index 0000000..8bf5c84 --- /dev/null +++ b/webapp_gallery/src/common/copy_button.tsx @@ -0,0 +1,3 @@ +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import {IconButton,Tooltip} from "@mui/material"; +export function Copy_Button({value}:{value:string}) {return void navigator.clipboard.writeText(value)}>;} diff --git a/webapp_gallery/src/common/empty_state.tsx b/webapp_gallery/src/common/empty_state.tsx new file mode 100644 index 0000000..7a22c0f --- /dev/null +++ b/webapp_gallery/src/common/empty_state.tsx @@ -0,0 +1,2 @@ +import {Box,Typography} from "@mui/material"; +export function Empty_State({message}:{message:string}) {return {message};} diff --git a/webapp_gallery/src/common/json_viewer.tsx b/webapp_gallery/src/common/json_viewer.tsx new file mode 100644 index 0000000..731d411 --- /dev/null +++ b/webapp_gallery/src/common/json_viewer.tsx @@ -0,0 +1,2 @@ +import {Box} from "@mui/material"; +export function Json_Viewer({value}:{value:unknown}) {return {JSON.stringify(value,null,2)};} diff --git a/webapp_gallery/src/common/metric_grid.tsx b/webapp_gallery/src/common/metric_grid.tsx new file mode 100644 index 0000000..5d5cfdb --- /dev/null +++ b/webapp_gallery/src/common/metric_grid.tsx @@ -0,0 +1,3 @@ +import {Box} from "@mui/material"; +import {Metric_Value} from "./metric_value"; +export function Metric_Grid({values}:{values:Array<[string,string|number]>}) {return {values.map(([label,value])=>)};} diff --git a/webapp_gallery/src/common/metric_value.tsx b/webapp_gallery/src/common/metric_value.tsx new file mode 100644 index 0000000..58c7939 --- /dev/null +++ b/webapp_gallery/src/common/metric_value.tsx @@ -0,0 +1,2 @@ +import {Box,Typography} from "@mui/material"; +export function Metric_Value({label,value}:{label:string;value:string|number}) {return {value}{label};} diff --git a/webapp_gallery/src/common/status_chip.tsx b/webapp_gallery/src/common/status_chip.tsx new file mode 100644 index 0000000..c92510d --- /dev/null +++ b/webapp_gallery/src/common/status_chip.tsx @@ -0,0 +1,2 @@ +import {Chip} from "@mui/material"; +export function Status_Chip({state,label}:{state:string;label:string}) {const color=state==="ready"?"success":state==="error"||state==="closed"?"error":"warning";return ;} diff --git a/webapp_gallery/src/dag/dag_layout.ts b/webapp_gallery/src/dag/dag_layout.ts new file mode 100644 index 0000000..5ab55f2 --- /dev/null +++ b/webapp_gallery/src/dag/dag_layout.ts @@ -0,0 +1,3 @@ +import ELK from "elkjs/lib/elk.bundled.js";import type {Dag_View_Model} from "./dag_types"; +const elk=new ELK();const layout_cache=new Map(); +export async function layout_dag(model:Dag_View_Model,direction:"RIGHT"|"DOWN"="RIGHT"):Promise{const key=`${model.version}:${direction}:${model.nodes.map(node=>node.id).join(",")}:${model.edges.map(edge=>`${edge.source}>${edge.target}`).join(",")}`;const cached=layout_cache.get(key);if(cached)return{...cached,nodes:cached.nodes.map((node,index)=>({...node,data:model.nodes[index].data})),edges:model.edges};const result=await elk.layout({id:"root",layoutOptions:{"elk.algorithm":"layered","elk.direction":direction,"elk.spacing.nodeNode":"36","elk.layered.spacing.nodeNodeBetweenLayers":"70"},children:model.nodes.map(node=>({id:node.id,width:250,height:96})),edges:model.edges.map(edge=>({id:edge.id,sources:[edge.source],targets:[edge.target]}))});const positions=new Map(result.children?.map(node=>[node.id,{x:node.x??0,y:node.y??0}]));const laid={...model,nodes:model.nodes.map(node=>({...node,position:positions.get(node.id)??node.position}))};layout_cache.set(key,laid);return laid;} diff --git a/webapp_gallery/src/dag/dag_legend.tsx b/webapp_gallery/src/dag/dag_legend.tsx new file mode 100644 index 0000000..15c6c7a --- /dev/null +++ b/webapp_gallery/src/dag/dag_legend.tsx @@ -0,0 +1 @@ +import {Chip,Stack} from "@mui/material";export function Dag_Legend(){return ;} diff --git a/webapp_gallery/src/dag/dag_model.ts b/webapp_gallery/src/dag/dag_model.ts new file mode 100644 index 0000000..84e283f --- /dev/null +++ b/webapp_gallery/src/dag/dag_model.ts @@ -0,0 +1,2 @@ +import type {Gallery_Captured_Frame,Gallery_Node_Statistics,Gallery_Render_Plan} from "../protocol/gallery_types";import type {Dag_View_Model} from "./dag_types"; +export function build_dag_model(plan:Gallery_Render_Plan,frame:Gallery_Captured_Frame|null,statistics:Gallery_Node_Statistics[]=[],selected_node_id:number|null=null):Dag_View_Model {const executions=new Map((frame?.node_executions??[]).map(item=>[item.node_id,item]));const analysis=new Map((frame?.analysis?.nodes??[]).map(item=>[item.node_id,item]));const historical=new Map(statistics.map(item=>[item.node_id,item]));return{version:plan.version,nodes:plan.nodes.map(node=>{const id=String(node.node_id??node.id);const execution=executions.get(node.node_id);return{id,type:"dag_node",position:{x:0,y:0},data:{render_node:node,duration_ns:execution?.duration_ns,worker_id:execution?.worker_id,critical:analysis.get(node.node_id)?.on_critical_path,selected:node.node_id===selected_node_id,historical:historical.get(node.node_id)}};}),edges:plan.edges.map((edge,index)=>({id:`${edge.from}-${edge.to}-${index}`,source:String(edge.from),target:String(edge.to),animated:Boolean(frame)}))};} diff --git a/webapp_gallery/src/dag/dag_node.tsx b/webapp_gallery/src/dag/dag_node.tsx new file mode 100644 index 0000000..f4c2e3e --- /dev/null +++ b/webapp_gallery/src/dag/dag_node.tsx @@ -0,0 +1,2 @@ +import {Handle,Position,type NodeProps} from "@xyflow/react";import {Box,Typography} from "@mui/material";import type {Node} from "@xyflow/react";import type {Dag_Node_Data} from "./dag_types";import {format_nanoseconds} from "../protocol/format"; +export function Dag_Node({data}:NodeProps>) {const node=data.render_node;return {node.kind}{node.name||node.label}{node.owner}{data.duration_ns===undefined?"未执行":`${format_nanoseconds(data.duration_ns)} · worker ${data.worker_id}`};} diff --git a/webapp_gallery/src/dag/dag_types.ts b/webapp_gallery/src/dag/dag_types.ts new file mode 100644 index 0000000..52d96ca --- /dev/null +++ b/webapp_gallery/src/dag/dag_types.ts @@ -0,0 +1,3 @@ +import type {Edge,Node} from "@xyflow/react";import type {Gallery_Render_Node} from "../protocol/gallery_types"; +export interface Dag_Node_Data extends Record {render_node: Gallery_Render_Node; duration_ns?: number; worker_id?: number; critical?: boolean; selected?: boolean;} +export interface Dag_View_Model {nodes: Node[];edges: Edge[];version:number;} diff --git a/webapp_gallery/src/dag/render_dag.tsx b/webapp_gallery/src/dag/render_dag.tsx new file mode 100644 index 0000000..2e9f6a3 --- /dev/null +++ b/webapp_gallery/src/dag/render_dag.tsx @@ -0,0 +1,3 @@ +import {Background,Controls,MiniMap,ReactFlow} from "@xyflow/react";import {Box} from "@mui/material";import {useEffect,useMemo,useState} from "react";import type {Gallery_Captured_Frame,Gallery_Node_Statistics,Gallery_Render_Plan} from "../protocol/gallery_types";import {build_dag_model} from "./dag_model";import {layout_dag} from "./dag_layout";import {Dag_Node} from "./dag_node";import type {Dag_View_Model} from "./dag_types"; +const node_types={dag_node:Dag_Node}; +export function Render_Dag({plan,frame=null,statistics=[],selected_node_id,on_select_node}:{plan:Gallery_Render_Plan;frame?:Gallery_Captured_Frame|null;statistics?:Gallery_Node_Statistics[];selected_node_id:number|null;on_select_node:(id:number)=>void}) {const source=useMemo(()=>build_dag_model(plan,frame,statistics,selected_node_id),[plan,frame,statistics,selected_node_id]);const [model,set_model]=useState(source);useEffect(()=>{let current=true;void layout_dag(source).then(value=>current&&set_model(value));return()=>{current=false;};},[source]);return on_select_node(Number(node.id))}>;} diff --git a/webapp_gallery/src/gallery/category_filter.tsx b/webapp_gallery/src/gallery/category_filter.tsx new file mode 100644 index 0000000..e00c0ff --- /dev/null +++ b/webapp_gallery/src/gallery/category_filter.tsx @@ -0,0 +1,2 @@ +import {Chip,Stack} from "@mui/material"; +export function Category_Filter({categories,active_category,on_change}:{categories:string[];active_category:string;on_change:(category:string)=>void}) {return {categories.map(category=>on_change(category)}/>)};} diff --git a/webapp_gallery/src/gallery/frame_mode_tabs.tsx b/webapp_gallery/src/gallery/frame_mode_tabs.tsx new file mode 100644 index 0000000..0fd0908 --- /dev/null +++ b/webapp_gallery/src/gallery/frame_mode_tabs.tsx @@ -0,0 +1,2 @@ +import {Tab,Tabs} from "@mui/material";import type {Gallery_Frame_Mode} from "../protocol/gallery_types"; +export function Frame_Mode_Tabs({frame_modes,active_mode,on_change}:{frame_modes:Gallery_Frame_Mode[];active_mode:string;on_change:(id:string)=>void}) {return on_change(value)} variant="scrollable" scrollButtons="auto">{frame_modes.map(mode=>)};} diff --git a/webapp_gallery/src/gallery/gallery_page.tsx b/webapp_gallery/src/gallery/gallery_page.tsx new file mode 100644 index 0000000..53cc16b --- /dev/null +++ b/webapp_gallery/src/gallery/gallery_page.tsx @@ -0,0 +1,2 @@ +import {Box} from "@mui/material";import {useCallback,useEffect,useRef} from "react";import type {Gallery_Case,Gallery_Frame_Mode} from "../protocol/gallery_types";import type {Gallery_Plot_Session} from "../session/gallery_plot_session";import {Plot_Card,type Selected_Plot} from "./plot_card"; +export function Gallery_Page({cases,frame_mode,streams_paused,on_open_inspector}:{cases:Gallery_Case[];frame_mode:Gallery_Frame_Mode;streams_paused:boolean;on_open_inspector:(plot:Selected_Plot)=>void}) {const sessions=useRef(new Set());const register=useCallback((session:Gallery_Plot_Session,mount:boolean)=>{mount?sessions.current.add(session):sessions.current.delete(session);},[]);useEffect(()=>{let animation_frame=0;const loop=(now:number)=>{for(const session of sessions.current)session.tick(now);animation_frame=requestAnimationFrame(loop);};animation_frame=requestAnimationFrame(loop);return()=>cancelAnimationFrame(animation_frame);},[]);return {cases.map(gallery_case=>)};} diff --git a/webapp_gallery/src/gallery/gallery_summary.tsx b/webapp_gallery/src/gallery/gallery_summary.tsx new file mode 100644 index 0000000..afa3677 --- /dev/null +++ b/webapp_gallery/src/gallery/gallery_summary.tsx @@ -0,0 +1,2 @@ +import {Box,Paper,Typography} from "@mui/material";import type {Gallery_Catalog} from "../protocol/gallery_types";import {Metric_Grid} from "../common/metric_grid"; +export function Gallery_Summary({catalog}:{catalog:Gallery_Catalog}) {const coverage=catalog.coverage;return {catalog.navigation.hero_eyebrow}{catalog.navigation.hero_title}所有属性、动作、观察者和性能数据均由后端通过 WebSocket 返回。;} diff --git a/webapp_gallery/src/gallery/gallery_toolbar.tsx b/webapp_gallery/src/gallery/gallery_toolbar.tsx new file mode 100644 index 0000000..85c748e --- /dev/null +++ b/webapp_gallery/src/gallery/gallery_toolbar.tsx @@ -0,0 +1,4 @@ +import PauseIcon from "@mui/icons-material/Pause";import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import {AppBar,Box,Button,Toolbar,Typography} from "@mui/material"; +import {Status_Chip} from "../common/status_chip"; +export function Gallery_Toolbar({socket_state,socket_message,streams_paused,on_toggle_streams}:{socket_state:string;socket_message:string;streams_paused:boolean;on_toggle_streams:()=>void}) {return R2CORE2 · KERNEL · WEBSOCKETRenderive 性能画廊;} diff --git a/webapp_gallery/src/gallery/plot_card.tsx b/webapp_gallery/src/gallery/plot_card.tsx new file mode 100644 index 0000000..646fb85 --- /dev/null +++ b/webapp_gallery/src/gallery/plot_card.tsx @@ -0,0 +1,23 @@ +import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import {Box,Button,Card,CardActions,CardContent,CardHeader,Chip,Stack,Typography} from "@mui/material"; +import {useEffect} from "react"; +import type {Gallery_Case,Gallery_Frame_Mode} from "../protocol/gallery_types"; +import {use_plot_session} from "../hooks/use_plot_session"; +import type {Gallery_Plot_Session} from "../session/gallery_plot_session"; +import {Plot_Canvas} from "../plot/plot_canvas"; +import {Plot_Status} from "../plot/plot_status"; +import {Performance_Strip} from "../plot/performance_strip"; +import {Kernel_Observer_Summary} from "../plot/kernel_observer_summary"; + +export interface Selected_Plot {session:Gallery_Plot_Session;} +export function Plot_Card({gallery_case,frame_mode,active,streams_paused,on_register,on_open_inspector}:{gallery_case:Gallery_Case;frame_mode:Gallery_Frame_Mode;active:boolean;streams_paused:boolean;on_register:(session:Gallery_Plot_Session,mount:boolean)=>void;on_open_inspector:(plot:Selected_Plot)=>void}) { + const [session,snapshot]=use_plot_session(gallery_case,frame_mode); + useEffect(()=>{on_register(session,true);return()=>on_register(session,false);},[session,on_register]); + useEffect(()=>session.set_activity(active,streams_paused),[session,active,streams_paused]); + return {event.preventDefault();if(snapshot.ready)on_open_inspector({session});}}> + } action={}/> + {frame_mode.observer_visible&&}{gallery_case.description}{snapshot.notice&&{snapshot.notice}} + {snapshot.controls.length} 属性{snapshot.actions.length} 动作{snapshot.frame_count} 像素帧 + ; +} diff --git a/webapp_gallery/src/hooks/use_element_size.ts b/webapp_gallery/src/hooks/use_element_size.ts new file mode 100644 index 0000000..56e9ef8 --- /dev/null +++ b/webapp_gallery/src/hooks/use_element_size.ts @@ -0,0 +1,3 @@ +import {useEffect, useState, type RefObject} from "react"; +export interface Element_Size {width:number;height:number;} +export function use_element_size(ref: RefObject): Element_Size {const [size,set_size]=useState({width:0,height:0});useEffect(()=>{const element=ref.current;if(!element)return;const observer=new ResizeObserver(entries=>{const rect=entries[0]?.contentRect;if(rect)set_size({width:rect.width,height:rect.height});});observer.observe(element);return()=>observer.disconnect();},[ref]);return size;} diff --git a/webapp_gallery/src/hooks/use_gallery_catalog.ts b/webapp_gallery/src/hooks/use_gallery_catalog.ts new file mode 100644 index 0000000..ca4508d --- /dev/null +++ b/webapp_gallery/src/hooks/use_gallery_catalog.ts @@ -0,0 +1,4 @@ +import {useEffect, useState} from "react"; +import type {Gallery_Catalog} from "../protocol/gallery_types"; +import {load_gallery_catalog} from "../transport/catalog_loader"; +export function use_gallery_catalog(): {catalog: Gallery_Catalog|null; state: string; message: string} {const [catalog,set_catalog]=useState(null);const [status,set_status]=useState({state:"connecting",message:"读取后端目录"});useEffect(()=>load_gallery_catalog(set_catalog,(state,message)=>set_status({state,message})),[]);return {catalog,...status};} diff --git a/webapp_gallery/src/hooks/use_plot_session.ts b/webapp_gallery/src/hooks/use_plot_session.ts new file mode 100644 index 0000000..0486ca7 --- /dev/null +++ b/webapp_gallery/src/hooks/use_plot_session.ts @@ -0,0 +1,9 @@ +import {useEffect, useMemo, useSyncExternalStore} from "react"; +import type {Gallery_Case, Gallery_Frame_Mode} from "../protocol/gallery_types"; +import {Gallery_Plot_Session} from "../session/gallery_plot_session"; +export function use_plot_session(gallery_case: Gallery_Case, frame_mode: Gallery_Frame_Mode): [Gallery_Plot_Session, ReturnType] { + const session=useMemo(()=>new Gallery_Plot_Session(gallery_case,frame_mode),[gallery_case,frame_mode]); + const snapshot=useSyncExternalStore(session.subscribe,session.get_snapshot,session.get_snapshot); + useEffect(()=>()=>session.dispose(),[session]); + return [session,snapshot]; +} diff --git a/webapp_gallery/src/inspector/actions_panel.tsx b/webapp_gallery/src/inspector/actions_panel.tsx new file mode 100644 index 0000000..68405e9 --- /dev/null +++ b/webapp_gallery/src/inspector/actions_panel.tsx @@ -0,0 +1,3 @@ +import {Button,Paper,Stack,TextField,Typography} from "@mui/material";import {useState} from "react";import type {Gallery_Action,Json_Primitive} from "../protocol/gallery_types"; +function Action_Row({action,on_action}:{action:Gallery_Action;on_action:(id:string,argument?:Json_Primitive)=>void}) {const [argument,set_argument]=useState(String(action.argument_default??""));return {action.label}{action.api}{action.argument_input&&set_argument(event.target.value)} sx={{width:130}}/>};} +export function Actions_Panel({actions,on_action}:{actions:Gallery_Action[];on_action:(id:string,argument?:Json_Primitive)=>void}) {return {actions.map(action=>)};} diff --git a/webapp_gallery/src/inspector/control_field.tsx b/webapp_gallery/src/inspector/control_field.tsx new file mode 100644 index 0000000..6700364 --- /dev/null +++ b/webapp_gallery/src/inspector/control_field.tsx @@ -0,0 +1,2 @@ +import {FormControlLabel,MenuItem,Switch,TextField} from "@mui/material";import {useEffect,useState} from "react";import type {Gallery_Control,Json_Value} from "../protocol/gallery_types"; +export function Control_Field({control,on_commit}:{control:Gallery_Control;on_commit:(value:Json_Value)=>void}) {const [draft,set_draft]=useState(String(control.value??""));const [editing,set_editing]=useState(false);useEffect(()=>{if(!editing)set_draft(String(control.value??""));},[control.value,editing]);if(control.input==="boolean")return on_commit(checked)}/>} label={control.label}/>;if(control.input==="select")return set_editing(true)} onBlur={()=>set_editing(false)} onChange={event=>{set_draft(event.target.value);on_commit(event.target.value);}}>{control.options.map(option=>{option.label})};const commit=()=>{set_editing(false);if(control.input!=="number"){if(draft!==String(control.value??""))on_commit(draft);return;}const number=Number(draft);if(!Number.isFinite(number)||(control.minimum!==undefined&&numbercontrol.maximum)){set_draft(String(control.value??""));return;}if(number!==Number(control.value))on_commit(number);};return set_editing(true)} onChange={event=>set_draft(event.target.value)} onBlur={commit} onKeyDown={event=>{if(event.key==="Enter")commit();else if(event.key==="Escape"){set_draft(String(control.value??""));set_editing(false);}}}/>;} diff --git a/webapp_gallery/src/inspector/controls_panel.tsx b/webapp_gallery/src/inspector/controls_panel.tsx new file mode 100644 index 0000000..8cfcbc9 --- /dev/null +++ b/webapp_gallery/src/inspector/controls_panel.tsx @@ -0,0 +1,2 @@ +import {Accordion,AccordionDetails,AccordionSummary,Stack,Typography} from "@mui/material";import ExpandMoreIcon from "@mui/icons-material/ExpandMore";import type {Gallery_Control,Json_Value} from "../protocol/gallery_types";import {Control_Field} from "./control_field";import {nested_patch} from "../protocol/gallery_descriptor"; +export function Controls_Panel({controls,on_patch}:{controls:Gallery_Control[];on_patch:(target:string,patch:Record)=>void}) {const groups=new Map();for(const control of controls){const group=control.group||"其他";groups.set(group,[...(groups.get(group)??[]),control]);}return {[...groups].map(([group,items],index)=>}>{group} · {items.length} 项{items.map(control=>on_patch(control.target,nested_patch(control.path,value))}/>)})};} diff --git a/webapp_gallery/src/inspector/inspector_drawer.tsx b/webapp_gallery/src/inspector/inspector_drawer.tsx new file mode 100644 index 0000000..58671ef --- /dev/null +++ b/webapp_gallery/src/inspector/inspector_drawer.tsx @@ -0,0 +1,4 @@ +import CloseIcon from "@mui/icons-material/Close";import RefreshIcon from "@mui/icons-material/Refresh";import RestartAltIcon from "@mui/icons-material/RestartAlt"; +import {Box,Drawer,IconButton,Stack,Tooltip,Typography} from "@mui/material";import {useState,useSyncExternalStore} from "react";import type {Gallery_Plot_Session} from "../session/gallery_plot_session";import {EMPTY_PLOT_SNAPSHOT} from "../session/gallery_plot_snapshot";import {Inspector_Tabs} from "./inspector_tabs";import {Controls_Panel} from "./controls_panel";import {Actions_Panel} from "./actions_panel";import {Observer_Panel} from "./observer_panel";import {Performance_Panel} from "./performance_panel";import {Capture_Panel} from "../capture/capture_panel";import {Render_Dag} from "../dag/render_dag";import {Empty_State} from "../common/empty_state"; +const empty_subscribe=()=>()=>undefined;const empty_snapshot=()=>EMPTY_PLOT_SNAPSHOT; +export function Inspector_Drawer({open,session,active_tab,on_change_tab,on_close}:{open:boolean;session:Gallery_Plot_Session|null;active_tab:string;on_change_tab:(tab:string)=>void;on_close:()=>void}) {const snapshot=useSyncExternalStore(session?.subscribe??empty_subscribe,session?.get_snapshot??empty_snapshot,session?.get_snapshot??empty_snapshot);const [selected_node_id,set_selected_node_id]=useState(null);let body=null;if(session){if(active_tab==="controls")body=session.patch(target,patch)}/>;else if(active_tab==="actions")body={session.action(action,argument);setTimeout(()=>session.request_frame(performance.now(),true),40);}}/>;else if(active_tab==="observer")body=;else if(active_tab==="performance")body=;else if(active_tab==="performance_capture")body=snapshot.performance_capture?{session.action(action,count);setTimeout(()=>session.request_frame(performance.now(),true),40);}}/>:;else if(active_tab==="render_plan")body=snapshot.render_plan?:;}return {session&&`${session.frame_mode.strategy} / ${session.gallery_case.component}`}{session?.gallery_case.title??"Inspector"}session?.reset_monitoring()}>session?.refresh()}>{body};} diff --git a/webapp_gallery/src/inspector/inspector_tabs.tsx b/webapp_gallery/src/inspector/inspector_tabs.tsx new file mode 100644 index 0000000..377fc9b --- /dev/null +++ b/webapp_gallery/src/inspector/inspector_tabs.tsx @@ -0,0 +1,3 @@ +import {Tab,Tabs} from "@mui/material"; +export const INSPECTOR_TABS=[{id:"controls",label:"控件属性"},{id:"actions",label:"专属 API"},{id:"observer",label:"内核观察器"},{id:"performance",label:"性能监测"},{id:"performance_capture",label:"Performance Capture"},{id:"render_plan",label:"Render DAG"}] as const; +export function Inspector_Tabs({active_tab,on_change}:{active_tab:string;on_change:(tab:string)=>void}) {return on_change(value)} variant="scrollable" scrollButtons="auto">{INSPECTOR_TABS.map(tab=>)};} diff --git a/webapp_gallery/src/inspector/observer_panel.tsx b/webapp_gallery/src/inspector/observer_panel.tsx new file mode 100644 index 0000000..1bb8d92 --- /dev/null +++ b/webapp_gallery/src/inspector/observer_panel.tsx @@ -0,0 +1,2 @@ +import {Stack,Typography} from "@mui/material";import type {Gallery_Telemetry} from "../protocol/gallery_types";import {Json_Viewer} from "../common/json_viewer"; +export function Observer_Panel({telemetry}:{telemetry:Gallery_Telemetry}) {return 内核与 Renderable 观察数据;} diff --git a/webapp_gallery/src/inspector/performance_panel.tsx b/webapp_gallery/src/inspector/performance_panel.tsx new file mode 100644 index 0000000..af79708 --- /dev/null +++ b/webapp_gallery/src/inspector/performance_panel.tsx @@ -0,0 +1,2 @@ +import {Stack,Typography} from "@mui/material";import type {Gallery_Telemetry} from "../protocol/gallery_types";import {Json_Viewer} from "../common/json_viewer"; +export function Performance_Panel({telemetry}:{telemetry:Gallery_Telemetry}) {return 性能滑动窗口;} diff --git a/webapp_gallery/src/main.tsx b/webapp_gallery/src/main.tsx new file mode 100644 index 0000000..bad8984 --- /dev/null +++ b/webapp_gallery/src/main.tsx @@ -0,0 +1,7 @@ +import {StrictMode} from "react"; +import {createRoot} from "react-dom/client"; +import {CssBaseline,ThemeProvider} from "@mui/material"; +import {gallery_theme} from "./theme"; +import {App} from "./app"; +import "@xyflow/react/dist/style.css"; +createRoot(document.getElementById("root")!).render(); diff --git a/webapp_gallery/src/plot/kernel_observer_summary.tsx b/webapp_gallery/src/plot/kernel_observer_summary.tsx new file mode 100644 index 0000000..dd6b281 --- /dev/null +++ b/webapp_gallery/src/plot/kernel_observer_summary.tsx @@ -0,0 +1,2 @@ +import {Box,Typography} from "@mui/material";import type {Gallery_Telemetry} from "../protocol/gallery_types";import {Json_Viewer} from "../common/json_viewer"; +export function Kernel_Observer_Summary({telemetry}:{telemetry:Gallery_Telemetry}) {const observer=telemetry.frame_observer??telemetry.observer;return Kernel Observer{observer?:等待观察数据};} diff --git a/webapp_gallery/src/plot/performance_strip.tsx b/webapp_gallery/src/plot/performance_strip.tsx new file mode 100644 index 0000000..28524d0 --- /dev/null +++ b/webapp_gallery/src/plot/performance_strip.tsx @@ -0,0 +1,3 @@ +import {Box,Typography} from "@mui/material";import type {Gallery_Telemetry} from "../protocol/gallery_types";import {value_at_path} from "../protocol/format"; +const METRICS:Array<[string,string,string]>=[["后端 FPS","performance.measured_fps"," FPS"],["像素 FPS","performance.pixel_response_fps"," FPS"],["渲染","performance.last_render_ms"," ms"],["编码","performance.last_pixel_encode_ms"," ms"]]; +export function Performance_Strip({telemetry}:{telemetry:Gallery_Telemetry}) {return {METRICS.map(([label,path,suffix])=>{label}{Number(value_at_path(telemetry,path)||0).toFixed(1)}{suffix})};} diff --git a/webapp_gallery/src/plot/plot_canvas.tsx b/webapp_gallery/src/plot/plot_canvas.tsx new file mode 100644 index 0000000..308f888 --- /dev/null +++ b/webapp_gallery/src/plot/plot_canvas.tsx @@ -0,0 +1,3 @@ +import {Box} from "@mui/material";import {useEffect,useRef} from "react";import type {Gallery_Plot_Session} from "../session/gallery_plot_session";import {use_element_size} from "../hooks/use_element_size"; +function modifiers(event: {shiftKey:boolean;ctrlKey:boolean;altKey:boolean;metaKey:boolean}): number {return(event.shiftKey?1:0)|(event.ctrlKey?2:0)|(event.altKey?4:0)|(event.metaKey?8:0);} +export function Plot_Canvas({session}:{session:Gallery_Plot_Session}) {const shell_ref=useRef(null),canvas_ref=useRef(null);const size=use_element_size(shell_ref);useEffect(()=>{const canvas=canvas_ref.current;if(!canvas)return;session.attach_canvas(canvas);return()=>session.detach_canvas();},[session]);useEffect(()=>{if(size.width&&size.height)session.resize(size.width,size.height);},[session,size]);const position=(event:React.PointerEvent|React.WheelEvent)=>{const canvas=canvas_ref.current!;const rect=canvas.getBoundingClientRect();return{x:(event.clientX-rect.left)*canvas.width/Math.max(1,rect.width),y:(event.clientY-rect.top)*canvas.height/Math.max(1,rect.height)};};const pointer=(type:"pointer_move"|"pointer_press"|"pointer_release",event:React.PointerEvent)=>session.pointer(type,{...position(event),button:["left","middle","right"][event.button]??"none",buttons:event.buttons,modifiers:modifiers(event)});return session.key("key_press",{key:event.key,nativeKey:event.keyCode,repeat:event.repeat,modifiers:modifiers(event)})} onKeyUp={event=>session.key("key_release",{key:event.key,nativeKey:event.keyCode,repeat:false,modifiers:modifiers(event)})}>pointer("pointer_move",event)} onPointerDown={event=>{if(event.button!==2){shell_ref.current?.focus();event.currentTarget.setPointerCapture(event.pointerId);pointer("pointer_press",event);}}} onPointerUp={event=>event.button!==2&&pointer("pointer_release",event)} onPointerLeave={()=>session.leave()} onWheel={event=>{event.preventDefault();session.wheel({...position(event),pixelDeltaX:event.deltaX,pixelDeltaY:event.deltaY,angleDeltaX:-event.deltaX*8,angleDeltaY:-event.deltaY*8,buttons:event.buttons,modifiers:modifiers(event)});}}/>;} diff --git a/webapp_gallery/src/plot/plot_status.tsx b/webapp_gallery/src/plot/plot_status.tsx new file mode 100644 index 0000000..373e02b --- /dev/null +++ b/webapp_gallery/src/plot/plot_status.tsx @@ -0,0 +1,2 @@ +import {Stack} from "@mui/material";import {Status_Chip} from "../common/status_chip";import type {Gallery_Plot_Snapshot} from "../session/gallery_plot_snapshot"; +export function Plot_Status({snapshot,active}:{snapshot:Gallery_Plot_Snapshot;active:boolean}) {return ;} diff --git a/webapp_gallery/src/protocol/format.ts b/webapp_gallery/src/protocol/format.ts new file mode 100644 index 0000000..e3df0fd --- /dev/null +++ b/webapp_gallery/src/protocol/format.ts @@ -0,0 +1,9 @@ +import type {Gallery_Dashboard_Field, Gallery_Dashboard, Json_Value} from "./gallery_types"; +export function format_nanoseconds(value: unknown): string {const ns = Math.max(0, Number(value) || 0); return ns < 1_000 ? `${Math.round(ns)} ns` : ns < 1_000_000 ? `${(ns / 1_000).toFixed(2)} µs` : `${(ns / 1_000_000).toFixed(3)} ms`;} +export function value_at_path(root: unknown, path = ""): unknown {return path.split(".").filter(Boolean).reduce((value, key) => value && typeof value === "object" ? (value as Record)[key] : undefined, root);} +export function format_dashboard_field(field: Gallery_Dashboard_Field, telemetry: Record, dashboard: Gallery_Dashboard): string { + const raw = value_at_path(telemetry, field.source) ?? field.default ?? ""; const number = Number(raw) || 0; const digits = field.digits ?? 0; + if (field.format === "fixed") return number.toFixed(digits); if (field.format === "integer") return number.toLocaleString(); if (field.format === "milliseconds") return `${number.toFixed(digits)} ms`; if (field.format === "fps") return `${number.toFixed(digits)} FPS`; if (field.format === "bytes") return `${number.toLocaleString()} B`; if (field.format === "nanoseconds") return format_nanoseconds(number); + if (field.format === "enum") return dashboard.value_maps?.[field.value_map ?? ""]?.[String(raw)] ?? String(raw); + return String(raw ?? "—"); +} diff --git a/webapp_gallery/src/protocol/gallery_descriptor.ts b/webapp_gallery/src/protocol/gallery_descriptor.ts new file mode 100644 index 0000000..d68e112 --- /dev/null +++ b/webapp_gallery/src/protocol/gallery_descriptor.ts @@ -0,0 +1,31 @@ +import type {Gallery_Control, Gallery_Descriptor_Field, Gallery_Resource, Json_Value} from "./gallery_types"; + +export function field_visible(expression: string | undefined, data: Record | undefined): boolean { + if (!expression) return true; + const equality = expression.match(/^\$\{\$self\.([A-Za-z_][A-Za-z0-9_]*) == '([^']*)'}$/); + return equality ? String(data?.[equality[1]]) === equality[2] : true; +} +export function build_controls(resources: Gallery_Resource[] = []): Gallery_Control[] { + const controls: Gallery_Control[] = []; + const visit = (fields: Gallery_Descriptor_Field[], data: Record, target: string, group: string, path: string[] = [], labels: string[] = []) => { + for (const field of fields ?? []) { + const presentation = field.presentation ?? {}; + if (!field_visible(presentation.visible_on, data)) continue; + const field_path = [...path, field.name]; + const field_labels = [...labels, presentation.label ?? field.name]; + const value = data?.[field.name]; + if (field.children?.length) { + visit(field.children, value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}, target, group, field_path, field_labels); + } else if (field.editable) { + controls.push({id: field_path.join("."), target, path: field_path, label: field_labels.join(" / "), api: field_path.join("."), description: presentation.description ?? "", group, input: presentation.control === "automatic" ? "text" : presentation.control ?? "text", minimum: field.minimum, maximum: field.maximum, step: field.multiple_of, options: presentation.options ?? [], value}); + } + } + }; + for (const resource of resources) visit(resource.descriptor?.fields ?? [], resource.data ?? {}, resource.target, resource.view?.title ?? resource.title ?? resource.descriptor?.label ?? "控件属性"); + return controls; +} +export function nested_patch(path: string[], value: Json_Value): Record { + let result: Json_Value = value; + for (const key of [...path].reverse()) result = {[key]: result}; + return result as Record; +} diff --git a/webapp_gallery/src/protocol/gallery_messages.ts b/webapp_gallery/src/protocol/gallery_messages.ts new file mode 100644 index 0000000..2b59483 --- /dev/null +++ b/webapp_gallery/src/protocol/gallery_messages.ts @@ -0,0 +1,9 @@ +import type {Gallery_Client_Performance, Json_Primitive, Json_Value} from "./gallery_types"; + +export function gallery_message(type: string, payload: Record = {}): string { + return JSON.stringify({category: "event", type, ...payload}); +} +export function gallery_open_message(case_id: string, frame_mode: string): string {return gallery_message("gallery_open", {case: case_id, frame_mode});} +export function gallery_patch_message(target: string, patch: Record): string {return gallery_message("gallery_patch", {target, patch});} +export function gallery_action_message(action: string, argument?: Json_Primitive): string {return gallery_message("gallery_action", argument === undefined ? {action} : {action, argument});} +export function gallery_metrics_payload(client_performance: Gallery_Client_Performance): Record {return {client_metrics: client_performance as unknown as Record};} diff --git a/webapp_gallery/src/protocol/gallery_parser.ts b/webapp_gallery/src/protocol/gallery_parser.ts new file mode 100644 index 0000000..a3926c8 --- /dev/null +++ b/webapp_gallery/src/protocol/gallery_parser.ts @@ -0,0 +1,14 @@ +import type {Gallery_Response} from "./gallery_types"; + +const KNOWN_RESPONSE_TYPES = new Set(["catalog", "case_state", "refresh_state", "observer_state", "error"]); +export class Gallery_Protocol_Error extends Error {} + +export function parse_gallery_response(raw: string): Gallery_Response | null { + let value: unknown; + try { value = JSON.parse(raw); } catch { throw new Gallery_Protocol_Error("后端返回了无效 JSON"); } + if (!value || typeof value !== "object" || Array.isArray(value)) throw new Gallery_Protocol_Error("后端响应必须是对象"); + const type = (value as {type?: unknown}).type; + if (typeof type !== "string") throw new Gallery_Protocol_Error("后端响应缺少 type"); + if (!KNOWN_RESPONSE_TYPES.has(type)) return null; + return value as Gallery_Response; +} diff --git a/webapp_gallery/src/protocol/gallery_types.ts b/webapp_gallery/src/protocol/gallery_types.ts new file mode 100644 index 0000000..6873e47 --- /dev/null +++ b/webapp_gallery/src/protocol/gallery_types.ts @@ -0,0 +1,45 @@ +export type Json_Primitive = string | number | boolean | null; +export type Json_Value = Json_Primitive | Json_Value[] | {[key: string]: Json_Value}; +export type Connection_State = "connecting" | "ready" | "error" | "closed"; + +export interface Gallery_Option {label: string; value: Json_Primitive;} +export interface Gallery_Descriptor_Field { + name: string; editable?: boolean; minimum?: number; maximum?: number; multiple_of?: number; + children?: Gallery_Descriptor_Field[]; + presentation?: {label?: string; description?: string; control?: string; visible_on?: string; options?: Gallery_Option[]}; +} +export interface Gallery_Descriptor {label?: string; fields: Gallery_Descriptor_Field[];} +export interface Gallery_Resource {target: string; title?: string; descriptor: Gallery_Descriptor; data: Record; view?: {title?: string};} +export interface Gallery_Control {id: string; target: string; path: string[]; label: string; api: string; description: string; group: string; input: string; minimum?: number; maximum?: number; step?: number; options: Gallery_Option[]; value: Json_Value | undefined;} +export interface Gallery_Action {id: string; label: string; api: string; group?: string; argument_input?: string; argument_default?: Json_Primitive; request_frame?: boolean;} + +export interface Gallery_Navigation {default_mode: string; all_categories_label: string; hero_eyebrow: string; hero_title: string; catalog_loaded_text: string;} +export interface Gallery_Coverage {case_count: number; page_count: number; canvas_count: number; manual_control_count: number; manual_action_count: number;} +export interface Gallery_Frame_Mode {id: string; title: string; strategy: string; description: string; order: number; accent: string; automatic: boolean; request_after_response: boolean; request_on_animation_frame: boolean; observer_visible: boolean; frame_button_label: string;} +export interface Gallery_Case {id: string; title: string; description: string; component: string; category: string; order: number; control_count_by_mode?: Record; action_count_by_mode?: Record;} +export interface Gallery_Dashboard_Field {label: string; source?: string; sources?: string[]; enabled_source?: string; duration_sources?: Record; duration_source?: string; active_value?: Json_Primitive; disabled_label?: string; default?: Json_Primitive; digits?: number; format?: string; separator?: string; joiner?: string; value_map?: string; cell_class?: string;} +export interface Gallery_Dashboard {performance: {fields: Gallery_Dashboard_Field[]}; limits: {title: string; aria_label: string; current_source: string; active_label: string; inactive_label: string; disabled_label: string; fields: Gallery_Dashboard_Field[]}; observer: Record; menu_views: Record; value_maps?: Record>;} +export interface Gallery_Catalog {type: "catalog"; navigation: Gallery_Navigation; dashboard: Gallery_Dashboard; frame_modes: Gallery_Frame_Mode[]; cases: Gallery_Case[]; coverage: Gallery_Coverage; descriptor?: Gallery_Descriptor; transport?: {socket_per_canvas?: boolean};} + +export interface Gallery_Client_Performance {transport_fps: number; presentation_fps: number; display_interval_latest_ms: number; display_interval_ms: number; display_interval_average_ms: number; display_interval_p95_ms: number; display_interval_p99_ms: number; display_interval_deviation_ms: number; frame_round_trip_ms: number; frame_round_trip_average_ms: number; frame_round_trip_p95_ms: number; frame_round_trip_p99_ms: number; frame_round_trip_deviation_ms: number; changed_pixel_frames: number; duplicate_pixel_frames: number; overwritten_pixel_frames: number; frame_request_timeout_count: number; last_pixel_receive_age_ms: number; last_pixel_change_age_ms: number; websocket_buffered_bytes: number;} +export interface Gallery_Performance extends Record {} +export interface Gallery_Kernel_Observer extends Record {} +export interface Gallery_Renderable_Observer extends Gallery_Resource {} +export interface Gallery_Telemetry {[key: string]: unknown; renderable_observers?: Gallery_Renderable_Observer[]; performance_capture?: Gallery_Performance_Capture;} + +export interface Gallery_Render_Node {node_id: number; id?: string | number; name: string; label?: string; owner: string; kind: string;} +export interface Gallery_Render_Edge {from: number | string; to: number | string;} +export interface Gallery_Renderable_Cache {owner_id: number; name: string; prepare_cache: string; paint_cache: string;} +export interface Gallery_Render_Plan {version: number; nodes: Gallery_Render_Node[]; edges: Gallery_Render_Edge[]; renderables: Gallery_Renderable_Cache[];} +export interface Gallery_Node_Execution {node_id: number; worker_id: number; start_offset_ns: number; end_offset_ns: number; duration_ns: number; metrics?: Record;} +export interface Gallery_Node_Analysis {node_id: number; scheduler_wait_ns: number; critical_path_contribution: number; on_critical_path: boolean;} +export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: {nodes: Gallery_Node_Analysis[]};} +export interface Gallery_Node_Statistics {node_id: number; moving_average_ns: number; p95_ns: number; p99_ns: number; critical_path_frequency: number;} +export interface Gallery_Plan_Statistics {render_plan_version: number; frame_count: number; render_average_ns: number; render_p95_ns: number; average_parallelism: number; peak_parallelism: number; scheduler_wait_average_ns: number; nodes: Gallery_Node_Statistics[];} +export interface Gallery_Capture_Session {session_id: number; active: boolean; requested_count: number; captured_count: number; frames: Gallery_Captured_Frame[]; node_statistics: Gallery_Node_Statistics[]; plan_statistics: Gallery_Plan_Statistics[]; summary?: Record;} +export interface Gallery_Performance_Capture {controller: {enabled?: boolean; session_id?: number}; sessions: Gallery_Capture_Session[]; plans: Gallery_Render_Plan[];} + +export interface Gallery_Error_Response {type: "error"; message: string; field_errors?: Record;} +export interface Gallery_Case_State {type: "case_state" | "refresh_state"; case?: string; frame_mode?: {strategy?: string} | string; controls?: {resources?: Gallery_Resource[]; observers?: Gallery_Renderable_Observer[]; render_plan?: Gallery_Render_Plan; performance_capture?: Gallery_Performance_Capture}; actions?: {data?: Gallery_Action[]}; telemetry?: Gallery_Telemetry; notice?: string;} +export interface Gallery_Observer_State {type: "observer_state"; telemetry: Gallery_Telemetry;} +export type Gallery_Response = Gallery_Catalog | Gallery_Error_Response | Gallery_Case_State | Gallery_Observer_State; diff --git a/webapp_gallery/src/protocol/pixel_frame.ts b/webapp_gallery/src/protocol/pixel_frame.ts new file mode 100644 index 0000000..3694c18 --- /dev/null +++ b/webapp_gallery/src/protocol/pixel_frame.ts @@ -0,0 +1,21 @@ +export const PIXEL_FRAME_HEADER_SIZE = 16; +export interface Pixel_Frame {width: number; height: number; stride: number; pixels: Uint8Array;} + +export function decode_pixel_frame(buffer: ArrayBuffer): Pixel_Frame { + if (buffer.byteLength < PIXEL_FRAME_HEADER_SIZE) throw new Error("RVP1 frame is shorter than its header"); + const bytes = new Uint8Array(buffer); + if (String.fromCharCode(...bytes.subarray(0, 4)) !== "RVP1") throw new Error("RVP1 magic is invalid"); + const view = new DataView(buffer, 0, PIXEL_FRAME_HEADER_SIZE); + const width = view.getUint32(4, true), height = view.getUint32(8, true), stride = view.getUint32(12, true); + if (!width || !height || stride < width * 4) throw new Error("RVP1 dimensions or stride are invalid"); + const payload_size = stride * height; + if (!Number.isSafeInteger(payload_size) || buffer.byteLength < PIXEL_FRAME_HEADER_SIZE + payload_size) throw new Error("RVP1 pixel payload is truncated"); + return {width, height, stride, pixels: bytes.subarray(PIXEL_FRAME_HEADER_SIZE, PIXEL_FRAME_HEADER_SIZE + payload_size)}; +} +export function pack_pixel_rows(frame: Pixel_Frame): Uint8ClampedArray { + const row_size = frame.width * 4; + if (frame.stride === row_size) return new Uint8ClampedArray(frame.pixels.buffer, frame.pixels.byteOffset, row_size * frame.height); + const packed = new Uint8ClampedArray(row_size * frame.height); + for (let row = 0; row < frame.height; row++) packed.set(frame.pixels.subarray(row * frame.stride, row * frame.stride + row_size), row * row_size); + return packed; +} diff --git a/webapp_gallery/src/runtime/client_performance.ts b/webapp_gallery/src/runtime/client_performance.ts new file mode 100644 index 0000000..269f341 --- /dev/null +++ b/webapp_gallery/src/runtime/client_performance.ts @@ -0,0 +1,19 @@ +import type {Gallery_Client_Performance} from "../protocol/gallery_types"; +interface Timed_Sample {timestamp: number; value: number;} +interface Statistics {average: number; deviation: number; p50: number; p95: number; p99: number;} +function statistics(values: number[]): Statistics {if (!values.length) return {average: 0, deviation: 0, p50: 0, p95: 0, p99: 0}; const sorted = values.toSorted((left, right) => left - right); const average = sorted.reduce((sum, value) => sum + value, 0) / sorted.length; const deviation = Math.sqrt(sorted.reduce((sum, value) => sum + (value - average) ** 2, 0) / sorted.length); const percentile = (ratio: number) => sorted[Math.max(0, Math.ceil(ratio * sorted.length) - 1)]; return {average, deviation, p50: percentile(.5), p95: percentile(.95), p99: percentile(.99)};} +function trim(samples: Timed_Sample[], after: number): void {while (samples.length && samples[0].timestamp < after) samples.shift();} +function rate(samples: Timed_Sample[], now: number): number {trim(samples, now - 1000); return samples.length < 2 ? 0 : (samples.length - 1) * 1000 / Math.max(1, samples.at(-1)!.timestamp - samples[0].timestamp);} + +export class Client_Performance { + private transport_samples: Timed_Sample[] = []; private presentation_samples: Timed_Sample[] = []; private display_samples: Timed_Sample[] = []; private round_trip_samples: Timed_Sample[] = []; + private last_animation_frame = 0; private last_pixel_receive = 0; private last_pixel_change = 0; private previous_signature: number | null = null; + private changed_frames = 0; private duplicate_frames = 0; private overwritten_frames = 0; private timeout_count = 0; + record_animation_frame(now: number): void {if (this.last_animation_frame) this.display_samples.push({timestamp: now, value: Math.max(0, now - this.last_animation_frame)}); this.last_animation_frame = now; trim(this.display_samples, now - 10_000);} + record_pixel(now: number, signature: number, overwritten: boolean): void {this.transport_samples.push({timestamp: now, value: 0}); this.last_pixel_receive = now; if (signature !== this.previous_signature) {this.changed_frames++; this.last_pixel_change = now;} else this.duplicate_frames++; this.previous_signature = signature; if (overwritten) this.overwritten_frames++;} + record_presentation(now: number): void {this.presentation_samples.push({timestamp: now, value: 0});} + record_round_trip(now: number, value: number): void {this.round_trip_samples.push({timestamp: now, value}); trim(this.round_trip_samples, now - 10_000);} + record_timeout(): void {this.timeout_count++;} + reset(): void {this.transport_samples=[]; this.presentation_samples=[]; this.display_samples=[]; this.round_trip_samples=[]; this.last_animation_frame=0; this.last_pixel_receive=0; this.last_pixel_change=0; this.previous_signature=null; this.changed_frames=0; this.duplicate_frames=0; this.overwritten_frames=0; this.timeout_count=0;} + snapshot(now: number, websocket_buffered_bytes: number): Gallery_Client_Performance {trim(this.round_trip_samples, now - 10_000); trim(this.display_samples, now - 10_000); const display=statistics(this.display_samples.map(item=>item.value)); const round_trip=statistics(this.round_trip_samples.map(item=>item.value)); return {transport_fps:rate(this.transport_samples,now),presentation_fps:rate(this.presentation_samples,now),display_interval_latest_ms:this.display_samples.at(-1)?.value??0,display_interval_ms:display.p50,display_interval_average_ms:display.average,display_interval_p95_ms:display.p95,display_interval_p99_ms:display.p99,display_interval_deviation_ms:display.deviation,frame_round_trip_ms:this.round_trip_samples.at(-1)?.value??0,frame_round_trip_average_ms:round_trip.average,frame_round_trip_p95_ms:round_trip.p95,frame_round_trip_p99_ms:round_trip.p99,frame_round_trip_deviation_ms:round_trip.deviation,changed_pixel_frames:this.changed_frames,duplicate_pixel_frames:this.duplicate_frames,overwritten_pixel_frames:this.overwritten_frames,frame_request_timeout_count:this.timeout_count,last_pixel_receive_age_ms:this.last_pixel_receive?Math.max(0,now-this.last_pixel_receive):0,last_pixel_change_age_ms:this.last_pixel_change?Math.max(0,now-this.last_pixel_change):0,websocket_buffered_bytes};} +} diff --git a/webapp_gallery/src/runtime/frame_request_controller.ts b/webapp_gallery/src/runtime/frame_request_controller.ts new file mode 100644 index 0000000..340bdb0 --- /dev/null +++ b/webapp_gallery/src/runtime/frame_request_controller.ts @@ -0,0 +1,9 @@ +export const FRAME_TIMEOUT_MS = 1500; +export class Frame_Request_Controller { + private pending = false; private started_at = 0; private timeout: number | undefined; + constructor(private readonly send_frame: () => boolean, private readonly on_timeout: () => void, private readonly on_round_trip: (now: number, value: number) => void) {} + request_frame(now: number): boolean {if (this.pending || !this.send_frame()) return false; this.pending=true; this.started_at=now; this.timeout=window.setTimeout(()=>{this.pending=false;this.started_at=0;this.timeout=undefined;this.on_timeout();},FRAME_TIMEOUT_MS); return true;} + receive_frame(now: number): void {if (!this.pending) return; this.pending=false; window.clearTimeout(this.timeout); this.timeout=undefined; this.on_round_trip(now,Math.max(0,now-this.started_at)); this.started_at=0;} + cancel(): void {this.pending=false;this.started_at=0;window.clearTimeout(this.timeout);this.timeout=undefined;} + is_pending(): boolean {return this.pending;} +} diff --git a/webapp_gallery/src/runtime/pixel_presenter.ts b/webapp_gallery/src/runtime/pixel_presenter.ts new file mode 100644 index 0000000..2168c3a --- /dev/null +++ b/webapp_gallery/src/runtime/pixel_presenter.ts @@ -0,0 +1,8 @@ +import {decode_pixel_frame, pack_pixel_rows, type Pixel_Frame} from "../protocol/pixel_frame"; +export class Pixel_Presenter { + private latest_frame: Pixel_Frame | null = null; private canvas: HTMLCanvasElement | null = null; private context: CanvasRenderingContext2D | null = null; + attach(canvas: HTMLCanvasElement): void {this.canvas=canvas; this.context=canvas.getContext("2d",{alpha:false});} + detach(): void {this.canvas=null;this.context=null;this.latest_frame=null;} + accept(buffer: ArrayBuffer): {signature: number; overwritten: boolean} {const frame=decode_pixel_frame(buffer); let hash=(2166136261^frame.width^(frame.height<<16))>>>0; const count=Math.min(4096,frame.pixels.length); for(let index=0;index>>0;} const overwritten=this.latest_frame!==null;this.latest_frame=frame;return {signature:hash,overwritten};} + present(): boolean {const frame=this.latest_frame, canvas=this.canvas, context=this.context;if(!frame||!canvas||!context)return false;this.latest_frame=null;if(canvas.width!==frame.width||canvas.height!==frame.height){canvas.width=frame.width;canvas.height=frame.height;}const pixels=new Uint8ClampedArray(frame.width*frame.height*4);pixels.set(pack_pixel_rows(frame));context.putImageData(new ImageData(pixels,frame.width,frame.height),0,0);return true;} +} diff --git a/webapp_gallery/src/session/gallery_plot_session.ts b/webapp_gallery/src/session/gallery_plot_session.ts new file mode 100644 index 0000000..8e743d5 --- /dev/null +++ b/webapp_gallery/src/session/gallery_plot_session.ts @@ -0,0 +1,45 @@ +import {build_controls} from "../protocol/gallery_descriptor"; +import {parse_gallery_response} from "../protocol/gallery_parser"; +import type {Gallery_Case, Gallery_Frame_Mode, Gallery_Performance_Capture, Gallery_Render_Plan, Json_Primitive, Json_Value} from "../protocol/gallery_types"; +import {Gallery_Socket, gallery_socket_url} from "../transport/gallery_socket"; +import {Client_Performance} from "../runtime/client_performance"; +import {Frame_Request_Controller} from "../runtime/frame_request_controller"; +import {Pixel_Presenter} from "../runtime/pixel_presenter"; +import {EMPTY_PLOT_SNAPSHOT, type Gallery_Plot_Snapshot} from "./gallery_plot_snapshot"; + +export class Gallery_Plot_Session { + private snapshot: Gallery_Plot_Snapshot = EMPTY_PLOT_SNAPSHOT; + private readonly listeners = new Set<() => void>(); + private readonly socket: Gallery_Socket; + private readonly presenter = new Pixel_Presenter(); + private readonly client_performance = new Client_Performance(); + private readonly frame_controller: Frame_Request_Controller; + private disposed = false; private visible = false; private streams_paused = false; private reconnect_timer: number | undefined; private last_observe_at = 0; private sent_width = 0; private sent_height = 0; private received_frames = 0; + constructor(readonly gallery_case: Gallery_Case, readonly frame_mode: Gallery_Frame_Mode) { + this.socket = new Gallery_Socket(gallery_socket_url(), {on_json: raw=>this.receive_json(raw),on_binary: buffer=>this.receive_pixels(buffer),on_state: state=>this.receive_socket_state(state)}); + this.frame_controller = new Frame_Request_Controller(()=>this.socket.send("frame"),()=>{this.client_performance.record_timeout();if(this.stream_active())this.request_frame(performance.now());},(now,value)=>this.client_performance.record_round_trip(now,value)); + } + subscribe = (listener: () => void): (() => void) => {this.listeners.add(listener);return()=>this.listeners.delete(listener);}; + get_snapshot = (): Gallery_Plot_Snapshot => this.snapshot; + private update(patch: Partial): void {this.snapshot={...this.snapshot,...patch};this.listeners.forEach(listener=>listener());} + connect(): void {if(this.disposed)return;window.clearTimeout(this.reconnect_timer);this.socket.connect();} + dispose(): void {this.disposed=true;window.clearTimeout(this.reconnect_timer);this.frame_controller.cancel();if(this.snapshot.ready)this.socket.send("hide");this.socket.close();this.presenter.detach();this.listeners.clear();} + attach_canvas(canvas: HTMLCanvasElement): void {this.presenter.attach(canvas);} + detach_canvas(): void {this.presenter.detach();} + set_activity(visible: boolean, streams_paused: boolean): void {const was_active=this.stream_active();this.visible=visible;this.streams_paused=streams_paused;if(visible&&!this.socket.is_open())this.connect();const active=this.stream_active();if(this.snapshot.ready&&active!==was_active)this.socket.send(active?"show":"hide");if(active)this.request_frame(performance.now());} + private stream_active(): boolean {return this.visible&&!this.streams_paused;} + private receive_socket_state(state: "connecting"|"ready"|"error"|"closed"): void {if(this.disposed)return;if(state==="ready"){this.update({connection_state:"ready",protocol_error:""});this.socket.send("gallery_open",{case:this.gallery_case.id,frame_mode:this.frame_mode.id});}else{this.update({connection_state:state,ready:state==="error"?this.snapshot.ready:false});if(state==="closed"&&this.visible)this.reconnect_timer=window.setTimeout(()=>this.connect(),1000);}} + private receive_json(raw: string): void {try{const response=parse_gallery_response(raw);if(!response)return;if(response.type==="error"){this.update({notice:Object.values(response.field_errors??{})[0]??response.message});return;}if(response.type==="observer_state"){this.update({telemetry:response.telemetry,performance_capture:response.telemetry.performance_capture??this.snapshot.performance_capture,frame_count:this.received_frames});return;}if(response.type==="case_state"||response.type==="refresh_state"){const controls=response.controls;this.update({ready:true,controls:build_controls(controls?.resources),actions:response.actions?.data??[],telemetry:response.telemetry??{},render_plan:controls?.render_plan??null,performance_capture:controls?.performance_capture??response.telemetry?.performance_capture??null,notice:response.notice??"",frame_count:this.received_frames,protocol_error:""});if(this.stream_active()){this.socket.send("show");this.request_frame(performance.now());}}}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"协议解析失败"});this.socket.close();}} + private receive_pixels(buffer: ArrayBuffer): void {try{const now=performance.now();this.frame_controller.receive_frame(now);const accepted=this.presenter.accept(buffer);this.received_frames++;this.client_performance.record_pixel(now,accepted.signature,accepted.overwritten);if(this.frame_mode.request_after_response&&this.stream_active())this.request_frame(now);}catch(error){this.update({connection_state:"error",protocol_error:error instanceof Error?error.message:"像素协议错误"});this.socket.close();}} + tick(now: number): void {if(!this.stream_active())return;if(this.presenter.present())this.client_performance.record_presentation(now);if(this.frame_mode.request_on_animation_frame)this.request_frame(now);if(this.snapshot.ready&&now-this.last_observe_at>=650){this.last_observe_at=now;this.socket.send("gallery_observe",{client_metrics:this.client_performance.snapshot(now,this.socket.buffered_bytes()) as unknown as Json_Value});}} + request_frame(now=performance.now(), explicit=false): boolean {if(!this.snapshot.ready||!this.socket.is_open()||(!explicit&&!this.stream_active())||(explicit&&!this.visible)||(!explicit&&!this.frame_mode.automatic))return false;return this.frame_controller.request_frame(now);} + resize(width: number,height: number): void {width=Math.max(240,Math.round(width));height=Math.max(180,Math.round(height));if(width===this.sent_width&&height===this.sent_height)return;this.sent_width=width;this.sent_height=height;if(this.socket.is_open())this.socket.send("resize",{width,height});} + pointer(type: "pointer_move"|"pointer_press"|"pointer_release", payload: Record): void {this.socket.send(type,payload);} + leave(): void {this.socket.send("leave");} + wheel(payload: Record): void {this.socket.send("wheel",payload);} + key(type: "key_press"|"key_release",payload: Record): void {this.socket.send(type,payload);} + patch(target: string,patch: Record): void {this.socket.send("gallery_patch",{target,patch});} + action(action: string,argument?: Json_Primitive): void {this.socket.send("gallery_action",argument===undefined?{action}:{action,argument});} + refresh(): void {this.socket.send("gallery_refresh",{client_metrics:this.client_performance.snapshot(performance.now(),this.socket.buffered_bytes()) as unknown as Json_Value});} + reset_monitoring(): void {this.client_performance.reset();this.socket.send("gallery_reset_monitoring");} +} diff --git a/webapp_gallery/src/session/gallery_plot_snapshot.ts b/webapp_gallery/src/session/gallery_plot_snapshot.ts new file mode 100644 index 0000000..c47eaef --- /dev/null +++ b/webapp_gallery/src/session/gallery_plot_snapshot.ts @@ -0,0 +1,3 @@ +import type {Connection_State, Gallery_Action, Gallery_Control, Gallery_Performance_Capture, Gallery_Render_Plan, Gallery_Telemetry} from "../protocol/gallery_types"; +export interface Gallery_Plot_Snapshot {connection_state: Connection_State; ready: boolean; controls: Gallery_Control[]; actions: Gallery_Action[]; telemetry: Gallery_Telemetry; render_plan: Gallery_Render_Plan | null; performance_capture: Gallery_Performance_Capture | null; notice: string; frame_count: number; protocol_error: string;} +export const EMPTY_PLOT_SNAPSHOT: Gallery_Plot_Snapshot = {connection_state:"connecting",ready:false,controls:[],actions:[],telemetry:{},render_plan:null,performance_capture:null,notice:"",frame_count:0,protocol_error:""}; diff --git a/webapp_gallery/src/theme.ts b/webapp_gallery/src/theme.ts new file mode 100644 index 0000000..cea9af5 --- /dev/null +++ b/webapp_gallery/src/theme.ts @@ -0,0 +1,2 @@ +import {createTheme} from "@mui/material/styles"; +export const gallery_theme=createTheme({palette:{mode:"dark",primary:{main:"#59d6c5"},secondary:{main:"#ffbd69"},background:{default:"#07111f",paper:"#0d1b2d"}},shape:{borderRadius:12},typography:{fontFamily:'Inter,"Segoe UI","Microsoft YaHei",sans-serif',h1:{fontWeight:800},h2:{fontWeight:750},button:{textTransform:"none",fontWeight:700}},components:{MuiPaper:{styleOverrides:{root:{backgroundImage:"none",border:"1px solid rgba(132,179,206,.16)"}}},MuiButton:{defaultProps:{disableElevation:true}}}}); diff --git a/webapp_gallery/src/transport/catalog_loader.ts b/webapp_gallery/src/transport/catalog_loader.ts new file mode 100644 index 0000000..548aa43 --- /dev/null +++ b/webapp_gallery/src/transport/catalog_loader.ts @@ -0,0 +1,25 @@ +import {parse_gallery_response} from "../protocol/gallery_parser"; +import {Gallery_Socket, gallery_socket_url} from "./gallery_socket"; +import type {Gallery_Catalog} from "../protocol/gallery_types"; + +export function load_gallery_catalog(on_catalog: (catalog: Gallery_Catalog) => void, on_state: (state: string, message: string) => void): () => void { + let disposed = false, completed = false, reconnect_timer: number | undefined; + let socket: Gallery_Socket; + const connect = () => { + socket = new Gallery_Socket(gallery_socket_url(), { + on_state: state => { + if (disposed) return; + if (state === "ready") socket.send("gallery_catalog"); + else if (state === "error") on_state("error", "目录连接错误 · 自动重连"); + else if (state === "closed" && !completed) reconnect_timer = window.setTimeout(connect, 1000); + }, + on_binary: () => undefined, + on_json: raw => { + try {const response = parse_gallery_response(raw); if (response?.type === "catalog") {completed = true; on_catalog(response); on_state("ready", response.navigation.catalog_loaded_text); socket.close();} else if (response?.type === "error") on_state("error", response.message);} catch (error) {on_state("error", error instanceof Error ? error.message : "目录解析失败");} + } + }); + socket.connect(); + }; + connect(); + return () => {disposed = true; window.clearTimeout(reconnect_timer); socket?.close();}; +} diff --git a/webapp_gallery/src/transport/gallery_socket.ts b/webapp_gallery/src/transport/gallery_socket.ts new file mode 100644 index 0000000..9537e4c --- /dev/null +++ b/webapp_gallery/src/transport/gallery_socket.ts @@ -0,0 +1,29 @@ +import {gallery_message} from "../protocol/gallery_messages"; +import type {Json_Value} from "../protocol/gallery_types"; + +export interface Gallery_Socket_Events {on_json(raw: string): void; on_binary(buffer: ArrayBuffer): void; on_state(state: "connecting" | "ready" | "error" | "closed"): void;} +export function gallery_socket_url(location_value: Location = window.location): string { + const query = new URLSearchParams(location_value.search); + const hosted = location_value.protocol !== "file:" && ["/", "/index.html", "/gallery", "/gallery/"].includes(location_value.pathname); + const preview = location_value.port === "63342"; + const port = query.get("port") || (hosted && !preview ? location_value.port : "8848") || "8848"; + const host = query.get("host") || (hosted ? location_value.hostname : "127.0.0.1") || "127.0.0.1"; + return `${location_value.protocol === "https:" ? "wss" : "ws"}://${host}:${port}/renderive/gallery`; +} +export class Gallery_Socket { + private socket: WebSocket | null = null; + constructor(private readonly url: string, private readonly events: Gallery_Socket_Events) {} + connect(): void { + if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) return; + this.events.on_state("connecting"); + const socket = new WebSocket(this.url); this.socket = socket; socket.binaryType = "arraybuffer"; + socket.addEventListener("open", () => {if (this.socket === socket) this.events.on_state("ready");}); + socket.addEventListener("message", event => {if (this.socket !== socket) return; typeof event.data === "string" ? this.events.on_json(event.data) : event.data instanceof ArrayBuffer && this.events.on_binary(event.data);}); + socket.addEventListener("error", () => {if (this.socket === socket) this.events.on_state("error");}); + socket.addEventListener("close", () => {if (this.socket === socket) {this.socket = null; this.events.on_state("closed");}}); + } + close(): void {const socket = this.socket; this.socket = null; socket?.close();} + send(type: string, payload: Record = {}): boolean {if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return false; this.socket.send(gallery_message(type, payload)); return true;} + buffered_bytes(): number {return this.socket?.bufferedAmount ?? 0;} + is_open(): boolean {return this.socket?.readyState === WebSocket.OPEN;} +} diff --git a/webapp_gallery/styles.css b/webapp_gallery/styles.css deleted file mode 100644 index a7220a0..0000000 --- a/webapp_gallery/styles.css +++ /dev/null @@ -1,87 +0,0 @@ -:root { - color-scheme: dark; - font-family: Inter, "Segoe UI Variable", "Microsoft YaHei UI", sans-serif; - --bg: #07090d; --panel: #10141b; --panel2: #151b24; --line: #29313d; - --strong: #3b4655; --text: #f2f5f8; --muted: #929eac; --accent: #45ddbe; - --blue: #6aa9ff; --warning: #ffd166; --danger: #ff6885; -} -* { box-sizing: border-box; } -html { background: var(--bg); } -body { - margin: 0; min-width: 320px; min-height: 100vh; color: var(--text); - background: linear-gradient(rgba(255,255,255,.018) 1px, transparent 1px), - linear-gradient(90deg, rgba(255,255,255,.018) 1px, transparent 1px), - radial-gradient(circle at 16% -10%, rgba(69,221,190,.12), transparent 34rem), var(--bg); - background-size: 24px 24px, 24px 24px, auto, auto; -} -button, input, select { font: inherit; } button { color: inherit; } -h1, h2, h3, p { margin-top: 0; } -.eyebrow { margin: 0 0 5px; color: var(--accent); font: 700 10px/1.2 ui-monospace, monospace; letter-spacing: .15em; text-transform: uppercase; } -.topbar { position: sticky; z-index: 20; top: 0; display: flex; align-items: center; justify-content: space-between; gap: 22px; min-height: 76px; padding: 12px clamp(18px,4vw,56px); border-bottom: 1px solid #ffffff14; background: #07090dea; backdrop-filter: blur(20px); } -.brand, .topbar-actions { display: flex; align-items: center; gap: 14px; } -.brand-mark { display: grid; place-items: center; width: 46px; height: 46px; border-radius: 12px; color: #07110f; background: var(--accent); font: 800 16px/1 ui-monospace, monospace; } -h1 { margin: 0; font-size: clamp(17px,2vw,23px); } -.connection-status, .card-socket { display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font: 600 11px/1 ui-monospace, monospace; } -.connection-status i, .card-socket i { width: 8px; height: 8px; border-radius: 50%; background: var(--warning); box-shadow: 0 0 12px currentColor; } -[data-state="ready"] i { background: var(--accent); } [data-state="error"] i { background: var(--danger); } -.card-health { display:flex; align-items:flex-end; flex-direction:column; gap:7px; } -.motion-status { display:inline-flex; align-items:center; gap:6px; color:var(--muted); font:600 9px/1 ui-monospace,monospace; } -.motion-status i { width:6px; height:6px; border-radius:50%; background:var(--warning); } -.motion-status[data-state="moving"] { color:var(--accent); }.motion-status[data-state="moving"] i { background:var(--accent); box-shadow:0 0 10px var(--accent); animation:motion-pulse .9s ease-in-out infinite alternate; } -.motion-status[data-state="duplicate"] { color:var(--warning); }.motion-status[data-state="duplicate"] i { background:var(--warning); } -.motion-status[data-state="stalled"] { color:var(--danger); }.motion-status[data-state="stalled"] i { background:var(--danger); } -@keyframes motion-pulse { to { opacity:.35; transform:scale(.72); } } -.quiet-button, .open-menu, .frame-button, .icon-button, .menu-tabs button, .menu-refresh-bar button, .category-filter button, .mode-tabs button, .action-button { border: 1px solid var(--line); border-radius: 8px; background: var(--panel2); cursor: pointer; transition: .16s border-color,.16s background,.16s transform; } -button:hover { border-color: var(--accent); } button:active { transform: translateY(1px); } -.quiet-button { padding: 9px 13px; font-size: 12px; } -.hero { display: grid; grid-template-columns: minmax(300px,1fr) minmax(430px,.9fr); align-items: end; gap: 40px; max-width: 1680px; margin: auto; padding: clamp(32px,5vw,64px) clamp(18px,4vw,56px) 28px; } -.hero h2 { margin-bottom: 10px; font-size: clamp(27px,4vw,47px); letter-spacing: -.045em; } -.hero > div > p:last-child { margin: 0; max-width: 750px; color: var(--muted); line-height: 1.7; } -.metrics { display: grid; grid-template-columns: repeat(4,1fr); gap: 1px; margin: 0; border: 1px solid var(--line); background: var(--line); } -.metrics div { padding: 17px; background: var(--panel); } .metrics dt { color: var(--text); font: 750 clamp(20px,3vw,33px)/1 ui-monospace,monospace; } .metrics dd { margin: 8px 0 0; color: var(--muted); font-size: 10px; } -.mode-tabs { display: grid; grid-template-columns: repeat(auto-fit,minmax(220px,1fr)); gap: 10px; max-width: 1680px; margin: auto; padding: 0 clamp(18px,4vw,56px) 18px; } -.mode-tabs button { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; color: var(--muted); text-align: left; } -.mode-tabs button span { color: var(--text); font-weight: 750; } .mode-tabs button code { font-size: 10px; } -.mode-tabs button.active { border-color: var(--accent); background: #123029; box-shadow: inset 0 0 0 1px #45ddbe35; } -.category-filter { display: flex; gap: 7px; overflow-x: auto; max-width: 1680px; margin: auto; padding: 0 clamp(18px,4vw,56px) 20px; } -.category-filter button { flex: 0 0 auto; padding: 8px 11px; color: var(--muted); font-size: 11px; } -.category-filter button.active { color: #06110e; border-color: var(--accent); background: var(--accent); } -.pages, .mode-page { max-width: 1680px; margin: auto; } -.mode-page[hidden] { display: none; } -.page-header { display: flex; align-items: end; justify-content: space-between; gap: 24px; padding: 3px clamp(18px,4vw,56px) 18px; } -.page-header h2 { margin: 0; font-size: 23px; }.page-strategy { margin-bottom: 5px; color: var(--accent); font: 700 10px/1 ui-monospace,monospace; }.page-description { max-width: 650px; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.6; text-align: right; } -.gallery { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 18px; padding: 0 clamp(18px,4vw,56px) 70px; } -.loading-card { display: grid; place-items: center; gap: 12px; min-height: 360px; margin: 0 clamp(18px,4vw,56px); border: 1px dashed var(--strong); color: var(--muted); } -.loader { width: 18px; height: 18px; border: 2px solid var(--strong); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } -.plot-card { min-width: 0; overflow: hidden; border: 1px solid var(--line); border-top-color: var(--mode-accent); border-radius: 13px; background: linear-gradient(180deg,#ffffff08,transparent 30%),var(--panel); box-shadow: 0 20px 55px #0000002e; } -.plot-card[hidden] { display:none; } -.card-header, .card-footer { display:flex; align-items:center; justify-content:space-between; gap:15px; padding:13px 15px; }.card-header { border-bottom:1px solid var(--line); }.card-category { display:block; margin-bottom:4px; color:var(--blue); font:700 9px/1 ui-monospace,monospace; letter-spacing:.1em; }.card-title { margin:0; font-size:17px; } -.canvas-shell { position:relative; height:clamp(245px,27vw,350px); outline:none; background:#060a11; cursor:crosshair; }.canvas-shell:focus-visible { box-shadow:inset 0 0 0 2px var(--accent); }.canvas-shell canvas { display:block; width:100%; height:100%; }.canvas-hint { position:absolute; right:9px; bottom:8px; padding:5px 7px; border:1px solid #ffffff1e; border-radius:5px; color:#ffffffa8; background:#04070bc9; font:9px/1 ui-monospace,monospace; pointer-events:none; }.canvas-loading { position:absolute; inset:0; display:grid; place-content:center; justify-items:center; gap:9px; color:var(--muted); background:#080d15; }.plot-card[data-ready="true"] .canvas-loading { display:none; } -.performance-strip { display:grid; grid-template-columns:repeat(auto-fit,minmax(105px,1fr)); gap:1px; margin:0; border-block:1px solid var(--line); background:var(--line); }.performance-strip div { min-width:0; padding:9px 8px; background:#0c1118; }.performance-strip dt { min-width:0; overflow:visible; color:var(--accent); font:700 11px/1.35 ui-monospace,monospace; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; word-break:break-word; }.performance-strip [data-format="text"] dt,.performance-strip [data-format="duration_enum"] dt { font-size:10px; }.performance-strip dd { margin:5px 0 0; color:var(--muted); font-size:8px; line-height:1.35; white-space:normal; overflow-wrap:anywhere; word-break:break-word; } -.limit-flags { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; padding:7px 10px; border-bottom:1px solid var(--line); color:var(--muted); background:#090e15; font:9px/1.35 ui-monospace,monospace; }.limit-flags strong { grid-column:1/-1; color:var(--text); }.limit-flags span { min-width:0; padding:6px 7px; border:1px solid var(--line); border-radius:4px; line-height:1.45; white-space:normal; overflow-wrap:anywhere; word-break:break-word; }.limit-flags span[data-active="true"] { border-color:#ffb84d88; color:#ffcf82; background:#ff9d1712; }.limit-flags b { color:inherit; white-space:normal; overflow-wrap:anywhere; }.plot-card[data-observer-visible="false"] .limit-flags { display:none; } -.kernel-observer-panel { border-bottom:1px solid var(--line); background:#080d14; font-family:ui-monospace,monospace; } -.kernel-observer-panel > header { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:9px 11px; border-bottom:1px solid var(--line); color:var(--muted); font-size:8px; flex-wrap:wrap; } -.kernel-observer-panel > header div:first-child { display:flex; align-items:center; gap:9px; min-width:0; flex-wrap:wrap; }.kernel-observer-panel > header span { color:var(--accent); letter-spacing:.08em; overflow-wrap:anywhere; }.kernel-observer-panel > header strong,.kernel-observer-panel > header b { color:var(--text); font-weight:700; overflow-wrap:anywhere; } -.observer-counters,.latency-summary,.client-summary,.latency-details { display:grid; gap:1px; background:var(--line); } -.observer-counters { grid-template-columns:repeat(auto-fit,minmax(90px,1fr)); }.latency-summary,.client-summary { grid-template-columns:repeat(auto-fit,minmax(105px,1fr)); border-top:1px solid var(--line); }.latency-details { grid-template-columns:repeat(auto-fit,minmax(145px,1fr)); border-top:1px solid var(--line); } -.observer-counters div,.latency-summary div,.client-summary div,.latency-details div { min-width:0; padding:8px 7px; background:#0b1119; } -.kernel-observer-panel small { display:block; min-width:0; overflow:visible; margin-bottom:5px; color:var(--muted); font-size:7px; line-height:1.35; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; word-break:break-word; } -.kernel-observer-panel b { display:block; min-width:0; overflow:visible; color:#b7c7dc; font-size:9px; line-height:1.35; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; word-break:break-word; } -.latency-summary b { color:var(--accent); font-size:10px; }.latency-summary .critical { background:#10201e; }.latency-summary .critical b { color:#72f3d9; } -.plot-card[data-observer-visible="false"] .kernel-observer-panel { display:none; } -.card-meta { display:grid; grid-template-columns:1fr auto; gap:17px; padding:13px 15px; }.card-description { margin:0; color:var(--muted); font-size:11px; line-height:1.55; }.card-meta dl { display:flex; margin:0; }.card-meta dl div { min-width:52px; padding-left:11px; border-left:1px solid var(--line); }.card-meta dt { font:700 14px/1 ui-monospace,monospace; }.card-meta dd { margin:5px 0 0; color:var(--muted); font-size:9px; } -.card-footer { border-top:1px solid var(--line); background:#0000001e; }.card-footer code { color:var(--accent); font-size:10px; }.card-footer > div { display:flex; gap:7px; }.open-menu,.frame-button { padding:7px 9px; color:var(--muted); font-size:10px; }.frame-button { color:var(--text); } -.context-menu { position:fixed; z-index:100; width:min(570px,calc(100vw - 24px)); max-height:min(840px,calc(100vh - 24px)); overflow:hidden; border:1px solid var(--strong); border-radius:12px; background:#0f131afa; box-shadow:0 30px 90px #0000009e; backdrop-filter:blur(22px); }.context-menu[hidden] { display:none; } -.menu-header { display:flex; justify-content:space-between; gap:18px; padding:16px 18px 13px; border-bottom:1px solid var(--line); }.menu-header h2 { margin-bottom:6px; font-size:19px; }.menu-header p:last-child { margin:0; color:var(--muted); font-size:10px; line-height:1.5; }.icon-button { flex:0 0 auto; width:31px; height:31px; font-size:20px; } -.menu-tabs { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); padding:8px; border-bottom:1px solid var(--line); }.menu-tabs button { min-width:0; padding:8px 3px; border-color:transparent; color:var(--muted); background:transparent; font-size:9px; white-space:normal; }.menu-tabs button.active { color:var(--text); border-color:var(--strong); background:var(--panel2); } -.menu-refresh-bar { display:flex; justify-content:flex-end; gap:8px; padding:8px 12px 0; }.menu-refresh-bar button { min-width:112px; padding:7px 10px; color:var(--accent); font-size:10px; }.menu-refresh-bar button:disabled { cursor:wait; opacity:.55; } -.menu-body { max-height:calc(min(840px,100vh - 24px) - 232px); overflow:auto; padding:12px; overscroll-behavior:contain; }.control-group { border-bottom:1px solid var(--line); }.control-group + .control-group { margin-top:8px; }.control-group > summary { position:sticky; z-index:2; top:-12px; display:flex; align-items:center; justify-content:space-between; gap:12px; min-height:38px; padding:9px 8px; color:var(--accent); background:#111720f8; cursor:pointer; font:700 10px/1.35 ui-monospace,monospace; list-style-position:inside; }.control-group > summary span { min-width:0; overflow-wrap:anywhere; }.control-group > summary small { flex:0 0 auto; color:var(--muted); font-size:8px; font-weight:600; }.control-group[open] > summary { border-bottom:1px solid var(--line); color:var(--text); } -.control-row { display:grid; grid-template-columns:minmax(145px,1fr) minmax(130px,.75fr); gap:13px; align-items:center; min-height:49px; padding:8px 9px; border-top:1px solid #ffffff0e; }.control-copy label,.action-copy strong { display:block; margin-bottom:4px; font-size:11px; }.control-copy code,.action-copy code { display:block; overflow:hidden; color:var(--muted); font-size:9px; text-overflow:ellipsis; white-space:nowrap; }.control-input { width:100%; min-width:0; padding:7px 8px; border:1px solid var(--strong); border-radius:6px; outline:none; color:var(--text); background:#090d13; font-size:10px; }.control-input:focus { border-color:var(--accent); }input[type="color"].control-input { height:34px; padding:3px; }input[type="checkbox"].control-input { justify-self:end; width:38px; height:20px; accent-color:var(--accent); } -.action-row { display:flex; align-items:center; justify-content:space-between; gap:13px; padding:10px 9px; border-top:1px solid #ffffff0e; }.action-copy { min-width:0; }.action-controls { display:flex; gap:7px; align-items:center; }.action-controls input { width:105px; }.action-button { padding:7px 10px; color:#07110f; border-color:var(--accent); background:var(--accent); font-size:10px; font-weight:700; } -.telemetry-grid { display:grid; grid-template-columns:minmax(165px,.7fr) 1fr; margin:0; border:1px solid var(--line); }.telemetry-grid dt,.telemetry-grid dd { margin:0; padding:8px 10px; border-bottom:1px solid var(--line); font:10px/1.4 ui-monospace,monospace; overflow-wrap:anywhere; }.telemetry-grid dt { color:var(--muted); background:#ffffff06; }.telemetry-grid dd { color:var(--accent); } -.descriptor-group .telemetry-grid { border:0; } -.performance-capture-view,.render-plan-view{display:grid;gap:12px;min-width:0}.capture-section{min-width:0;padding:12px;border:1px solid var(--line);border-radius:8px;background:#0b1017}.capture-section>h3{margin:0 0 10px;color:#dce7f4;font-size:12px;letter-spacing:.06em;text-transform:uppercase}.capture-section>h4{margin:12px 0 7px;color:#9fb2c8;font-size:11px}.capture-control-row,.capture-browser-selectors,.capture-plan-selectors{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.capture-control-row button,.capture-frame-list button,.capture-plan-selectors select,.capture-browser-selectors select{border:1px solid #41556e;border-radius:5px;background:#121b26;color:#e9eef5;padding:7px 9px}.capture-control-row input{width:78px;border:1px solid #41556e;border-radius:5px;background:#090e15;color:#fff;padding:7px}.capture-control-row button:disabled{opacity:.45}.capture-progress{margin-top:9px;color:#91a5bb;font:11px ui-monospace,monospace}.capture-progress[data-active="true"]{color:#45ddbe}.capture-frame-list{display:grid;gap:5px;margin-top:9px;max-height:190px;overflow:auto}.capture-frame-list button{text-align:left;font:11px ui-monospace,monospace}.capture-frame-list button[data-selected="true"]{border-color:#45ddbe;background:#10251f}.capture-cache{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:7px}.capture-cache>h3{grid-column:1/-1}.capture-cache-card{display:grid;gap:4px;padding:8px;border:1px solid #2a394c;border-radius:5px;background:#0e151e}.capture-cache-card strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.capture-cache-card span{color:#91a5bb;font:10px ui-monospace,monospace}.capture-dag-section,.capture-timeline-section{overflow:auto}.capture-dag,.capture-timeline{display:block;min-width:850px;width:100%;height:auto}.capture-dag-heading{fill:#91a5bb;font:bold 11px ui-monospace,monospace;letter-spacing:.12em}.capture-dag-edge{fill:none;stroke:#536a84;stroke-width:1.4;opacity:.75}.capture-dag-node{cursor:pointer;outline:none}.capture-dag-node rect{rx:6;fill:#151d28;stroke:#45566c;stroke-width:1.2}.capture-dag-node[data-kind="prepare"] rect{fill:#10241f;stroke:#2f8873}.capture-dag-node[data-kind="paint"] rect{fill:#132037;stroke:#477bc1}.capture-dag-node[data-kind="composite"] rect{fill:#2b2110;stroke:#b98a38}.capture-dag-node[data-critical="true"] rect{stroke:#ffbb4d;stroke-width:2}.capture-dag-node[data-selected="true"] rect{stroke:#ff668a;stroke-width:3}.capture-dag-node text{fill:#e9eef5;font:10px ui-monospace,monospace;pointer-events:none}.capture-dag-node text:nth-of-type(n+2){fill:#9fb2c8}.capture-time-grid{stroke:#29384a;stroke-width:1}.capture-time-label,.capture-worker-label{fill:#91a5bb;font:9px ui-monospace,monospace}.capture-time-label{text-anchor:middle}.capture-time-block rect{fill:#3c78c2;stroke:#74a8e8;rx:3;cursor:pointer}.capture-time-block[data-selected="true"] rect{fill:#c44869;stroke:#ff8ca8}.capture-metrics{display:grid;grid-template-columns:minmax(120px,1fr) minmax(140px,1.5fr);gap:1px;margin:0;background:#233044}.capture-metrics dt,.capture-metrics dd{margin:0;padding:7px;background:#0e151e}.capture-metrics dt{color:#91a5bb}.capture-metrics dd{color:#eef4fb;font:11px ui-monospace,monospace;overflow-wrap:anywhere}.capture-frequency-list{margin-top:8px;color:#9fb2c8;font:10px ui-monospace,monospace}.capture-plan-selectors{margin-bottom:9px}.capture-plan-selectors select,.capture-browser-selectors select{min-width:145px}.capture-node-detail{border-color:#394c64} -.menu-footer { display:flex; justify-content:space-between; gap:14px; padding:10px 14px; border-top:1px solid var(--line); color:var(--muted); font-size:9px; }.menu-footer code { color:var(--accent); } -.toast { position:fixed; z-index:140; left:50%; bottom:22px; max-width:min(560px,calc(100vw - 30px)); padding:11px 15px; border:1px solid var(--strong); border-radius:8px; background:var(--panel2); box-shadow:0 18px 50px #00000073; transform:translateX(-50%); font-size:11px; }.toast[data-error="true"] { border-color:var(--danger); } -@media (max-width:980px) { .hero { grid-template-columns:1fr; }.gallery { grid-template-columns:1fr; }.mode-tabs button { flex-direction:column; align-items:flex-start; }.page-header { align-items:flex-start; flex-direction:column; }.page-description { text-align:left; } } -@media (max-width:650px) { .topbar { align-items:flex-start; }.topbar-actions { align-items:flex-end; flex-direction:column; }.connection-status span { display:none; }.metrics { grid-template-columns:repeat(2,1fr); }.mode-tabs { grid-template-columns:1fr; }.mode-tabs button { flex-direction:row; }.card-meta { grid-template-columns:1fr; }.performance-strip { grid-template-columns:repeat(auto-fit,minmax(92px,1fr)); }.limit-flags { grid-template-columns:1fr; }.observer-counters { grid-template-columns:repeat(auto-fit,minmax(80px,1fr)); }.latency-summary,.client-summary,.latency-details { grid-template-columns:repeat(auto-fit,minmax(100px,1fr)); }.context-menu { inset:auto 6px 6px!important; width:auto; max-height:calc(100vh - 12px); }.telemetry-grid { grid-template-columns:minmax(120px,.8fr) 1fr; }.menu-tabs button { font-size:8px; } } diff --git a/webapp_gallery/tests/components/control_field.test.tsx b/webapp_gallery/tests/components/control_field.test.tsx new file mode 100644 index 0000000..4258bf5 --- /dev/null +++ b/webapp_gallery/tests/components/control_field.test.tsx @@ -0,0 +1,3 @@ +import {fireEvent,render,screen} from "@testing-library/react";import {describe,expect,it,vi} from "vitest";import {Control_Field} from "../../src/inspector/control_field"; +const control={id:"gain",target:"plot",path:["gain"],label:"Gain",api:"gain",description:"",group:"Plot",input:"number",minimum:0,maximum:10,options:[],value:3}; +describe("control draft",()=>{it("does not overwrite an active draft during backend refresh",()=>{const commit=vi.fn();const view=render();const input=screen.getByLabelText("Gain");fireEvent.focus(input);fireEvent.change(input,{target:{value:"7"}});view.rerender();expect(input).toHaveValue(7);fireEvent.blur(input);expect(commit).toHaveBeenCalledWith(7);});}); diff --git a/webapp_gallery/tests/dag/dag_model.test.ts b/webapp_gallery/tests/dag/dag_model.test.ts new file mode 100644 index 0000000..04a4dd1 --- /dev/null +++ b/webapp_gallery/tests/dag/dag_model.test.ts @@ -0,0 +1,2 @@ +import {describe,expect,it} from "vitest";import {build_dag_model} from "../../src/dag/dag_model"; +describe("DAG view model",()=>{it("uses node_id for current plan and capture overlays",()=>{const plan={version:2,nodes:[{node_id:7,name:"paint",owner:"plot",kind:"paint"}],edges:[],renderables:[]};const frame={frame_id:1,render_plan_version:2,render_duration_ns:10,node_executions:[{node_id:7,worker_id:3,start_offset_ns:0,end_offset_ns:10,duration_ns:10}],analysis:{nodes:[{node_id:7,scheduler_wait_ns:1,critical_path_contribution:1,on_critical_path:true}]}};const model=build_dag_model(plan,frame,[],7);expect(model.nodes[0].id).toBe("7");expect(model.nodes[0].data).toMatchObject({duration_ns:10,worker_id:3,critical:true,selected:true});});}); diff --git a/webapp_gallery/tests/e2e/gallery.spec.ts b/webapp_gallery/tests/e2e/gallery.spec.ts new file mode 100644 index 0000000..31947ef --- /dev/null +++ b/webapp_gallery/tests/e2e/gallery.spec.ts @@ -0,0 +1,2 @@ +import {expect,test} from "@playwright/test"; +test("renders the React gallery shell while catalog reconnects",async({page})=>{await page.goto("/");await expect(page.getByText("Renderive 性能画廊")).toBeVisible();await expect(page.getByText("等待后端返回控件与帧策略目录")).toBeVisible();}); diff --git a/webapp_gallery/tests/protocol/gallery_parser.test.ts b/webapp_gallery/tests/protocol/gallery_parser.test.ts new file mode 100644 index 0000000..103d267 --- /dev/null +++ b/webapp_gallery/tests/protocol/gallery_parser.test.ts @@ -0,0 +1,2 @@ +import {describe,expect,it} from "vitest";import {Gallery_Protocol_Error,parse_gallery_response} from "../../src/protocol/gallery_parser"; +describe("gallery parser",()=>{it("accepts known responses",()=>expect(parse_gallery_response('{"type":"observer_state","telemetry":{}}')?.type).toBe("observer_state"));it("ignores future response types",()=>expect(parse_gallery_response('{"type":"future"}')).toBeNull());it("contains malformed input at the boundary",()=>expect(()=>parse_gallery_response("{" )).toThrow(Gallery_Protocol_Error));}); diff --git a/webapp_gallery/tests/protocol/pixel_frame.test.ts b/webapp_gallery/tests/protocol/pixel_frame.test.ts new file mode 100644 index 0000000..c73cae9 --- /dev/null +++ b/webapp_gallery/tests/protocol/pixel_frame.test.ts @@ -0,0 +1,3 @@ +import {describe,expect,it} from "vitest";import {decode_pixel_frame,pack_pixel_rows} from "../../src/protocol/pixel_frame"; +function frame(width:number,height:number,stride:number,payload:number[]):ArrayBuffer {const buffer=new ArrayBuffer(16+payload.length);const bytes=new Uint8Array(buffer);bytes.set([82,86,80,49]);const view=new DataView(buffer);view.setUint32(4,width,true);view.setUint32(8,height,true);view.setUint32(12,stride,true);bytes.set(payload,16);return buffer;} +describe("RVP1 decoder",()=>{it("decodes packed RGBA",()=>{const result=decode_pixel_frame(frame(1,1,4,[1,2,3,4]));expect(result).toMatchObject({width:1,height:1,stride:4});expect([...pack_pixel_rows(result)]).toEqual([1,2,3,4]);});it("removes per-row padding",()=>{const result=decode_pixel_frame(frame(1,2,8,[1,2,3,4,90,91,92,93,5,6,7,8,94,95,96,97]));expect([...pack_pixel_rows(result)]).toEqual([1,2,3,4,5,6,7,8]);});it("rejects invalid magic, stride, and truncated payload",()=>{expect(()=>decode_pixel_frame(frame(1,1,3,[1,2,3]))).toThrow();const truncated=frame(2,2,8,[1,2,3]);expect(()=>decode_pixel_frame(truncated)).toThrow();const magic=frame(1,1,4,[1,2,3,4]);new Uint8Array(magic)[0]=0;expect(()=>decode_pixel_frame(magic)).toThrow();});}); diff --git a/webapp_gallery/tests/runtime/client_performance.test.ts b/webapp_gallery/tests/runtime/client_performance.test.ts new file mode 100644 index 0000000..124a2e7 --- /dev/null +++ b/webapp_gallery/tests/runtime/client_performance.test.ts @@ -0,0 +1,2 @@ +import {describe,expect,it} from "vitest";import {Client_Performance} from "../../src/runtime/client_performance"; +describe("client performance",()=>{it("tracks change, duplicate and overwrite independently",()=>{const performance=new Client_Performance();performance.record_animation_frame(10);performance.record_animation_frame(26);performance.record_pixel(30,1,false);performance.record_pixel(40,1,true);performance.record_pixel(50,2,false);performance.record_presentation(55);performance.record_round_trip(60,12);const value=performance.snapshot(70,9);expect(value.changed_pixel_frames).toBe(2);expect(value.duplicate_pixel_frames).toBe(1);expect(value.overwritten_pixel_frames).toBe(1);expect(value.display_interval_latest_ms).toBe(16);expect(value.websocket_buffered_bytes).toBe(9);});}); diff --git a/webapp_gallery/tests/runtime/frame_request_controller.test.ts b/webapp_gallery/tests/runtime/frame_request_controller.test.ts new file mode 100644 index 0000000..3466242 --- /dev/null +++ b/webapp_gallery/tests/runtime/frame_request_controller.test.ts @@ -0,0 +1,2 @@ +import {afterEach,describe,expect,it,vi} from "vitest";import {FRAME_TIMEOUT_MS,Frame_Request_Controller} from "../../src/runtime/frame_request_controller"; +describe("frame request controller",()=>{afterEach(()=>vi.useRealTimers());it("allows one in-flight request and records binary RTT",()=>{const send=vi.fn(()=>true),round_trip=vi.fn();const controller=new Frame_Request_Controller(send,vi.fn(),round_trip);expect(controller.request_frame(10)).toBe(true);expect(controller.request_frame(11)).toBe(false);controller.receive_frame(26);expect(round_trip).toHaveBeenCalledWith(26,16);expect(controller.request_frame(27)).toBe(true);});it("releases a timed-out request",()=>{vi.useFakeTimers();const timeout=vi.fn();const controller=new Frame_Request_Controller(()=>true,timeout,vi.fn());controller.request_frame(0);vi.advanceTimersByTime(FRAME_TIMEOUT_MS);expect(timeout).toHaveBeenCalledOnce();expect(controller.is_pending()).toBe(false);});}); diff --git a/webapp_gallery/tests/setup.ts b/webapp_gallery/tests/setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/webapp_gallery/tests/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/webapp_gallery/tsconfig.app.json b/webapp_gallery/tsconfig.app.json new file mode 100644 index 0000000..4ceca5c --- /dev/null +++ b/webapp_gallery/tsconfig.app.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2023", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vitest/globals", "@testing-library/jest-dom"] + }, + "include": ["src", "tests", "vite.config.ts"] +} diff --git a/webapp_gallery/tsconfig.app.tsbuildinfo b/webapp_gallery/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..98f0a1d --- /dev/null +++ b/webapp_gallery/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/theme.ts","./src/capture/capture_frames.tsx","./src/capture/capture_panel.tsx","./src/capture/capture_sessions.tsx","./src/capture/capture_toolbar.tsx","./src/capture/frame_summary.tsx","./src/capture/node_detail.tsx","./src/capture/plan_comparison.tsx","./src/capture/worker_timeline.tsx","./src/common/copy_button.tsx","./src/common/empty_state.tsx","./src/common/json_viewer.tsx","./src/common/metric_grid.tsx","./src/common/metric_value.tsx","./src/common/status_chip.tsx","./src/dag/dag_layout.ts","./src/dag/dag_legend.tsx","./src/dag/dag_model.ts","./src/dag/dag_node.tsx","./src/dag/dag_types.ts","./src/dag/render_dag.tsx","./src/gallery/category_filter.tsx","./src/gallery/frame_mode_tabs.tsx","./src/gallery/gallery_page.tsx","./src/gallery/gallery_summary.tsx","./src/gallery/gallery_toolbar.tsx","./src/gallery/plot_card.tsx","./src/hooks/use_element_size.ts","./src/hooks/use_gallery_catalog.ts","./src/hooks/use_plot_session.ts","./src/inspector/actions_panel.tsx","./src/inspector/control_field.tsx","./src/inspector/controls_panel.tsx","./src/inspector/inspector_drawer.tsx","./src/inspector/inspector_tabs.tsx","./src/inspector/observer_panel.tsx","./src/inspector/performance_panel.tsx","./src/plot/kernel_observer_summary.tsx","./src/plot/performance_strip.tsx","./src/plot/plot_canvas.tsx","./src/plot/plot_status.tsx","./src/protocol/format.ts","./src/protocol/gallery_descriptor.ts","./src/protocol/gallery_messages.ts","./src/protocol/gallery_parser.ts","./src/protocol/gallery_types.ts","./src/protocol/pixel_frame.ts","./src/runtime/client_performance.ts","./src/runtime/frame_request_controller.ts","./src/runtime/pixel_presenter.ts","./src/session/gallery_plot_session.ts","./src/session/gallery_plot_snapshot.ts","./src/transport/catalog_loader.ts","./src/transport/gallery_socket.ts","./tests/setup.ts","./tests/components/control_field.test.tsx","./tests/dag/dag_model.test.ts","./tests/e2e/gallery.spec.ts","./tests/protocol/gallery_parser.test.ts","./tests/protocol/pixel_frame.test.ts","./tests/runtime/client_performance.test.ts","./tests/runtime/frame_request_controller.test.ts","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/webapp_gallery/tsconfig.json b/webapp_gallery/tsconfig.json new file mode 100644 index 0000000..5bcad57 --- /dev/null +++ b/webapp_gallery/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{"path": "./tsconfig.app.json"}] +} diff --git a/webapp_gallery/vite.config.ts b/webapp_gallery/vite.config.ts new file mode 100644 index 0000000..bf05ba5 --- /dev/null +++ b/webapp_gallery/vite.config.ts @@ -0,0 +1,9 @@ +import {defineConfig} from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + base: "./", + build: {outDir: "dist", emptyOutDir: true}, + test: {environment: "jsdom", setupFiles: "./tests/setup.ts", exclude: ["tests/e2e/**", "node_modules/**", "dist/**"]} +});