已合并
feat: add CANN performance UI report skill #517
unstain创建于 8月20日
feat: add CANN performance UI report skill #517
已合并
共 46 个文件变更+21444-2
| @@ -25,14 +25,17 @@ repos: | |||
| 25 | ) | 25 | ) |
| 26 | 26 | ||
| 27 | # OAT compliance check (Open Source Audit Tool) | 27 | # OAT compliance check (Open Source Audit Tool) |
| 28 | + # require_serial + pass_filenames=false: oat_check.sh 内部自行收集 staged | ||
| 29 | + # 文件,只调用一次。避免 pass_filenames=true 时每文件单独触发一次 OAT | ||
| 30 | + # 扫描,导致 oat-py 在 CI 环境中偶发不生成 PlainReport 报告而误报失败。 | ||
| 28 | - repo: local | 31 | - repo: local |
| 29 | hooks: | 32 | hooks: |
| 30 | - id: oat-check | 33 | - id: oat-check |
| 31 | name: OAT Compliance Check | 34 | name: OAT Compliance Check |
| 32 | entry: bash scripts/oat_check.sh | 35 | entry: bash scripts/oat_check.sh |
| 33 | language: system | 36 | language: system |
| 34 | - pass_filenames: true | 37 | + pass_filenames: false |
| 35 | - types: [file] | 38 | + require_serial: true |
| 36 | stages: [pre-commit] | 39 | stages: [pre-commit] |
| 37 | verbose: true | 40 | verbose: true |
| 38 | 41 | ||
| @@ -0,0 +1,111 @@ | |||
| 1 | +--- | ||
| 2 | +name: cann-perf-ui-json-report | ||
| 3 | +description: Generate, refresh, validate, and preview PTO interactive profiling reports from Skill 2 architecture/overlay/binding outputs plus backend analysis, performance, Timeline, raw Chrome Trace, and optional HBM, findings, frequency, operator-detail, and expert-inventory data. Use when creating or repairing report/index.html, synchronizing reusable Skill 3 UI behavior, validating Layer navigation and scoped metrics, diagnosing missing nodes/edges/heat/tooltips/Trace bindings, or producing a standalone file:// report. | ||
| 4 | +--- | ||
| 5 | + | ||
| 6 | +# Generate UI JSON Report | ||
| 7 | + | ||
| 8 | +Render backend and source facts without inventing architecture, performance, ownership, diagnoses, or advice. Keep backend inputs read-only and treat `report/` as a generated runtime. | ||
| 9 | + | ||
| 10 | +## Read the relevant contracts | ||
| 11 | + | ||
| 12 | +- Read [references/data-contract.md](references/data-contract.md) before changing input paths, schemas, mapping identity, capabilities, or provenance. | ||
| 13 | +- Read [references/input-files.md](references/input-files.md) when identifying which model-side JSON, CSV, Trace, source, or Markdown files Skill 3 reads. | ||
| 14 | +- Read [references/ui-contract.md](references/ui-contract.md) before changing architecture, Layer navigation, Inspector, Operator List, TraceView, HBM, selection, styling, or tooltips. | ||
| 15 | +- Read [references/validation-matrix.md](references/validation-matrix.md) before changing validators, generation transactions, template synchronization, or final acceptance. | ||
| 16 | +- When changing architecture projection or graph rendering, also read the installed `pto-json-architecture-graph` Skill and its architecture contract. Keep the vendored graph validator synchronized. | ||
| 17 | + | ||
| 18 | +## Preserve fact boundaries | ||
| 19 | + | ||
| 20 | +- Join analysis, performance, normalized Timeline, raw Trace and Skill 2 outputs only through their explicit stable identities. | ||
| 21 | +- Render architecture solely from `model_architecture_graph.v1` roots and edges. Never infer an edge from hierarchy, source order, kernel order, Timeline order, or Trace Flow. | ||
| 22 | +- Make performance and Trace interactions available only to explicitly mapped backend nodes. Keep source-only nodes metric-free and Trace-free. | ||
| 23 | +- Keep runtime auxiliary outside source model dataflow. | ||
| 24 | +- Preserve original item/edge IDs, metadata, tensor fields, constraints, provenance and source references. | ||
| 25 | +- Keep source-authored model labels unchanged when switching UI language. | ||
| 26 | +- Default a new report to Chinese while honoring a saved Chinese/English preference. | ||
| 27 | + | ||
| 28 | +## Generate | ||
| 29 | + | ||
| 30 | +Prefer a Skill 2 `ui_report_handoff.v1` manifest. New reports require it; legacy reports may retain an existing `report-config.js`. | ||
| 31 | + | ||
| 32 | +```bash | ||
| 33 | +rtk node <skill-dir>/scripts/generate-report.mjs \ | ||
| 34 | + --repo <report-repo> \ | ||
| 35 | + --handoff <ui-report-handoff.json> \ | ||
| 36 | + --refresh-template | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +Optional inputs: | ||
| 40 | + | ||
| 41 | +```bash | ||
| 42 | +rtk node <skill-dir>/scripts/generate-report.mjs \ | ||
| 43 | + --repo <report-repo> \ | ||
| 44 | + --trace <trace_view.json> \ | ||
| 45 | + --hbm-dir <aligned-hbm-dir> \ | ||
| 46 | + --refresh-template | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +Rules: | ||
| 50 | + | ||
| 51 | +- `--refresh-template` replaces reusable runtime UI files but preserves or regenerates the model-specific config and preserves Skill 2 outputs. | ||
| 52 | +- Generate the complete next report transactionally. On any failure, restore the previous report and trace byte-for-byte. | ||
| 53 | +- Require `skill3_adapter: generic` and consume the graph, overlay and bindings emitted by `cann-perf-breakdown-to-ui-json` for every model family. | ||
| 54 | +- Rebuild `report-embedded-data.js`; never edit it manually. | ||
| 55 | +- Missing optional inputs degrade to empty typed data. Missing required inputs fail with a field-specific error. | ||
| 56 | + | ||
| 57 | +## Check without writes | ||
| 58 | + | ||
| 59 | +```bash | ||
| 60 | +rtk node <skill-dir>/scripts/generate-report.mjs --repo <report-repo> --check | ||
| 61 | +``` | ||
| 62 | + | ||
| 63 | +`--check` is strictly read-only and cannot be combined with `--refresh-template`, `--trace`, or `--hbm-dir`. It must not create placeholders, rename `index.html`, rewrite manifests, or touch timestamps. For changes to the generator, run: | ||
| 64 | + | ||
| 65 | +```bash | ||
| 66 | +rtk node <skill-dir>/scripts/test-generation-safety.mjs --repo <known-good-report-repo> | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +## Required deterministic checks | ||
| 70 | + | ||
| 71 | +Generation must run: | ||
| 72 | + | ||
| 73 | +```bash | ||
| 74 | +rtk node <skill-dir>/scripts/validate-architecture-graph.mjs \ | ||
| 75 | + <repo>/report/outputs/model_architecture_graph.json \ | ||
| 76 | + --source-root section/source_architecture \ | ||
| 77 | + --require-semantic-port-policy | ||
| 78 | +rtk node <skill-dir>/scripts/validate-report.mjs --repo <repo> | ||
| 79 | +rtk node <skill-dir>/scripts/test-layer-report-metrics.mjs --repo <repo> | ||
| 80 | +rtk node <skill-dir>/scripts/test-projected-fanout.mjs --repo <repo> | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +Run Layer, expert, HBM, and expected-graph-feature assertions only when their handoff capabilities apply. A declared capability with missing evidence is a failure; a genuinely inapplicable capability is `not_applicable`, not a fake pass. | ||
| 84 | + | ||
| 85 | +Reject undeclared template drift. Permit reviewed per-report deviations only through `ReportRuntimeConfig.templateOverrides`/handoff `template_overrides`. | ||
| 86 | + | ||
| 87 | +Write `model_skill_validation_manifest.v2` with separate deterministic and manual states. Deterministic success leaves `overall_status: pending_manual_validation` until browser and visual checks finish. | ||
| 88 | + | ||
| 89 | +## Final browser validation | ||
| 90 | + | ||
| 91 | +After deterministic checks pass, perform one smoke test at 1440 × 1000 using the canonical report URL and one `file://` smoke when standalone delivery matters. At minimum verify: | ||
| 92 | + | ||
| 93 | +- no console/resource errors; | ||
| 94 | +- architecture, Inspector and Trace load current data; | ||
| 95 | +- mapped/source-only/aggregate/runtime selections respect their fact boundaries; | ||
| 96 | +- three non-adjacent Layers update scoped graph badges, Inspector metrics, Sequence events and Flow without changing operator identity; | ||
| 97 | +- declared branches, residuals and Layer-selection bridges remain connected; | ||
| 98 | +- heat legend, localized labels, tooltips, HBM missing state and optional expert projection match the UI contract; | ||
| 99 | +- empty-canvas selection reset and Trace focus/zoom/pan remain functional. | ||
| 100 | + | ||
| 101 | +Record browser, file-protocol and visual results in the validation manifest; only then set overall status to passed. | ||
| 102 | + | ||
| 103 | +## Failure routing | ||
| 104 | + | ||
| 105 | +- Identity, node coverage, Timeline owner or trace-binding mismatch: return a Skill 1/2 data requirement; do not patch backend JSON. | ||
| 106 | +- Missing edge/tensor/provenance, invalid repeat membership, fan-out/fan-in/residual shortfall: return a Skill 2 graph requirement. | ||
| 107 | +- Source lock mismatch: stop and re-extract/review architecture; never update hashes blindly. | ||
| 108 | +- Layout, styling, localization, interaction, selection or runtime transport: fix Skill 3 template and regenerate. | ||
| 109 | +- Preserve unresolved facts as unavailable; never map by leaf-label similarity merely to raise coverage. | ||
| 110 | + | ||
| 111 | +Report counts and observed capabilities from current files, never historical examples. | ||
| @@ -0,0 +1,111 @@ | |||
| 1 | +--- | ||
| 2 | +name: cann-perf-ui-json-report | ||
| 3 | +description: Generate, refresh, validate, and preview PTO interactive profiling reports from Skill 2 architecture/overlay/binding outputs plus backend analysis, performance, Timeline, raw Chrome Trace, and optional HBM, findings, frequency, operator-detail, and expert-inventory data. Use when creating or repairing report/index.html, synchronizing reusable Skill 3 UI behavior, validating Layer navigation and scoped metrics, diagnosing missing nodes/edges/heat/tooltips/Trace bindings, or producing a standalone file:// report. | ||
| 4 | +--- | ||
| 5 | + | ||
| 6 | +# Generate UI JSON Report | ||
| 7 | + | ||
| 8 | +Render backend and source facts without inventing architecture, performance, ownership, diagnoses, or advice. Keep backend inputs read-only and treat `report/` as a generated runtime. | ||
| 9 | + | ||
| 10 | +## Read the relevant contracts | ||
| 11 | + | ||
| 12 | +- Read [references/data-contract.md](references/data-contract.md) before changing input paths, schemas, mapping identity, capabilities, or provenance. | ||
| 13 | +- Read [references/input-files.md](references/input-files.md) when identifying which model-side JSON, CSV, Trace, source, or Markdown files Skill 3 reads. | ||
| 14 | +- Read [references/ui-contract.md](references/ui-contract.md) before changing architecture, Layer navigation, Inspector, Operator List, TraceView, HBM, selection, styling, or tooltips. | ||
| 15 | +- Read [references/validation-matrix.md](references/validation-matrix.md) before changing validators, generation transactions, template synchronization, or final acceptance. | ||
| 16 | +- When changing architecture projection or graph rendering, also read the installed `pto-json-architecture-graph` Skill and its architecture contract. Keep the vendored graph validator synchronized. | ||
| 17 | + | ||
| 18 | +## Preserve fact boundaries | ||
| 19 | + | ||
| 20 | +- Join analysis, performance, normalized Timeline, raw Trace and Skill 2 outputs only through their explicit stable identities. | ||
| 21 | +- Render architecture solely from `model_architecture_graph.v1` roots and edges. Never infer an edge from hierarchy, source order, kernel order, Timeline order, or Trace Flow. | ||
| 22 | +- Make performance and Trace interactions available only to explicitly mapped backend nodes. Keep source-only nodes metric-free and Trace-free. | ||
| 23 | +- Keep runtime auxiliary outside source model dataflow. | ||
| 24 | +- Preserve original item/edge IDs, metadata, tensor fields, constraints, provenance and source references. | ||
| 25 | +- Keep source-authored model labels unchanged when switching UI language. | ||
| 26 | +- Default a new report to Chinese while honoring a saved Chinese/English preference. | ||
| 27 | + | ||
| 28 | +## Generate | ||
| 29 | + | ||
| 30 | +Prefer a Skill 2 `ui_report_handoff.v1` manifest. New reports require it; legacy reports may retain an existing `report-config.js`. | ||
| 31 | + | ||
| 32 | +```bash | ||
| 33 | +rtk node <skill-dir>/scripts/generate-report.mjs \ | ||
| 34 | + --repo <report-repo> \ | ||
| 35 | + --handoff <ui-report-handoff.json> \ | ||
| 36 | + --refresh-template | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +Optional inputs: | ||
| 40 | + | ||
| 41 | +```bash | ||
| 42 | +rtk node <skill-dir>/scripts/generate-report.mjs \ | ||
| 43 | + --repo <report-repo> \ | ||
| 44 | + --trace <trace_view.json> \ | ||
| 45 | + --hbm-dir <aligned-hbm-dir> \ | ||
| 46 | + --refresh-template | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +Rules: | ||
| 50 | + | ||
| 51 | +- `--refresh-template` replaces reusable runtime UI files but preserves or regenerates the model-specific config and preserves Skill 2 outputs. | ||
| 52 | +- Generate the complete next report transactionally. On any failure, restore the previous report and trace byte-for-byte. | ||
| 53 | +- Require `skill3_adapter: generic` and consume the graph, overlay and bindings emitted by `cann-perf-breakdown-to-ui-json` for every model family. | ||
| 54 | +- Rebuild `report-embedded-data.js`; never edit it manually. | ||
| 55 | +- Missing optional inputs degrade to empty typed data. Missing required inputs fail with a field-specific error. | ||
| 56 | + | ||
| 57 | +## Check without writes | ||
| 58 | + | ||
| 59 | +```bash | ||
| 60 | +rtk node <skill-dir>/scripts/generate-report.mjs --repo <report-repo> --check | ||
| 61 | +``` | ||
| 62 | + | ||
| 63 | +`--check` is strictly read-only and cannot be combined with `--refresh-template`, `--trace`, or `--hbm-dir`. It must not create placeholders, rename `index.html`, rewrite manifests, or touch timestamps. For changes to the generator, run: | ||
| 64 | + | ||
| 65 | +```bash | ||
| 66 | +rtk node <skill-dir>/scripts/test-generation-safety.mjs --repo <known-good-report-repo> | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +## Required deterministic checks | ||
| 70 | + | ||
| 71 | +Generation must run: | ||
| 72 | + | ||
| 73 | +```bash | ||
| 74 | +rtk node <skill-dir>/scripts/validate-architecture-graph.mjs \ | ||
| 75 | + <repo>/report/outputs/model_architecture_graph.json \ | ||
| 76 | + --source-root section/source_architecture \ | ||
| 77 | + --require-semantic-port-policy | ||
| 78 | +rtk node <skill-dir>/scripts/validate-report.mjs --repo <repo> | ||
| 79 | +rtk node <skill-dir>/scripts/test-layer-report-metrics.mjs --repo <repo> | ||
| 80 | +rtk node <skill-dir>/scripts/test-projected-fanout.mjs --repo <repo> | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +Run Layer, expert, HBM, and expected-graph-feature assertions only when their handoff capabilities apply. A declared capability with missing evidence is a failure; a genuinely inapplicable capability is `not_applicable`, not a fake pass. | ||
| 84 | + | ||
| 85 | +Reject undeclared template drift. Permit reviewed per-report deviations only through `ReportRuntimeConfig.templateOverrides`/handoff `template_overrides`. | ||
| 86 | + | ||
| 87 | +Write `model_skill_validation_manifest.v2` with separate deterministic and manual states. Deterministic success leaves `overall_status: pending_manual_validation` until browser and visual checks finish. | ||
| 88 | + | ||
| 89 | +## Final browser validation | ||
| 90 | + | ||
| 91 | +After deterministic checks pass, perform one smoke test at 1440 × 1000 using the canonical report URL and one `file://` smoke when standalone delivery matters. At minimum verify: | ||
| 92 | + | ||
| 93 | +- no console/resource errors; | ||
| 94 | +- architecture, Inspector and Trace load current data; | ||
| 95 | +- mapped/source-only/aggregate/runtime selections respect their fact boundaries; | ||
| 96 | +- three non-adjacent Layers update scoped graph badges, Inspector metrics, Sequence events and Flow without changing operator identity; | ||
| 97 | +- declared branches, residuals and Layer-selection bridges remain connected; | ||
| 98 | +- heat legend, localized labels, tooltips, HBM missing state and optional expert projection match the UI contract; | ||
| 99 | +- empty-canvas selection reset and Trace focus/zoom/pan remain functional. | ||
| 100 | + | ||
| 101 | +Record browser, file-protocol and visual results in the validation manifest; only then set overall status to passed. | ||
| 102 | + | ||
| 103 | +## Failure routing | ||
| 104 | + | ||
| 105 | +- Identity, node coverage, Timeline owner or trace-binding mismatch: return a Skill 1/2 data requirement; do not patch backend JSON. | ||
| 106 | +- Missing edge/tensor/provenance, invalid repeat membership, fan-out/fan-in/residual shortfall: return a Skill 2 graph requirement. | ||
| 107 | +- Source lock mismatch: stop and re-extract/review architecture; never update hashes blindly. | ||
| 108 | +- Layout, styling, localization, interaction, selection or runtime transport: fix Skill 3 template and regenerate. | ||
| 109 | +- Preserve unresolved facts as unavailable; never map by leaf-label similarity merely to raise coverage. | ||
| 110 | + | ||
| 111 | +Report counts and observed capabilities from current files, never historical examples. | ||
| @@ -0,0 +1,4 @@ | |||
| 1 | +interface: | ||
| 2 | + display_name: "CANN 性能 UI 报告" | ||
| 3 | + short_description: "从结构化 UI 事实生成并验证可离线打开的交互式性能报告" | ||
| 4 | + default_prompt: "Use $cann-perf-ui-json-report to generate and validate an offline interactive profiling report from UI facts." | ||
Askills/cann-perf-ui-json-report/assets/report-template/design-system/patterns/ide-frame/pattern.css+1017-0
Askills/cann-perf-ui-json-report/assets/report-template/design-system/patterns/ide-frame/pattern.js+527-0
| @@ -0,0 +1,527 @@ | |||
| 1 | +// | ||
| 2 | +// Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 3 | +// you may not use this file except in compliance with the License. | ||
| 4 | +// You may obtain a copy of the License at | ||
| 5 | +// | ||
| 6 | +// http://www.apache.org/licenses/LICENSE-2.0 | ||
| 7 | +// | ||
| 8 | +// Unless required by applicable law or agreed to in writing, software | ||
| 9 | +// distributed under the License is distributed on an "AS IS" BASIS, | ||
| 10 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 11 | +// See the License for the specific language governing permissions and | ||
| 12 | +// limitations under the License. | ||
| 13 | +// | ||
| 14 | +(function registerPtoIdeFrame(global) { | ||
| 15 | + 'use strict'; | ||
| 16 | + | ||
| 17 | + const qs = (root, selector) => root.querySelector(selector); | ||
| 18 | + const qsa = (root, selector) => Array.from(root.querySelectorAll(selector)); | ||
| 19 | + let playbackUid = 0; | ||
| 20 | + | ||
| 21 | + function parseNumberList(value, fallback) { | ||
| 22 | + if (!value) return fallback; | ||
| 23 | + const list = String(value) | ||
| 24 | + .split(',') | ||
| 25 | + .map((item) => Number(item.trim())) | ||
| 26 | + .filter((item) => Number.isFinite(item)); | ||
| 27 | + return list.length ? list : fallback; | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + function directPanes(split) { | ||
| 31 | + return Array.from(split.children).filter((child) => child.matches?.('[data-ide-pane]')); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + function splitStorageKey(frame, split, index) { | ||
| 35 | + const host = frame.dataset.host || 'standalone'; | ||
| 36 | + const id = split.dataset.ideSplit || `split-${index + 1}`; | ||
| 37 | + return split.dataset.storageKey || `pto-ide-frame-${host}-${id}-v3`; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + function initialSizesForSplit(split, panes, direction, fallbackSizes, gutterSize) { | ||
| 41 | + const pixelSizes = parseNumberList(split.dataset.pixelSizes || split.dataset.defaultPixels, []); | ||
| 42 | + if (!pixelSizes.length) return fallbackSizes; | ||
| 43 | + const fallback = Array.isArray(fallbackSizes) && fallbackSizes.length === panes.length | ||
| 44 | + ? fallbackSizes | ||
| 45 | + : Array.from({ length: panes.length }, () => 1); | ||
| 46 | + | ||
| 47 | + const rect = split.getBoundingClientRect(); | ||
| 48 | + const styles = global.getComputedStyle?.(split); | ||
| 49 | + const paddingA = direction === 'vertical' | ||
| 50 | + ? Number.parseFloat(styles?.paddingTop || '0') | ||
| 51 | + : Number.parseFloat(styles?.paddingLeft || '0'); | ||
| 52 | + const paddingB = direction === 'vertical' | ||
| 53 | + ? Number.parseFloat(styles?.paddingBottom || '0') | ||
| 54 | + : Number.parseFloat(styles?.paddingRight || '0'); | ||
| 55 | + const rawSize = direction === 'vertical' ? rect.height : rect.width; | ||
| 56 | + const available = rawSize - paddingA - paddingB - Math.max(0, panes.length - 1) * gutterSize; | ||
| 57 | + if (!Number.isFinite(available) || available <= 0) return fallbackSizes; | ||
| 58 | + | ||
| 59 | + const fixedPixels = panes.map((_pane, index) => { | ||
| 60 | + const value = Number(pixelSizes[index]); | ||
| 61 | + return Number.isFinite(value) && value > 0 ? Math.min(value, available) : 0; | ||
| 62 | + }); | ||
| 63 | + const fixedTotal = fixedPixels.reduce((sum, size) => sum + size, 0); | ||
| 64 | + const flexibleIndexes = panes | ||
| 65 | + .map((_pane, index) => index) | ||
| 66 | + .filter((index) => fixedPixels[index] <= 0); | ||
| 67 | + const remaining = Math.max(0, available - fixedTotal); | ||
| 68 | + const flexibleWeight = flexibleIndexes.reduce((sum, index) => { | ||
| 69 | + const weight = Number(fallback[index]); | ||
| 70 | + return sum + (Number.isFinite(weight) && weight > 0 ? weight : 1); | ||
| 71 | + }, 0); | ||
| 72 | + const resolvedPixels = fixedPixels.map((size, index) => { | ||
| 73 | + if (size > 0) return size; | ||
| 74 | + const weight = Number(fallback[index]); | ||
| 75 | + const safeWeight = Number.isFinite(weight) && weight > 0 ? weight : 1; | ||
| 76 | + return flexibleWeight > 0 ? remaining * safeWeight / flexibleWeight : remaining / Math.max(1, flexibleIndexes.length); | ||
| 77 | + }); | ||
| 78 | + | ||
| 79 | + return resolvedPixels.map((size) => size / available * 100); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + function initSplit(frame, split, index, options = {}) { | ||
| 83 | + const helper = global.PtoWorkbenchShell; | ||
| 84 | + if (!helper?.initResizablePanes) return null; | ||
| 85 | + | ||
| 86 | + const panes = directPanes(split); | ||
| 87 | + if (panes.length < 2) return null; | ||
| 88 | + | ||
| 89 | + const direction = split.dataset.splitDirection || split.dataset.direction || 'horizontal'; | ||
| 90 | + const configuredSizes = parseNumberList(split.dataset.sizes, options.sizes); | ||
| 91 | + const minSize = parseNumberList(split.dataset.minSize, options.minSize || 0); | ||
| 92 | + const gutterSize = Number.isFinite(Number(split.dataset.gutterSize)) | ||
| 93 | + ? Number(split.dataset.gutterSize) | ||
| 94 | + : (options.gutterSize || 7); | ||
| 95 | + const sizes = initialSizesForSplit(split, panes, direction, configuredSizes, gutterSize); | ||
| 96 | + | ||
| 97 | + return helper.initResizablePanes({ | ||
| 98 | + root: split, | ||
| 99 | + panes, | ||
| 100 | + direction, | ||
| @@ -0,0 +1,180 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2025 Huawei Technologies Co., Ltd. | ||
| 3 | + * | ||
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 5 | + * you may not use this file except in compliance with the License. | ||
| 6 | + * You may obtain a copy of the License at | ||
| 7 | + * | ||
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 9 | + * | ||
| 10 | + * Unless required by applicable law or agreed to in writing, software | ||
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 13 | + * See the License for the specific language governing permissions and | ||
| 14 | + * limitations under the License. | ||
| 15 | + */ | ||
| 16 | + | ||
| 17 | +body { | ||
| 18 | + margin: 0; | ||
| 19 | + min-height: 100vh; | ||
| 20 | + display: grid; | ||
| 21 | + place-items: center; | ||
| 22 | + background: var(--background); | ||
| 23 | + color: var(--foreground); | ||
| 24 | + font: var(--text-body); | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +.pattern-stage { | ||
| 28 | + width: min(760px, calc(100vw - 48px)); | ||
| 29 | + padding: 24px; | ||
| 30 | + border: 1px solid var(--border-subtle); | ||
| 31 | + border-radius: var(--panel-radius); | ||
| 32 | + background: var(--background-elevated); | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +.pattern-title { | ||
| 36 | + margin: 0 0 6px; | ||
| 37 | + font: var(--text-title-2); | ||
| 38 | + color: var(--foreground); | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +.pattern-subtitle { | ||
| 42 | + margin: 0 0 16px; | ||
| 43 | + color: var(--foreground-secondary); | ||
| 44 | + font: var(--text-body-sm); | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +.pto-pattern-swimlane-task { | ||
| 48 | + position: relative; | ||
| 49 | + display: flex; | ||
| 50 | + flex-direction: column; | ||
| 51 | + gap: 10px; | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +.pto-pattern-swimlane-task__row { | ||
| 55 | + display: grid; | ||
| 56 | + grid-template-columns: 72px minmax(0, 1fr); | ||
| 57 | + gap: 10px; | ||
| 58 | + align-items: center; | ||
| 59 | +} | ||
| 60 | + | ||
| 61 | +.pto-pattern-swimlane-task__label { | ||
| 62 | + font-family: var(--font-sans); | ||
| 63 | + font-size: 12px; | ||
| 64 | + font-weight: 500; | ||
| 65 | + line-height: 1.2; | ||
| 66 | + color: var(--foreground-muted); | ||
| 67 | + text-transform: uppercase; | ||
| 68 | + letter-spacing: 0.04em; | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +.pto-pattern-swimlane-task__canvas { | ||
| 72 | + display: block; | ||
| 73 | + width: 100%; | ||
| 74 | + height: 16px; | ||
| 75 | + cursor: pointer; | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +.pto-pattern-swimlane-task__canvas.is-hovered { | ||
| 79 | + filter: brightness(1.1); | ||
| 80 | +} | ||
| 81 | + | ||
| 82 | +.pto-swimlane-task-tooltip { | ||
| 83 | + position: absolute; | ||
| 84 | + display: none; | ||
| 85 | + z-index: 12; | ||
| 86 | + min-width: 220px; | ||
| 87 | + max-width: 340px; | ||
| 88 | + padding: 8px 10px; | ||
| 89 | + border: 1px solid var(--border-strong); | ||
| 90 | + border-radius: var(--radius-md); | ||
| 91 | + background: var(--surface-3); | ||
| 92 | + color: var(--foreground); | ||
| 93 | + box-shadow: var(--shadow-md); | ||
| 94 | + pointer-events: none; | ||
| 95 | + font-family: var(--font-sans); | ||
| 96 | + font-size: 12px; | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +.pto-swimlane-task-tooltip.is-visible { | ||
| 100 | + display: block; | ||
| 101 | + animation: pto-swimlane-tooltip-in 0.1s ease; | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +.pto-swimlane-task-tooltip__title { | ||
| 105 | + margin-bottom: 6px; | ||
| 106 | + padding-bottom: 4px; | ||
| 107 | + border-bottom: 1px solid var(--border-default); | ||
| 108 | + color: var(--foreground); | ||
| 109 | + font-family: var(--font-mono); | ||
| 110 | + font-size: 12px; | ||
| 111 | + font-weight: 700; | ||
| 112 | +} | ||
| 113 | + | ||
| 114 | +.pto-swimlane-task-tooltip__row { | ||
| 115 | + display: flex; | ||
| 116 | + justify-content: space-between; | ||
| 117 | + gap: 16px; | ||
| 118 | + line-height: 1.5; | ||
| 119 | +} | ||
| 120 | + | ||
| 121 | +.pto-swimlane-task-tooltip__key { | ||
| 122 | + color: var(--foreground-muted); | ||
| 123 | + font-size: 12px; | ||
| 124 | + letter-spacing: 0.05em; | ||
| 125 | + text-transform: uppercase; | ||
| 126 | +} | ||
| 127 | + | ||
| 128 | +.pto-swimlane-task-tooltip__value { | ||
| 129 | + color: var(--foreground); | ||
| 130 | + font-family: var(--font-mono); | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +.pto-swimlane-task-tooltip__value.is-bad { | ||
| 134 | + color: var(--danger); | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +.pto-swimlane-task-tooltip__value.is-warn { | ||
| 138 | + color: var(--warning); | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +.pto-swimlane-task-tooltip__value.is-ok { | ||
| 142 | + color: var(--success); | ||
| 143 | +} | ||
| 144 | + | ||
| 145 | +@keyframes pto-swimlane-tooltip-in { | ||
| 146 | + from { | ||
| 147 | + opacity: 0; | ||
| 148 | + transform: translateY(2px); | ||
| 149 | + } | ||
| 150 | + to { | ||
| 151 | + opacity: 1; | ||
| 152 | + transform: translateY(0); | ||
| 153 | + } | ||
| 154 | +} | ||
| 155 | + | ||
| 156 | +html[data-embed-preview='true'] body { | ||
| 157 | + height: auto; | ||
| 158 | + min-height: 0; | ||
| 159 | + display: block; | ||
| 160 | + background: transparent; | ||
| 161 | + overflow: hidden; | ||
| 162 | +} | ||
| 163 | + | ||
| 164 | +html[data-embed-preview='true'] .pattern-stage { | ||
| 165 | + width: 100%; | ||
| 166 | + padding: 0; | ||
| 167 | + border: 0; | ||
| 168 | + border-radius: 0; | ||
| 169 | + background: none; | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +html[data-embed-preview='true'] .pattern-title, | ||
| 173 | +html[data-embed-preview='true'] .pattern-subtitle { | ||
| 174 | + display: none; | ||
| 175 | +} | ||
| 176 | + | ||
| 177 | +html[data-embed-preview='true'] .pto-pattern-swimlane-task { | ||
| 178 | + gap: 8px; | ||
| 179 | + min-height: 190px; | ||
| 180 | +} | ||
| @@ -0,0 +1,696 @@ | |||
| 1 | +// | ||
| 2 | +// Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 3 | +// you may not use this file except in compliance with the License. | ||
| 4 | +// You may obtain a copy of the License at | ||
| 5 | +// | ||
| 6 | +// http://www.apache.org/licenses/LICENSE-2.0 | ||
| 7 | +// | ||
| 8 | +// Unless required by applicable law or agreed to in writing, software | ||
| 9 | +// distributed under the License is distributed on an "AS IS" BASIS, | ||
| 10 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 11 | +// See the License for the specific language governing permissions and | ||
| 12 | +// limitations under the License. | ||
| 13 | +// | ||
| 14 | +(function registerPtoSwimlaneTaskPattern(global) { | ||
| 15 | + 'use strict'; | ||
| 16 | + | ||
| 17 | + const DEFAULTS = { | ||
| 18 | + minBarSegmentCountsPx: 84, | ||
| 19 | + sideRatio: 0.2, | ||
| 20 | + minInWidth: 10, | ||
| 21 | + maxInWidth: 42, | ||
| 22 | + minOutWidth: 12, | ||
| 23 | + maxOutWidth: 48, | ||
| 24 | + selectedLightenAmount: 10, | ||
| 25 | + emphasizedLightenAmount: 6, | ||
| 26 | + baseFillAlpha: 0.24, | ||
| 27 | + borderSelected: 'rgba(255,255,255,0.88)', | ||
| 28 | + borderRelated: 'rgba(255,255,255,0.46)', | ||
| 29 | + borderDefault: 'rgba(255,255,255,0.16)', | ||
| 30 | + textColor: 'rgba(255,255,255,0.92)', | ||
| 31 | + darkTextColor: 'rgba(15,23,42,0.94)', | ||
| 32 | + topHighlight: 'rgba(255,255,255,0.08)', | ||
| 33 | + }; | ||
| 34 | + | ||
| 35 | + const DEFAULT_STITCH_COLORS = ['#735bb4', '#4d70ba', '#4a9568', '#ba8053', '#45a2ad', '#b46494', '#238fcf', '#c99524']; | ||
| 36 | + | ||
| 37 | + const DEFAULT_CATEGORICAL_COLORMAP = [ | ||
| 38 | + '#5b8def', | ||
| 39 | + '#f2a23a', | ||
| 40 | + '#a97ae6', | ||
| 41 | + '#38b8b2', | ||
| 42 | + '#e46b8a', | ||
| 43 | + '#43a3d9', | ||
| 44 | + '#8b929e', | ||
| 45 | + '#76b85a', | ||
| 46 | + '#c58b5f', | ||
| 47 | + '#d86ad9', | ||
| 48 | + '#d7b84e', | ||
| 49 | + '#6d88d7', | ||
| 50 | + ]; | ||
| 51 | + | ||
| 52 | + const DEFAULT_SEMANTIC_DOMAIN = [ | ||
| 53 | + 'matmul', | ||
| 54 | + 'vec_softmax', | ||
| 55 | + 'vec_layernorm', | ||
| 56 | + 'vec_elementwise', | ||
| 57 | + 'vec_reduce', | ||
| 58 | + 'mte_load', | ||
| 59 | + 'cpu_sched', | ||
| 60 | + ]; | ||
| 61 | + | ||
| 62 | + const DEFAULT_LABEL_COLORS = { | ||
| 63 | + 'Prolog-Quant': '#8d6bc7', | ||
| 64 | + 'Query-Linear': '#735bb4', | ||
| 65 | + 'Query-Dequant': '#4d70ba', | ||
| 66 | + 'Query-Hadamard': '#6f63b8', | ||
| 67 | + 'Weight-Linear': '#4a9568', | ||
| 68 | + 'Key-Linear': '#ba8053', | ||
| 69 | + 'Key-Hadamard': '#c48b60', | ||
| 70 | + 'Key-LayerNorm': '#b46494', | ||
| 71 | + 'Key-Rope2D': '#45a2ad', | ||
| 72 | + fake: '#6f6a64', | ||
| 73 | + unknown: '#6f6a64', | ||
| 74 | + }; | ||
| 75 | + | ||
| 76 | + const DEFAULT_LANE_KIND_COLORS = { | ||
| 77 | + fake: '#6f6a64', | ||
| 78 | + aic: '#735bb4', | ||
| 79 | + AIC: '#735bb4', | ||
| 80 | + aiv: '#4d70ba', | ||
| 81 | + AIV: '#4d70ba', | ||
| 82 | + aicpu: '#4a9568', | ||
| 83 | + AICCtrl: '#4a9568', | ||
| 84 | + AICSched: '#4a9568', | ||
| 85 | + MTEIn: '#ba8053', | ||
| 86 | + MTEOut: '#ba8053', | ||
| 87 | + other: '#8c847c', | ||
| 88 | + }; | ||
| 89 | + | ||
| 90 | + function buildTaskSegmentSpec(task, widthPx) { | ||
| 91 | + const semantic = String(task?.label || task?.displayName || task?.rawName || 'compute'); | ||
| 92 | + const inputCount = Array.isArray(task?.inputRawMagic) ? task.inputRawMagic.length : 0; | ||
| 93 | + const outputCount = Array.isArray(task?.outputRawMagic) ? task.outputRawMagic.length : 0; | ||
| 94 | + const hasInput = inputCount > 0; | ||
| 95 | + const hasOutput = outputCount > 0; | ||
| 96 | + const showCounts = widthPx >= DEFAULTS.minBarSegmentCountsPx; | ||
| 97 | + | ||
| 98 | + return [ | ||
| 99 | + { key: 'in', text: hasInput ? (showCounts ? `IN ${inputCount}` : 'IN') : '', visible: hasInput }, | ||
| 100 | + { key: 'compute', text: semantic }, | ||
| @@ -0,0 +1,85 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2025 Huawei Technologies Co., Ltd. | ||
| 3 | + * | ||
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 5 | + * you may not use this file except in compliance with the License. | ||
| 6 | + * You may obtain a copy of the License at | ||
| 7 | + * | ||
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 9 | + * | ||
| 10 | + * Unless required by applicable law or agreed to in writing, software | ||
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 13 | + * See the License for the specific language governing permissions and | ||
| 14 | + * limitations under the License. | ||
| 15 | + */ | ||
| 16 | + | ||
| 17 | +.pto-timeline-time-selection { | ||
| 18 | + position: absolute; | ||
| 19 | + z-index: 3; | ||
| 20 | + overflow: hidden; | ||
| 21 | + pointer-events: none; | ||
| 22 | +} | ||
| 23 | + | ||
| 24 | +.pto-timeline-time-selection[hidden], | ||
| 25 | +.pto-timeline-time-selection-summary[hidden] { | ||
| 26 | + display: none; | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +.pto-timeline-time-selection__fill { | ||
| 30 | + position: absolute; | ||
| 31 | + top: 0; | ||
| 32 | + bottom: 0; | ||
| 33 | + left: var(--pto-selection-start); | ||
| 34 | + width: var(--pto-selection-width); | ||
| 35 | + background: color-mix(in srgb, var(--focus-ring) 18%, transparent); | ||
| 36 | + box-shadow: | ||
| 37 | + inset 1px 0 0 color-mix(in srgb, var(--focus-ring) 82%, transparent), | ||
| 38 | + inset -1px 0 0 color-mix(in srgb, var(--focus-ring) 82%, transparent); | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +.pto-timeline-time-selection[data-kind='event'] .pto-timeline-time-selection__fill { | ||
| 42 | + background: color-mix(in srgb, var(--focus-ring) 24%, transparent); | ||
| 43 | +} | ||
| 44 | + | ||
| 45 | +.pto-timeline-time-selection__marker { | ||
| 46 | + position: absolute; | ||
| 47 | + top: 0; | ||
| 48 | + bottom: 0; | ||
| 49 | + width: 1px; | ||
| 50 | + background: var(--focus-ring); | ||
| 51 | + opacity: 0.92; | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +.pto-timeline-time-selection__marker.is-start { | ||
| 55 | + left: var(--pto-selection-start); | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +.pto-timeline-time-selection__marker.is-end { | ||
| 59 | + left: calc(var(--pto-selection-start) + var(--pto-selection-width) - 1px); | ||
| 60 | +} | ||
| 61 | + | ||
| 62 | +.pto-timeline-time-selection-summary { | ||
| 63 | + position: absolute; | ||
| 64 | + z-index: 18; | ||
| 65 | + top: 4px; | ||
| 66 | + left: calc(var(--pto-timeline-label-width, 0px) + (100% - var(--pto-timeline-label-width, 0px)) / 2); | ||
| 67 | + max-width: calc(100% - var(--pto-timeline-label-width, 0px) - 16px); | ||
| 68 | + overflow: hidden; | ||
| 69 | + transform: translateX(-50%); | ||
| 70 | + padding: 2px 8px; | ||
| 71 | + border: 1px solid color-mix(in srgb, var(--focus-ring) 58%, var(--border-default)); | ||
| 72 | + border-radius: var(--radius-sm); | ||
| 73 | + background: color-mix(in srgb, var(--surface-3) 94%, transparent); | ||
| 74 | + color: var(--foreground); | ||
| 75 | + box-shadow: var(--shadow-sm); | ||
| 76 | + font: var(--text-mono); | ||
| 77 | + line-height: 16px; | ||
| 78 | + text-overflow: ellipsis; | ||
| 79 | + white-space: nowrap; | ||
| 80 | + pointer-events: none; | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +.is-selecting-time { | ||
| 84 | + cursor: col-resize; | ||
| 85 | +} | ||
| @@ -0,0 +1,330 @@ | |||
| 1 | +// | ||
| 2 | +// Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 3 | +// you may not use this file except in compliance with the License. | ||
| 4 | +// You may obtain a copy of the License at | ||
| 5 | +// | ||
| 6 | +// http://www.apache.org/licenses/LICENSE-2.0 | ||
| 7 | +// | ||
| 8 | +// Unless required by applicable law or agreed to in writing, software | ||
| 9 | +// distributed under the License is distributed on an "AS IS" BASIS, | ||
| 10 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 11 | +// See the License for the specific language governing permissions and | ||
| 12 | +// limitations under the License. | ||
| 13 | +// | ||
| 14 | +(function registerPtoTimelineTimeSelectionPattern(global) { | ||
| 15 | + 'use strict'; | ||
| 16 | + | ||
| 17 | + const DEFAULT_DRAG_THRESHOLD = 4; | ||
| 18 | + | ||
| 19 | + function number(value, fallback = 0) { | ||
| 20 | + const parsed = Number(value); | ||
| 21 | + return Number.isFinite(parsed) ? parsed : fallback; | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + function clamp(value, min, max) { | ||
| 25 | + return Math.max(min, Math.min(max, value)); | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + function normalizeSelection(selection) { | ||
| 29 | + if (!selection) return null; | ||
| 30 | + const first = number(selection.start, NaN); | ||
| 31 | + const second = number(selection.end, NaN); | ||
| 32 | + if (!Number.isFinite(first) || !Number.isFinite(second)) return null; | ||
| 33 | + return { | ||
| 34 | + ...selection, | ||
| 35 | + start: Math.min(first, second), | ||
| 36 | + end: Math.max(first, second), | ||
| 37 | + }; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + function xToTime(x, width, start, end) { | ||
| 41 | + const safeWidth = Math.max(1, number(width, 1)); | ||
| 42 | + const safeStart = number(start); | ||
| 43 | + const safeEnd = Math.max(safeStart, number(end, safeStart)); | ||
| 44 | + const ratio = clamp(number(x) / safeWidth, 0, 1); | ||
| 45 | + return safeStart + (safeEnd - safeStart) * ratio; | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + function timeToX(time, width, start, end) { | ||
| 49 | + const safeWidth = Math.max(1, number(width, 1)); | ||
| 50 | + const safeStart = number(start); | ||
| 51 | + const safeEnd = Math.max(safeStart + Number.EPSILON, number(end, safeStart)); | ||
| 52 | + return clamp((number(time, safeStart) - safeStart) / (safeEnd - safeStart), 0, 1) * safeWidth; | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + function selectionFromEvent(event) { | ||
| 56 | + if (!event) return null; | ||
| 57 | + const start = number(event.ts ?? event.start, NaN); | ||
| 58 | + const explicitEnd = number(event.end, NaN); | ||
| 59 | + const duration = Math.max(0, number(event.dur ?? event.duration, 0)); | ||
| 60 | + const end = Number.isFinite(explicitEnd) ? explicitEnd : start + duration; | ||
| 61 | + if (!Number.isFinite(start) || !Number.isFinite(end)) return null; | ||
| 62 | + return normalizeSelection({ kind: 'event', start, end, event }); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + function formatAbsoluteUs(value, language = 'en') { | ||
| 66 | + const locale = language === 'zh' ? 'zh-CN' : 'en-US'; | ||
| 67 | + return `${number(value).toLocaleString(locale, { | ||
| 68 | + minimumFractionDigits: 0, | ||
| 69 | + maximumFractionDigits: 3, | ||
| 70 | + useGrouping: true, | ||
| 71 | + })} µs`; | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + function formatDuration(value) { | ||
| 75 | + const us = Math.max(0, number(value)); | ||
| 76 | + if (us >= 1_000_000) return `${(us / 1_000_000).toFixed(us >= 10_000_000 ? 1 : 2)} s`; | ||
| 77 | + if (us >= 1000) return `${(us / 1000).toFixed(us >= 10_000 ? 1 : 2)} ms`; | ||
| 78 | + if (us >= 1) return `${us.toFixed(us >= 100 ? 1 : 3)} µs`; | ||
| 79 | + return `${(us * 1000).toFixed(us >= 0.1 ? 1 : 2)} ns`; | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + function formatSelectionSummary(selection, language = 'en') { | ||
| 83 | + const normalized = normalizeSelection(selection); | ||
| 84 | + if (!normalized) return ''; | ||
| 85 | + const values = [ | ||
| 86 | + formatAbsoluteUs(normalized.start, language), | ||
| 87 | + formatAbsoluteUs(normalized.end, language), | ||
| 88 | + formatDuration(normalized.end - normalized.start), | ||
| 89 | + ]; | ||
| 90 | + const labels = language === 'zh' | ||
| 91 | + ? ['开始', '结束', '持续'] | ||
| 92 | + : ['Start', 'End', 'Duration']; | ||
| 93 | + return values.map((value, index) => `${labels[index]} ${value}`).join(' · '); | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + function createSelectionLayer(root) { | ||
| 97 | + if (!root) throw new Error('Timeline selection layer root is required'); | ||
| 98 | + const layer = document.createElement('div'); | ||
| 99 | + layer.className = 'pto-timeline-time-selection'; | ||
| 100 | + layer.hidden = true; | ||
| 101 | + layer.setAttribute('aria-hidden', 'true'); | ||
| 102 | + layer.innerHTML = ` | ||
| 103 | + <span class="pto-timeline-time-selection__fill"></span> | ||
| 104 | + <span class="pto-timeline-time-selection__marker is-start"></span> | ||
| 105 | + <span class="pto-timeline-time-selection__marker is-end"></span> | ||
| 106 | + `; | ||
| 107 | + root.appendChild(layer); | ||
| 108 | + return layer; | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + function updateSelectionLayer(layer, selection, geometry = {}) { | ||
| 112 | + if (!layer) return; | ||
| 113 | + const normalized = normalizeSelection(selection); | ||
| 114 | + const rangeStart = number(geometry.rangeStart, NaN); | ||
| 115 | + const rangeEnd = number(geometry.rangeEnd, NaN); | ||
| 116 | + const trackWidth = Math.max(1, number(geometry.trackWidth, 1)); | ||
| 117 | + if ( | ||
| 118 | + !normalized | ||
| 119 | + || !Number.isFinite(rangeStart) | ||
| 120 | + || !Number.isFinite(rangeEnd) | ||
| 121 | + || normalized.end < rangeStart | ||
| 122 | + || normalized.start > rangeEnd | ||
| 123 | + ) { | ||
| 124 | + layer.hidden = true; | ||
| 125 | + return; | ||
| 126 | + } | ||
| 127 | + const startX = timeToX(Math.max(normalized.start, rangeStart), trackWidth, rangeStart, rangeEnd); | ||
| 128 | + const endX = timeToX(Math.min(normalized.end, rangeEnd), trackWidth, rangeStart, rangeEnd); | ||
| 129 | + layer.hidden = false; | ||
| 130 | + layer.dataset.kind = normalized.kind || 'range'; | ||
| 131 | + layer.style.left = `${number(geometry.trackLeft)}px`; | ||
| 132 | + layer.style.top = `${number(geometry.top)}px`; | ||
| 133 | + layer.style.width = `${trackWidth}px`; | ||
| 134 | + layer.style.height = `${Math.max(1, number(geometry.height, 1))}px`; | ||
| 135 | + layer.style.setProperty('--pto-selection-start', `${startX}px`); | ||
| 136 | + layer.style.setProperty('--pto-selection-width', `${Math.max(1, endX - startX)}px`); | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + function createSelectionSummary(root, options = {}) { | ||
| 140 | + if (!root) throw new Error('Timeline selection summary root is required'); | ||
| 141 | + const summary = document.createElement('div'); | ||
| 142 | + summary.className = 'pto-timeline-time-selection-summary'; | ||
| 143 | + summary.hidden = true; | ||
| 144 | + summary.setAttribute('role', 'status'); | ||
| 145 | + summary.setAttribute('aria-live', 'polite'); | ||
| 146 | + summary.style.setProperty('--pto-timeline-label-width', `${number(options.labelWidth)}px`); | ||
| 147 | + root.appendChild(summary); | ||
| 148 | + return summary; | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + function updateSelectionSummary(summary, selection, language = 'en', geometry = null) { | ||
| 152 | + if (!summary) return; | ||
| 153 | + const normalized = normalizeSelection(selection); | ||
| 154 | + const content = formatSelectionSummary(normalized, language); | ||
| 155 | + summary.textContent = content; | ||
| 156 | + summary.hidden = !content; | ||
| 157 | + summary.dataset.kind = selection?.kind || 'range'; | ||
| 158 | + if (!content || !geometry) return; | ||
| 159 | + const rangeStart = number(geometry.rangeStart, NaN); | ||
| 160 | + const rangeEnd = number(geometry.rangeEnd, NaN); | ||
| 161 | + const trackWidth = Math.max(1, number(geometry.trackWidth, 1)); | ||
| 162 | + const viewportWidth = number(geometry.viewportWidth, NaN); | ||
| 163 | + if (![rangeStart, rangeEnd, viewportWidth].every(Number.isFinite)) return; | ||
| 164 | + | ||
| 165 | + const selectionCenter = (normalized.start + normalized.end) / 2; | ||
| 166 | + const contentX = number(geometry.trackLeft) | ||
| 167 | + + timeToX(selectionCenter, trackWidth, rangeStart, rangeEnd); | ||
| 168 | + const desiredLeft = contentX - number(geometry.scrollLeft); | ||
| 169 | + const padding = Math.max(0, number(geometry.viewportPadding, 8)); | ||
| 170 | + const availableWidth = Math.max(1, viewportWidth - padding * 2); | ||
| 171 | + const halfWidth = Math.min(summary.offsetWidth / 2, availableWidth / 2); | ||
| 172 | + const resolvedLeft = clamp( | ||
| 173 | + desiredLeft, | ||
| 174 | + padding + halfWidth, | ||
| 175 | + viewportWidth - padding - halfWidth, | ||
| 176 | + ); | ||
| 177 | + summary.style.left = `${resolvedLeft}px`; | ||
| 178 | + summary.dataset.alignment = Math.abs(resolvedLeft - desiredLeft) > 1 | ||
| 179 | + ? 'viewport-clamped' | ||
| 180 | + : 'selection-center'; | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + function bindTimelineInteraction(options = {}) { | ||
| 184 | + const target = options.target; | ||
| 185 | + if (!target) throw new Error('Timeline interaction target is required'); | ||
| 186 | + const threshold = Math.max(0, number(options.dragThreshold, DEFAULT_DRAG_THRESHOLD)); | ||
| 187 | + let drag = null; | ||
| 188 | + | ||
| 189 | + const geometry = () => { | ||
| 190 | + const next = options.getGeometry?.() || {}; | ||
| 191 | + return { | ||
| 192 | + width: Math.max(1, number(next.width, target.getBoundingClientRect().width || 1)), | ||
| 193 | + start: number(next.start), | ||
| 194 | + end: number(next.end), | ||
| 195 | + }; | ||
| 196 | + }; | ||
| 197 | + const localX = (event) => { | ||
| 198 | + const rect = target.getBoundingClientRect(); | ||
| 199 | + return clamp(event.clientX - rect.left, 0, Math.max(1, rect.width)); | ||
| 200 | + }; | ||
| 201 | + const selectionAt = (anchorX, currentX) => { | ||
| 202 | + const currentGeometry = geometry(); | ||
| 203 | + return normalizeSelection({ | ||
| 204 | + kind: 'range', | ||
| 205 | + start: xToTime(anchorX, currentGeometry.width, currentGeometry.start, currentGeometry.end), | ||
| 206 | + end: xToTime(currentX, currentGeometry.width, currentGeometry.start, currentGeometry.end), | ||
| 207 | + }); | ||
| 208 | + }; | ||
| 209 | + | ||
| 210 | + const onPointerDown = (event) => { | ||
| 211 | + if (event.button !== 0) return; | ||
| 212 | + target.focus?.({ preventScroll: true }); | ||
| 213 | + drag = { | ||
| 214 | + pointerId: event.pointerId, | ||
| 215 | + anchorX: localX(event), | ||
| 216 | + currentX: localX(event), | ||
| 217 | + moved: false, | ||
| 218 | + }; | ||
| 219 | + options.onInteractionStart?.(event); | ||
| 220 | + try { | ||
| 221 | + target.setPointerCapture(event.pointerId); | ||
| 222 | + } catch (_error) { | ||
| 223 | + // Pointer capture is an enhancement; document-local dragging still works without it. | ||
| 224 | + } | ||
| 225 | + }; | ||
| 226 | + | ||
| 227 | + const onPointerMove = (event) => { | ||
| 228 | + if (!drag || drag.pointerId !== event.pointerId) { | ||
| 229 | + options.onHover?.(event, localX(event)); | ||
| 230 | + return; | ||
| 231 | + } | ||
| 232 | + drag.currentX = localX(event); | ||
| 233 | + if (!drag.moved && Math.abs(drag.currentX - drag.anchorX) < threshold) return; | ||
| 234 | + if (!drag.moved) { | ||
| 235 | + drag.moved = true; | ||
| 236 | + target.classList.add('is-selecting-time'); | ||
| 237 | + options.onDragStart?.(event); | ||
| 238 | + } | ||
| 239 | + options.onPreview?.(selectionAt(drag.anchorX, drag.currentX), event); | ||
| 240 | + }; | ||
| 241 | + | ||
| 242 | + const finishPointer = (event, cancelled = false) => { | ||
| 243 | + if (!drag || drag.pointerId !== event.pointerId) return; | ||
| 244 | + const finished = drag; | ||
| 245 | + drag = null; | ||
| 246 | + target.classList.remove('is-selecting-time'); | ||
| 247 | + try { | ||
| 248 | + if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId); | ||
| 249 | + } catch (_error) { | ||
| 250 | + // The pointer may already have been released by the browser. | ||
| 251 | + } | ||
| 252 | + if (cancelled) { | ||
| 253 | + options.onCancel?.(event); | ||
| 254 | + } else if (finished.moved) { | ||
| 255 | + options.onCommit?.(selectionAt(finished.anchorX, localX(event)), event); | ||
| 256 | + } else { | ||
| 257 | + const hit = options.hitTest?.(localX(event), event); | ||
| 258 | + if (hit) options.onEventSelect?.(hit.event || hit, event); | ||
| 259 | + else options.onClear?.(event); | ||
| 260 | + } | ||
| 261 | + options.onInteractionEnd?.(event); | ||
| 262 | + }; | ||
| 263 | + | ||
| 264 | + const onPointerUp = (event) => finishPointer(event, false); | ||
| 265 | + const onPointerCancel = (event) => finishPointer(event, true); | ||
| 266 | + const onPointerLeave = (event) => { | ||
| 267 | + if (!drag) options.onLeave?.(event); | ||
| 268 | + }; | ||
| 269 | + const onWheel = (event) => { | ||
| 270 | + if (!event.ctrlKey && !event.metaKey) return; | ||
| 271 | + if (!event.deltaY) return; | ||
| 272 | + event.preventDefault(); | ||
| 273 | + options.onZoom?.({ | ||
| 274 | + direction: event.deltaY < 0 ? 1 : -1, | ||
| 275 | + x: localX(event), | ||
| 276 | + event, | ||
| 277 | + }); | ||
| 278 | + }; | ||
| 279 | + const onKeyDown = (event) => { | ||
| 280 | + if (event.key === 'Escape') { | ||
| 281 | + event.preventDefault(); | ||
| 282 | + options.onClear?.(event); | ||
| 283 | + return; | ||
| 284 | + } | ||
| 285 | + if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return; | ||
| 286 | + event.preventDefault(); | ||
| 287 | + options.onNavigate?.({ | ||
| 288 | + direction: event.key === 'ArrowLeft' ? -1 : 1, | ||
| 289 | + accelerated: event.shiftKey, | ||
| 290 | + event, | ||
| 291 | + }); | ||
| 292 | + }; | ||
| 293 | + | ||
| 294 | + target.addEventListener('pointerdown', onPointerDown); | ||
| 295 | + target.addEventListener('pointermove', onPointerMove); | ||
| 296 | + target.addEventListener('pointerup', onPointerUp); | ||
| 297 | + target.addEventListener('pointercancel', onPointerCancel); | ||
| 298 | + target.addEventListener('pointerleave', onPointerLeave); | ||
| 299 | + target.addEventListener('wheel', onWheel, { passive: false }); | ||
| 300 | + target.addEventListener('keydown', onKeyDown); | ||
| 301 | + | ||
| 302 | + return { | ||
| 303 | + destroy() { | ||
| 304 | + target.removeEventListener('pointerdown', onPointerDown); | ||
| 305 | + target.removeEventListener('pointermove', onPointerMove); | ||
| 306 | + target.removeEventListener('pointerup', onPointerUp); | ||
| 307 | + target.removeEventListener('pointercancel', onPointerCancel); | ||
| 308 | + target.removeEventListener('pointerleave', onPointerLeave); | ||
| 309 | + target.removeEventListener('wheel', onWheel); | ||
| 310 | + target.removeEventListener('keydown', onKeyDown); | ||
| 311 | + }, | ||
| 312 | + }; | ||
| 313 | + } | ||
| 314 | + | ||
| 315 | + global.PtoTimelineTimeSelectionPattern = { | ||
| 316 | + DEFAULT_DRAG_THRESHOLD, | ||
| 317 | + bindTimelineInteraction, | ||
| 318 | + createSelectionLayer, | ||
| 319 | + createSelectionSummary, | ||
| 320 | + formatAbsoluteUs, | ||
| 321 | + formatDuration, | ||
| 322 | + formatSelectionSummary, | ||
| 323 | + normalizeSelection, | ||
| 324 | + selectionFromEvent, | ||
| 325 | + timeToX, | ||
| 326 | + updateSelectionLayer, | ||
| 327 | + updateSelectionSummary, | ||
| 328 | + xToTime, | ||
| 329 | + }; | ||
| 330 | +})(window); | ||
| @@ -0,0 +1,121 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2025 Huawei Technologies Co., Ltd. | ||
| 3 | + * | ||
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 5 | + * you may not use this file except in compliance with the License. | ||
| 6 | + * You may obtain a copy of the License at | ||
| 7 | + * | ||
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 9 | + * | ||
| 10 | + * Unless required by applicable law or agreed to in writing, software | ||
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 13 | + * See the License for the specific language governing permissions and | ||
| 14 | + * limitations under the License. | ||
| 15 | + */ | ||
| 16 | + | ||
| 17 | +.pto-workbench-shell, | ||
| 18 | +.workbench-frame { | ||
| 19 | + --pto-workbench-shell-gutter: 10px; | ||
| 20 | + min-width: 0; | ||
| 21 | + min-height: 0; | ||
| 22 | + display: flex; | ||
| 23 | + flex-direction: column; | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +.pto-workbench-shell__panes, | ||
| 27 | +.workbench-frame-split { | ||
| 28 | + min-width: 0; | ||
| 29 | + min-height: 0; | ||
| 30 | + flex: 1 1 auto; | ||
| 31 | + display: flex; | ||
| 32 | + align-items: stretch; | ||
| 33 | + gap: 0; | ||
| 34 | + overflow: hidden; | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +.pto-workbench-shell__panes[data-split-direction="vertical"], | ||
| 38 | +.workbench-frame-split[data-split-direction="vertical"] { | ||
| 39 | + flex-direction: column; | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +.pto-workbench-shell__pane, | ||
| 43 | +.workbench-pane { | ||
| 44 | + min-width: 0; | ||
| 45 | + min-height: 0; | ||
| 46 | + flex: 0 0 auto; | ||
| 47 | + display: flex; | ||
| 48 | + flex-direction: column; | ||
| 49 | + overflow: hidden; | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | +.pto-workbench-shell__pane-body, | ||
| 53 | +.workbench-pane-body { | ||
| 54 | + min-width: 0; | ||
| 55 | + min-height: 0; | ||
| 56 | + flex: 1 1 auto; | ||
| 57 | + overflow: auto; | ||
| 58 | +} | ||
| 59 | + | ||
| 60 | +.pto-workbench-shell__split-gutter { | ||
| 61 | + flex: 0 0 var(--pto-workbench-shell-gutter, 10px); | ||
| 62 | + position: relative; | ||
| 63 | + min-width: var(--pto-workbench-shell-gutter, 10px); | ||
| 64 | + cursor: col-resize; | ||
| 65 | + touch-action: none; | ||
| 66 | + outline: none; | ||
| 67 | + background: transparent; | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +.pto-workbench-shell__split-gutter[data-split-direction="vertical"] { | ||
| 71 | + width: 100%; | ||
| 72 | + min-width: 0; | ||
| 73 | + min-height: var(--pto-workbench-shell-gutter, 10px); | ||
| 74 | + cursor: row-resize; | ||
| 75 | +} | ||
| 76 | + | ||
| 77 | +.pto-workbench-shell__split-gutter::before { | ||
| 78 | + content: ""; | ||
| 79 | + position: absolute; | ||
| 80 | + top: 50%; | ||
| 81 | + left: 50%; | ||
| 82 | + width: 3px; | ||
| 83 | + height: 44px; | ||
| 84 | + border-radius: var(--radius-pill, 999px); | ||
| 85 | + transform: translate(-50%, -50%); | ||
| 86 | + background: color-mix(in srgb, var(--foreground-muted, rgba(255, 255, 255, 0.42)) 46%, transparent); | ||
| 87 | + transition: | ||
| 88 | + background var(--duration-fast, 100ms) var(--easing-default, ease), | ||
| 89 | + width var(--duration-fast, 100ms) var(--easing-default, ease), | ||
| 90 | + height var(--duration-fast, 100ms) var(--easing-default, ease); | ||
| 91 | +} | ||
| 92 | + | ||
| 93 | +.pto-workbench-shell__split-gutter[data-split-direction="vertical"]::before { | ||
| 94 | + width: 44px; | ||
| 95 | + height: 3px; | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +.pto-workbench-shell__split-gutter:hover::before, | ||
| 99 | +.pto-workbench-shell__split-gutter:focus-visible::before, | ||
| 100 | +body.pto-is-pane-resizing .pto-workbench-shell__split-gutter::before { | ||
| 101 | + background: color-mix(in srgb, var(--foreground-secondary, rgba(255, 255, 255, 0.62)) 72%, transparent); | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +.pto-workbench-shell__split-gutter:focus-visible { | ||
| 105 | + box-shadow: 0 0 0 2px var(--focus-ring, rgba(67, 105, 239, 0.42)); | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +body.pto-is-pane-resizing, | ||
| 109 | +body.pto-is-pane-resizing * { | ||
| 110 | + user-select: none; | ||
| 111 | +} | ||
| 112 | + | ||
| 113 | +body.pto-is-pane-resizing[data-pto-resize-direction="horizontal"], | ||
| 114 | +body.pto-is-pane-resizing[data-pto-resize-direction="horizontal"] * { | ||
| 115 | + cursor: col-resize !important; | ||
| 116 | +} | ||
| 117 | + | ||
| 118 | +body.pto-is-pane-resizing[data-pto-resize-direction="vertical"], | ||
| 119 | +body.pto-is-pane-resizing[data-pto-resize-direction="vertical"] * { | ||
| 120 | + cursor: row-resize !important; | ||
| 121 | +} | ||
| @@ -0,0 +1,494 @@ | |||
| 1 | +// | ||
| 2 | +// Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 3 | +// you may not use this file except in compliance with the License. | ||
| 4 | +// You may obtain a copy of the License at | ||
| 5 | +// | ||
| 6 | +// http://www.apache.org/licenses/LICENSE-2.0 | ||
| 7 | +// | ||
| 8 | +// Unless required by applicable law or agreed to in writing, software | ||
| 9 | +// distributed under the License is distributed on an "AS IS" BASIS, | ||
| 10 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 11 | +// See the License for the specific language governing permissions and | ||
| 12 | +// limitations under the License. | ||
| 13 | +// | ||
| 14 | +(function attachPtoWorkbenchShell(global) { | ||
| 15 | + 'use strict'; | ||
| 16 | + | ||
| 17 | + const DEFAULT_ZOOM_MIN = 0.4; | ||
| 18 | + const DEFAULT_ZOOM_MAX = 1.2; | ||
| 19 | + const DEFAULT_ZOOM_STEP = 0.1; | ||
| 20 | + | ||
| 21 | + const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); | ||
| 22 | + const round = (value, precision = 2) => { | ||
| 23 | + const factor = 10 ** precision; | ||
| 24 | + return Math.round(value * factor) / factor; | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + const asElement = (target, root = document) => { | ||
| 28 | + if (!target) return null; | ||
| 29 | + if (target instanceof Element) return target; | ||
| 30 | + return root.querySelector(target); | ||
| 31 | + }; | ||
| 32 | + | ||
| 33 | + const normalizeDirection = (direction) => ( | ||
| 34 | + direction === 'vertical' ? 'vertical' : 'horizontal' | ||
| 35 | + ); | ||
| 36 | + | ||
| 37 | + const axisFor = (direction) => { | ||
| 38 | + const resolved = normalizeDirection(direction); | ||
| 39 | + return resolved === 'vertical' | ||
| 40 | + ? { | ||
| 41 | + direction: resolved, | ||
| 42 | + coordinate: 'clientY', | ||
| 43 | + size: 'height', | ||
| 44 | + sizeStyle: 'height', | ||
| 45 | + minStyle: 'minHeight', | ||
| 46 | + cursor: 'row-resize', | ||
| 47 | + ariaOrientation: 'horizontal', | ||
| 48 | + negativeKeys: new Set(['ArrowUp']), | ||
| 49 | + positiveKeys: new Set(['ArrowDown']), | ||
| 50 | + } | ||
| 51 | + : { | ||
| 52 | + direction: resolved, | ||
| 53 | + coordinate: 'clientX', | ||
| 54 | + size: 'width', | ||
| 55 | + sizeStyle: 'width', | ||
| 56 | + minStyle: 'minWidth', | ||
| 57 | + cursor: 'col-resize', | ||
| 58 | + ariaOrientation: 'vertical', | ||
| 59 | + negativeKeys: new Set(['ArrowLeft']), | ||
| 60 | + positiveKeys: new Set(['ArrowRight']), | ||
| 61 | + }; | ||
| 62 | + }; | ||
| 63 | + | ||
| 64 | + const normalizePanes = (panes, root = document) => { | ||
| 65 | + const items = panes | ||
| 66 | + ? Array.from(panes) | ||
| 67 | + : Array.from(root.querySelectorAll?.('[data-split-pane]') || []); | ||
| 68 | + return items.map((pane) => asElement(pane, root)).filter(Boolean); | ||
| 69 | + }; | ||
| 70 | + | ||
| 71 | + const normalizeSizes = (sizes, paneCount) => { | ||
| 72 | + const fallback = Array.from({ length: paneCount }, () => 100 / paneCount); | ||
| 73 | + if (!Array.isArray(sizes) || sizes.length !== paneCount) return fallback; | ||
| 74 | + if (!sizes.every((item) => Number.isFinite(item) && item > 0)) return fallback; | ||
| 75 | + const total = sizes.reduce((sum, size) => sum + size, 0); | ||
| 76 | + if (total <= 0) return fallback; | ||
| 77 | + return sizes.map((size) => size / total * 100); | ||
| 78 | + }; | ||
| 79 | + | ||
| 80 | + const normalizeMinSizes = (minSize, paneCount) => { | ||
| 81 | + if (Array.isArray(minSize)) { | ||
| 82 | + return Array.from({ length: paneCount }, (_item, index) => { | ||
| 83 | + const value = Number(minSize[index]); | ||
| 84 | + return Number.isFinite(value) ? Math.max(0, value) : 0; | ||
| 85 | + }); | ||
| 86 | + } | ||
| 87 | + const value = Number(minSize); | ||
| 88 | + return Array.from({ length: paneCount }, () => ( | ||
| 89 | + Number.isFinite(value) ? Math.max(0, value) : 0 | ||
| 90 | + )); | ||
| 91 | + }; | ||
| 92 | + | ||
| 93 | + const readStoredSizes = (key, fallback) => { | ||
| 94 | + if (!key) return fallback.slice(); | ||
| 95 | + try { | ||
| 96 | + const value = JSON.parse(global.localStorage?.getItem(key) || 'null'); | ||
| 97 | + if ( | ||
| 98 | + Array.isArray(value) | ||
| 99 | + && value.length === fallback.length | ||
| 100 | + && value.every((item) => Number.isFinite(item) && item > 0) | ||
| 101 | + ) { | ||
| 102 | + return normalizeSizes(value, fallback.length); | ||
| 103 | + } | ||
| 104 | + } catch (_error) { | ||
| 105 | + return fallback.slice(); | ||
| 106 | + } | ||
| 107 | + return fallback.slice(); | ||
| 108 | + }; | ||
| 109 | + | ||
| 110 | + const writeStoredSizes = (key, sizes) => { | ||
| 111 | + if (!key) return; | ||
| 112 | + try { | ||
| 113 | + global.localStorage?.setItem(key, JSON.stringify(sizes.map((item) => round(item, 2)))); | ||
| 114 | + } catch (_error) { | ||
| 115 | + // Storage can be unavailable in sandboxed previews; drag state still applies. | ||
| 116 | + } | ||
| 117 | + }; | ||
| 118 | + | ||
| 119 | + function createSplitGutter(index, direction = 'horizontal', options = {}) { | ||
| 120 | + const axis = axisFor(direction); | ||
| 121 | + const gutter = document.createElement('div'); | ||
| 122 | + gutter.className = options.gutterClass || 'pto-workbench-shell__split-gutter'; | ||
| 123 | + gutter.dataset.splitIndex = String(index); | ||
| 124 | + gutter.dataset.splitDirection = axis.direction; | ||
| 125 | + gutter.setAttribute('role', 'separator'); | ||
| 126 | + gutter.setAttribute('aria-orientation', axis.ariaOrientation); | ||
| 127 | + gutter.setAttribute('aria-label', options.gutterLabel || 'Resize adjacent panes'); | ||
| 128 | + gutter.tabIndex = options.keyboard === false ? -1 : 0; | ||
| 129 | + return gutter; | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + function getZoomLevels(options = {}) { | ||
| 133 | + const min = Number.isFinite(options.min) ? options.min : DEFAULT_ZOOM_MIN; | ||
| 134 | + const max = Number.isFinite(options.max) ? options.max : DEFAULT_ZOOM_MAX; | ||
| 135 | + const step = Number.isFinite(options.step) ? options.step : DEFAULT_ZOOM_STEP; | ||
| 136 | + const levels = []; | ||
| 137 | + for (let value = min; value <= max + step / 2; value += step) { | ||
| 138 | + levels.push(round(value, 2)); | ||
| 139 | + } | ||
| 140 | + return levels; | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + function nearestZoomIndex(levels, value) { | ||
| 144 | + const fallback = levels.indexOf(1); | ||
| 145 | + if (!Number.isFinite(value)) return fallback >= 0 ? fallback : 0; | ||
| 146 | + let bestIndex = 0; | ||
| 147 | + let bestDistance = Infinity; | ||
| 148 | + levels.forEach((level, index) => { | ||
| 149 | + const distance = Math.abs(level - value); | ||
| 150 | + if (distance < bestDistance) { | ||
| 151 | + bestIndex = index; | ||
| 152 | + bestDistance = distance; | ||
| 153 | + } | ||
| 154 | + }); | ||
| 155 | + return bestIndex; | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + function panePixelSizes(panes, axis) { | ||
| 159 | + return panes.map((pane) => pane.getBoundingClientRect()[axis.size]); | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + function panePercentSizes(panes, axis) { | ||
| 163 | + const sizes = panePixelSizes(panes, axis); | ||
| 164 | + const total = sizes.reduce((sum, size) => sum + size, 0); | ||
| 165 | + if (total <= 0) return []; | ||
| 166 | + return sizes.map((size) => size / total * 100); | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + function applyPaneSizes(panes, axis, sizes) { | ||
| 170 | + panes.forEach((pane, index) => { | ||
| 171 | + const size = Number.isFinite(sizes[index]) && sizes[index] > 0 ? sizes[index] : 1; | ||
| 172 | + pane.style.flex = `${size} 1 0%`; | ||
| 173 | + pane.style.flexBasis = '0%'; | ||
| 174 | + pane.style[axis.sizeStyle] = 'auto'; | ||
| 175 | + }); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + function resolvePairDelta(startSizes, pairIndex, delta, minSizes) { | ||
| 179 | + const nextSizes = startSizes.slice(); | ||
| 180 | + const leftIndex = pairIndex; | ||
| 181 | + const rightIndex = pairIndex + 1; | ||
| 182 | + const pairTotal = startSizes[leftIndex] + startSizes[rightIndex]; | ||
| 183 | + const leftMin = minSizes[leftIndex] || 0; | ||
| 184 | + const rightMin = minSizes[rightIndex] || 0; | ||
| 185 | + const maxLeft = Math.max(leftMin, pairTotal - rightMin); | ||
| 186 | + const nextLeft = clamp(startSizes[leftIndex] + delta, leftMin, maxLeft); | ||
| 187 | + nextSizes[leftIndex] = nextLeft; | ||
| 188 | + nextSizes[rightIndex] = Math.max(rightMin, pairTotal - nextLeft); | ||
| 189 | + return nextSizes; | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + function applyPixelSizesAsPercent(panes, axis, pixelSizes) { | ||
| 193 | + const total = pixelSizes.reduce((sum, size) => sum + size, 0); | ||
| 194 | + if (total <= 0) return []; | ||
| 195 | + const percent = pixelSizes.map((size) => size / total * 100); | ||
| 196 | + applyPaneSizes(panes, axis, percent); | ||
| 197 | + return percent; | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + function setResizeState(active, direction) { | ||
| 201 | + document.body.classList.toggle('pto-is-pane-resizing', active); | ||
| 202 | + if (active) { | ||
| 203 | + document.body.dataset.ptoResizeDirection = direction; | ||
| 204 | + } else { | ||
| 205 | + delete document.body.dataset.ptoResizeDirection; | ||
| 206 | + } | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + function initResizablePanes(rawOptions = {}) { | ||
| 210 | + const root = asElement(rawOptions.root) || document; | ||
| 211 | + const panes = normalizePanes(rawOptions.panes, root); | ||
| 212 | + if (panes.length < 2) { | ||
| 213 | + return { | ||
| 214 | + destroy() {}, | ||
| 215 | + getSizes: () => [], | ||
| 216 | + setSizes() {}, | ||
| 217 | + refresh() {}, | ||
| 218 | + }; | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + const parent = panes[0].parentElement; | ||
| 222 | + const axis = axisFor(rawOptions.direction); | ||
| 223 | + const fallbackSizes = normalizeSizes(rawOptions.sizes || rawOptions.defaultSize, panes.length); | ||
| 224 | + const initialSizes = readStoredSizes(rawOptions.storageKey, fallbackSizes); | ||
| 225 | + const minSizes = normalizeMinSizes(rawOptions.minSize ?? 0, panes.length); | ||
| 226 | + const gutterSize = Number.isFinite(rawOptions.gutterSize) ? rawOptions.gutterSize : 10; | ||
| 227 | + const keyboardStep = Number.isFinite(rawOptions.keyboardStep) ? rawOptions.keyboardStep : 24; | ||
| 228 | + const destroyFns = []; | ||
| 229 | + | ||
| 230 | + if (parent) parent.dataset.splitDirection = axis.direction; | ||
| 231 | + if (root.dataset) root.dataset.splitDirection = axis.direction; | ||
| 232 | + root.style?.setProperty?.('--pto-workbench-shell-gutter', `${gutterSize}px`); | ||
| 233 | + parent?.style?.setProperty?.('--pto-workbench-shell-gutter', `${gutterSize}px`); | ||
| 234 | + applyPaneSizes(panes, axis, initialSizes); | ||
| 235 | + | ||
| 236 | + const emit = (name, sizes, event) => { | ||
| 237 | + if (name === 'start') rawOptions.onDragStart?.(sizes, event); | ||
| 238 | + if (name === 'drag') rawOptions.onDrag?.(sizes, event); | ||
| 239 | + if (name === 'end') rawOptions.onDragEnd?.(sizes, event); | ||
| 240 | + rawOptions.onResize?.(sizes, { phase: name, event, direction: axis.direction }); | ||
| 241 | + }; | ||
| 242 | + | ||
| 243 | + panes.slice(0, -1).forEach((pane, index) => { | ||
| 244 | + const gutter = createSplitGutter(index + 1, axis.direction, rawOptions); | ||
| 245 | + gutter.style.setProperty('--pto-workbench-shell-gutter', `${gutterSize}px`); | ||
| 246 | + pane.after(gutter); | ||
| 247 | + rawOptions.onGutterCreate?.(gutter, { index, direction: axis.direction }); | ||
| 248 | + | ||
| 249 | + const applyDelta = (startSizes, delta, event, phase) => { | ||
| 250 | + const nextPixels = resolvePairDelta(startSizes, index, delta, minSizes); | ||
| 251 | + const nextSizes = applyPixelSizesAsPercent(panes, axis, nextPixels); | ||
| 252 | + emit(phase, nextSizes, event); | ||
| 253 | + return nextSizes; | ||
| 254 | + }; | ||
| 255 | + | ||
| 256 | + const onPointerDown = (event) => { | ||
| 257 | + if (event.button != null && event.button !== 0) return; | ||
| 258 | + event.preventDefault(); | ||
| 259 | + gutter.setPointerCapture?.(event.pointerId); | ||
| 260 | + setResizeState(true, axis.direction); | ||
| 261 | + const startCoordinate = event[axis.coordinate]; | ||
| 262 | + const startSizes = panePixelSizes(panes, axis); | ||
| 263 | + emit('start', panePercentSizes(panes, axis), event); | ||
| 264 | + | ||
| 265 | + const onMove = (moveEvent) => { | ||
| 266 | + const delta = moveEvent[axis.coordinate] - startCoordinate; | ||
| 267 | + applyDelta(startSizes, delta, moveEvent, 'drag'); | ||
| 268 | + }; | ||
| 269 | + | ||
| 270 | + const onUp = (upEvent) => { | ||
| 271 | + global.removeEventListener('pointermove', onMove); | ||
| 272 | + global.removeEventListener('pointerup', onUp); | ||
| 273 | + gutter.releasePointerCapture?.(upEvent.pointerId); | ||
| 274 | + setResizeState(false, axis.direction); | ||
| 275 | + const sizes = panePercentSizes(panes, axis); | ||
| 276 | + writeStoredSizes(rawOptions.storageKey, sizes); | ||
| 277 | + emit('end', sizes, upEvent); | ||
| 278 | + }; | ||
| 279 | + | ||
| 280 | + global.addEventListener('pointermove', onMove); | ||
| 281 | + global.addEventListener('pointerup', onUp, { once: true }); | ||
| 282 | + }; | ||
| 283 | + | ||
| 284 | + const onKeyDown = (event) => { | ||
| 285 | + if (rawOptions.keyboard === false) return; | ||
| 286 | + const multiplier = event.shiftKey ? 4 : 1; | ||
| 287 | + let delta = 0; | ||
| 288 | + if (axis.negativeKeys.has(event.key)) delta = -keyboardStep * multiplier; | ||
| 289 | + if (axis.positiveKeys.has(event.key)) delta = keyboardStep * multiplier; | ||
| 290 | + if (!delta) return; | ||
| 291 | + event.preventDefault(); | ||
| 292 | + const sizes = applyDelta(panePixelSizes(panes, axis), delta, event, 'drag'); | ||
| 293 | + writeStoredSizes(rawOptions.storageKey, sizes); | ||
| 294 | + emit('end', sizes, event); | ||
| 295 | + }; | ||
| 296 | + | ||
| 297 | + gutter.addEventListener('pointerdown', onPointerDown); | ||
| 298 | + gutter.addEventListener('keydown', onKeyDown); | ||
| 299 | + destroyFns.push(() => { | ||
| 300 | + gutter.removeEventListener('pointerdown', onPointerDown); | ||
| 301 | + gutter.removeEventListener('keydown', onKeyDown); | ||
| 302 | + gutter.remove(); | ||
| 303 | + }); | ||
| 304 | + }); | ||
| 305 | + | ||
| 306 | + return { | ||
| 307 | + destroy() { | ||
| 308 | + setResizeState(false, axis.direction); | ||
| 309 | + destroyFns.splice(0).forEach((fn) => fn()); | ||
| 310 | + panes.forEach((pane) => { | ||
| 311 | + pane.style.flex = ''; | ||
| 312 | + pane.style.flexBasis = ''; | ||
| 313 | + pane.style[axis.sizeStyle] = ''; | ||
| 314 | + }); | ||
| 315 | + if (parent) delete parent.dataset.splitDirection; | ||
| 316 | + if (root.dataset) delete root.dataset.splitDirection; | ||
| 317 | + }, | ||
| 318 | + getSizes: () => panePercentSizes(panes, axis), | ||
| 319 | + setSizes(nextSizes) { | ||
| 320 | + const sizes = normalizeSizes(nextSizes, panes.length); | ||
| 321 | + applyPaneSizes(panes, axis, sizes); | ||
| 322 | + writeStoredSizes(rawOptions.storageKey, sizes); | ||
| 323 | + rawOptions.onResize?.(sizes, { phase: 'api', direction: axis.direction }); | ||
| 324 | + }, | ||
| 325 | + refresh() { | ||
| 326 | + rawOptions.onResize?.(panePercentSizes(panes, axis), { | ||
| 327 | + phase: 'refresh', | ||
| 328 | + direction: axis.direction, | ||
| 329 | + }); | ||
| 330 | + }, | ||
| 331 | + }; | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + function initNestedResizablePanes(rawOptions = {}) { | ||
| 335 | + const configs = Array.isArray(rawOptions) | ||
| 336 | + ? rawOptions | ||
| 337 | + : (rawOptions.splits || []); | ||
| 338 | + const defaults = Array.isArray(rawOptions) ? {} : (rawOptions.defaults || {}); | ||
| 339 | + const instances = configs.map((config) => initResizablePanes({ ...defaults, ...config })); | ||
| 340 | + return { | ||
| 341 | + instances, | ||
| 342 | + destroy() { | ||
| 343 | + instances.splice(0).forEach((instance) => instance.destroy()); | ||
| 344 | + }, | ||
| 345 | + getSizes() { | ||
| 346 | + return instances.map((instance) => instance.getSizes()); | ||
| 347 | + }, | ||
| 348 | + refresh() { | ||
| 349 | + instances.forEach((instance) => instance.refresh()); | ||
| 350 | + }, | ||
| 351 | + }; | ||
| 352 | + } | ||
| 353 | + | ||
| 354 | + function initCanvasControls(rawOptions = {}) { | ||
| 355 | + const root = asElement(rawOptions.root) || document; | ||
| 356 | + const levels = rawOptions.levels || getZoomLevels(rawOptions); | ||
| 357 | + let zoomIndex = nearestZoomIndex(levels, rawOptions.defaultZoom ?? 1); | ||
| 358 | + let detailsVisible = rawOptions.detailsVisible ?? true; | ||
| 359 | + const detailToggle = asElement(rawOptions.detailToggle, root); | ||
| 360 | + const zoomOut = asElement(rawOptions.zoomOut, root); | ||
| 361 | + const zoomIn = asElement(rawOptions.zoomIn, root); | ||
| 362 | + const zoomReset = asElement(rawOptions.zoomReset, root); | ||
| 363 | + const zoomReadout = asElement(rawOptions.zoomReadout, root) || zoomReset; | ||
| 364 | + | ||
| 365 | + const apply = (source = 'sync') => { | ||
| 366 | + const zoom = levels[zoomIndex] || 1; | ||
| 367 | + rawOptions.onZoomChange?.(zoom, { source, zoomIndex, levels }); | ||
| 368 | + rawOptions.onDetailChange?.(detailsVisible, { source }); | ||
| 369 | + if (detailToggle) { | ||
| 370 | + detailToggle.textContent = detailsVisible | ||
| 371 | + ? (rawOptions.detailOnLabel || 'Details on') | ||
| 372 | + : (rawOptions.detailOffLabel || 'Details off'); | ||
| 373 | + detailToggle.setAttribute('aria-pressed', detailsVisible ? 'true' : 'false'); | ||
| 374 | + } | ||
| 375 | + if (zoomReadout) zoomReadout.textContent = `${Math.round(zoom * 100)}%`; | ||
| 376 | + if (zoomOut) zoomOut.disabled = zoomIndex <= 0; | ||
| 377 | + if (zoomIn) zoomIn.disabled = zoomIndex >= levels.length - 1; | ||
| 378 | + }; | ||
| 379 | + | ||
| 380 | + const setZoom = (nextZoom, source = 'api') => { | ||
| 381 | + zoomIndex = nearestZoomIndex(levels, nextZoom); | ||
| 382 | + apply(source); | ||
| 383 | + }; | ||
| 384 | + | ||
| 385 | + const listeners = []; | ||
| 386 | + const add = (node, type, handler) => { | ||
| 387 | + if (!node) return; | ||
| 388 | + node.addEventListener(type, handler); | ||
| 389 | + listeners.push(() => node.removeEventListener(type, handler)); | ||
| 390 | + }; | ||
| 391 | + | ||
| 392 | + add(detailToggle, 'click', () => { | ||
| 393 | + detailsVisible = !detailsVisible; | ||
| 394 | + apply('detail-toggle'); | ||
| 395 | + }); | ||
| 396 | + add(zoomOut, 'click', () => { | ||
| 397 | + zoomIndex = Math.max(0, zoomIndex - 1); | ||
| 398 | + apply('zoom-out'); | ||
| 399 | + }); | ||
| 400 | + add(zoomIn, 'click', () => { | ||
| 401 | + zoomIndex = Math.min(levels.length - 1, zoomIndex + 1); | ||
| 402 | + apply('zoom-in'); | ||
| 403 | + }); | ||
| 404 | + add(zoomReset, 'click', () => setZoom(rawOptions.resetZoom ?? 1, 'zoom-reset')); | ||
| 405 | + | ||
| 406 | + apply('init'); | ||
| 407 | + | ||
| 408 | + return { | ||
| 409 | + destroy() { | ||
| 410 | + listeners.splice(0).forEach((fn) => fn()); | ||
| 411 | + }, | ||
| 412 | + getZoom: () => levels[zoomIndex] || 1, | ||
| 413 | + getDetailsVisible: () => detailsVisible, | ||
| 414 | + setZoom, | ||
| 415 | + setDetailsVisible(nextValue, source = 'api') { | ||
| 416 | + detailsVisible = Boolean(nextValue); | ||
| 417 | + apply(source); | ||
| 418 | + }, | ||
| 419 | + refresh: apply, | ||
| 420 | + }; | ||
| 421 | + } | ||
| 422 | + | ||
| 423 | + function createRafCallback(callback) { | ||
| 424 | + let frame = 0; | ||
| 425 | + return (...args) => { | ||
| 426 | + if (frame) global.cancelAnimationFrame(frame); | ||
| 427 | + frame = global.requestAnimationFrame(() => { | ||
| 428 | + frame = 0; | ||
| 429 | + callback?.(...args); | ||
| 430 | + }); | ||
| 431 | + }; | ||
| 432 | + } | ||
| 433 | + | ||
| 434 | + function initWorkbenchShell(rawOptions = {}) { | ||
| 435 | + const root = asElement(rawOptions.root) || document; | ||
| 436 | + const scheduleLayout = createRafCallback(rawOptions.onLayout); | ||
| 437 | + const resizable = initResizablePanes({ | ||
| 438 | + root, | ||
| 439 | + panes: rawOptions.panes, | ||
| 440 | + direction: rawOptions.direction || 'horizontal', | ||
| 441 | + sizes: rawOptions.sizes, | ||
| 442 | + defaultSize: rawOptions.defaultSize, | ||
| 443 | + minSize: rawOptions.minSize, | ||
| 444 | + gutterSize: rawOptions.gutterSize, | ||
| 445 | + storageKey: rawOptions.storageKey, | ||
| 446 | + gutterClass: rawOptions.gutterClass, | ||
| 447 | + gutterLabel: rawOptions.gutterLabel, | ||
| 448 | + keyboard: rawOptions.keyboard, | ||
| 449 | + keyboardStep: rawOptions.keyboardStep, | ||
| 450 | + onDragStart: rawOptions.onDragStart, | ||
| 451 | + onDrag: (sizes, event) => { | ||
| 452 | + rawOptions.onDrag?.(sizes, event); | ||
| 453 | + scheduleLayout('drag'); | ||
| 454 | + }, | ||
| 455 | + onDragEnd: (sizes, event) => { | ||
| 456 | + rawOptions.onDragEnd?.(sizes, event); | ||
| 457 | + scheduleLayout('drag-end'); | ||
| 458 | + }, | ||
| 459 | + }); | ||
| 460 | + | ||
| 461 | + const canvasControls = rawOptions.canvasControls | ||
| 462 | + ? initCanvasControls({ | ||
| 463 | + ...rawOptions.canvasControls, | ||
| 464 | + root, | ||
| 465 | + }) | ||
| 466 | + : null; | ||
| 467 | + | ||
| 468 | + let resizeObserver = null; | ||
| 469 | + if (rawOptions.observeResize !== false && 'ResizeObserver' in global) { | ||
| 470 | + resizeObserver = new ResizeObserver(() => scheduleLayout('resize')); | ||
| 471 | + resizeObserver.observe(root); | ||
| 472 | + } | ||
| 473 | + | ||
| 474 | + return { | ||
| 475 | + destroy() { | ||
| 476 | + resizable.destroy(); | ||
| 477 | + canvasControls?.destroy(); | ||
| 478 | + resizeObserver?.disconnect(); | ||
| 479 | + }, | ||
| 480 | + resizable, | ||
| 481 | + canvasControls, | ||
| 482 | + refreshLayout: scheduleLayout, | ||
| 483 | + }; | ||
| 484 | + } | ||
| 485 | + | ||
| 486 | + global.PtoWorkbenchShell = { | ||
| 487 | + createSplitGutter, | ||
| 488 | + getZoomLevels, | ||
| 489 | + initCanvasControls, | ||
| 490 | + initNestedResizablePanes, | ||
| 491 | + initResizablePanes, | ||
| 492 | + initWorkbenchShell, | ||
| 493 | + }; | ||
| 494 | +})(window); | ||
| @@ -0,0 +1,306 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2025 Huawei Technologies Co., Ltd. | ||
| 3 | + * | ||
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 5 | + * you may not use this file except in compliance with the License. | ||
| 6 | + * You may obtain a copy of the License at | ||
| 7 | + * | ||
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 9 | + * | ||
| 10 | + * Unless required by applicable law or agreed to in writing, software | ||
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 13 | + * See the License for the specific language governing permissions and | ||
| 14 | + * limitations under the License. | ||
| 15 | + */ | ||
| 16 | + | ||
| 17 | +/* | ||
| 18 | + * Design Principle — Color Usage | ||
| 19 | + * Avoid large-area transparent color fills (e.g. rgba(blue, 0.12) card backgrounds). | ||
| 20 | + * Prefer: neutral surface (--surface-1/2) for card bg + small high-saturation tag/badge for type signal. | ||
| 21 | + * Transparent color fills are only acceptable for tight accent regions (tag, badge, icon bg) | ||
| 22 | + * or when explicitly requested by the designer. | ||
| 23 | + */ | ||
| 24 | + | ||
| 25 | +:root { | ||
| 26 | + /* Toolbar */ | ||
| 27 | + --comp-toolbar-height: 44px; | ||
| 28 | + --comp-toolbar-bg: color-mix(in srgb, var(--background) 92%, black); | ||
| 29 | + --comp-toolbar-border: var(--border-subtle); | ||
| 30 | + | ||
| 31 | + /* Button */ | ||
| 32 | + --button-height-sm: 30px; | ||
| 33 | + --button-height-md: 36px; | ||
| 34 | + --button-height-lg: 42px; | ||
| 35 | + --button-radius: var(--radius-lg); | ||
| 36 | + --button-padding-x-sm: var(--space-2); | ||
| 37 | + --button-padding-x-md: var(--space-3); | ||
| 38 | + --button-padding-x-lg: var(--space-4); | ||
| 39 | + --button-gap: 6px; | ||
| 40 | + --button-font: 500 12px / 1 var(--font-sans); | ||
| 41 | + --button-solid-bg: var(--foreground); | ||
| 42 | + --button-solid-bg-hover: color-mix(in srgb, var(--foreground) 88%, transparent); | ||
| 43 | + --button-solid-bg-press: color-mix(in srgb, var(--foreground) 80%, transparent); | ||
| 44 | + --button-solid-fg: var(--background); | ||
| 45 | + --button-solid-shadow: none; | ||
| 46 | + --button-secondary-bg: color-mix(in srgb, var(--surface-2) 92%, white 3%); | ||
| 47 | + --button-secondary-bg-hover: var(--surface-3); | ||
| 48 | + --button-secondary-bg-press: color-mix(in srgb, var(--surface-3) 84%, black); | ||
| 49 | + --button-secondary-border: var(--border-subtle); | ||
| 50 | + --button-secondary-fg: var(--foreground); | ||
| 51 | + --button-secondary-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 2px 8px rgba(0, 0, 0, 0.16); | ||
| 52 | + --button-ghost-bg-hover: var(--state-hover); | ||
| 53 | + --button-ghost-bg-press: var(--state-press); | ||
| 54 | + --button-ghost-fg: var(--foreground-secondary); | ||
| 55 | + --button-ghost-fg-hover: var(--foreground); | ||
| 56 | + --button-focus-ring: var(--focus-ring); | ||
| 57 | + --button-disabled-opacity: 0.42; | ||
| 58 | + | ||
| 59 | + /* Input */ | ||
| 60 | + --input-height-md: 34px; | ||
| 61 | + --input-radius: var(--radius-md); | ||
| 62 | + --input-border: var(--border-default); | ||
| 63 | + --input-bg: var(--surface-1); | ||
| 64 | + | ||
| 65 | + /* Panel */ | ||
| 66 | + --panel-radius: var(--radius-lg); | ||
| 67 | + --panel-padding: var(--space-4); | ||
| 68 | + --panel-padding-lg: var(--space-5); | ||
| 69 | + --panel-bg: var(--surface-2); | ||
| 70 | + --panel-border: var(--border-subtle); | ||
| 71 | + | ||
| 72 | + /* Card */ | ||
| 73 | + --card-radius: var(--radius-lg); | ||
| 74 | + --card-padding: var(--space-4); | ||
| 75 | + --card-bg: var(--surface-1); | ||
| 76 | + --card-border: var(--border-default); | ||
| 77 | + --card-shadow: none; | ||
| 78 | + --card-hover-border: var(--border-strong); | ||
| 79 | + --card-hover-bg: var(--state-hover); /* overlay on --card-bg, ArkUI-style */ | ||
| 80 | + --card-selected-border: var(--primary); | ||
| 81 | + --card-selected-bg: var(--state-selected); /* overlay on --card-bg */ | ||
| 82 | + | ||
| 83 | + /* Table */ | ||
| 84 | + --table-row-height: 36px; | ||
| 85 | + --table-header-height: 36px; | ||
| 86 | + --table-border: var(--border-subtle); | ||
| 87 | + | ||
| 88 | + /* Typography Composites — HarmonyOS dark aligned */ | ||
| 89 | + --text-display: 700 28px / 1.25 var(--font-sans); /* 页面大标题 */ | ||
| 90 | + --text-title-1: 600 20px / 1.30 var(--font-sans); /* 卡片/面板标题 */ | ||
| 91 | + --text-title-2: 600 16px / 1.30 var(--font-sans); /* 分组标题 */ | ||
| 92 | + --text-body: 400 14px / 1.50 var(--font-sans); /* 正文 */ | ||
| 93 | + --text-body-sm: 400 12px / 1.50 var(--font-sans); /* 次要说明 */ | ||
| 94 | + --text-label: 500 11px / 1.20 var(--font-sans); /* 标签/角标 */ | ||
| 95 | + --text-mono: 500 12px / 1.40 var(--font-mono); /* 数字/代码 */ | ||
| 96 | + | ||
| 97 | + /* Badge / Tag */ | ||
| 98 | + --tag-radius: var(--radius-pill); | ||
| 99 | + --tag-padding-x: var(--space-2); | ||
| 100 | + --tag-height: 20px; | ||
| 101 | + --comp-tag-bg: var(--state-muted); | ||
| 102 | + --comp-tag-border: var(--border-default); | ||
| 103 | + --comp-tag-fg: var(--foreground-muted); | ||
| 104 | + | ||
| 105 | + /* Stat Chip */ | ||
| 106 | + --stat-chip-height: 22px; | ||
| 107 | + --stat-chip-padding-x: 8px; | ||
| 108 | + --stat-chip-radius: var(--radius-pill); | ||
| 109 | + --stat-chip-bg: var(--surface-2); | ||
| 110 | + --stat-chip-border: var(--border-subtle); | ||
| 111 | + --stat-chip-fg: color-mix(in srgb, var(--foreground) 88%, var(--background)); | ||
| 112 | + | ||
| 113 | + /* Zoom Control Group */ | ||
| 114 | + --zoom-control-gap: 8px; | ||
| 115 | + --zoom-control-readout-fg: color-mix(in srgb, var(--foreground) 84%, var(--background)); | ||
| 116 | + | ||
| 117 | + /* Compact Button */ | ||
| 118 | + --button-height-compact: 34px; | ||
| 119 | + --button-padding-x-compact: 12px; | ||
| 120 | + | ||
| 121 | + /* Segmented Control */ | ||
| 122 | + --segmented-control-gap: 3px; | ||
| 123 | + --segmented-control-padding: 3px; | ||
| 124 | + --segmented-control-radius: 10px; | ||
| 125 | + --segmented-control-bg: var(--surface-disabled); | ||
| 126 | + --segmented-control-border: var(--border-subtle); | ||
| 127 | + | ||
| 128 | + /* Shared Panel Shell */ | ||
| 129 | + --panel-shell-radius: 14px; | ||
| 130 | + --panel-shell-bg: var(--background-elevated); | ||
| 131 | + --panel-shell-border: var(--border-subtle); | ||
| 132 | + --panel-shell-shadow: var(--shadow-lg); | ||
| 133 | + --panel-shell-backdrop-filter: var(--surface-backdrop-filter-soft); | ||
| 134 | + --panel-shell-quiet-bg: var(--surface-2); | ||
| 135 | + --panel-shell-quiet-border: transparent; | ||
| 136 | + --panel-shell-quiet-shadow: none; | ||
| 137 | + --panel-shell-header-padding: 12px 14px; | ||
| 138 | + --panel-shell-body-padding: 10px 16px 18px; | ||
| 139 | + | ||
| 140 | + /* Three-pane Workbench */ | ||
| 141 | + --workbench-frame-padding: var(--space-4); | ||
| 142 | + --workbench-frame-gap: var(--space-4); | ||
| 143 | + --workbench-frame-bg: var(--background-elevated); | ||
| 144 | + --workbench-pane-bg: var(--surface-2); | ||
| 145 | + --workbench-pane-border: var(--border-subtle); | ||
| 146 | + --workbench-pane-radius: var(--radius-xl); | ||
| 147 | + --workbench-pane-body-bg: color-mix(in srgb, var(--surface-1) 72%, transparent); | ||
| 148 | + | ||
| 149 | + /* IDE Frame */ | ||
| 150 | + --ide-frame-page-bg: var(--surface-3); | ||
| 151 | + --ide-frame-bg: var(--surface-2); | ||
| 152 | + --ide-frame-chrome-bg: transparent; | ||
| 153 | + --ide-frame-pane-bg: var(--background); | ||
| 154 | + --ide-frame-pane-border: var(--border-subtle); | ||
| 155 | + --ide-frame-control-bg: color-mix(in srgb, var(--ide-frame-page-bg) 28%, transparent); | ||
| 156 | + --ide-frame-control-border: transparent; | ||
| 157 | + --ide-frame-control-shadow: none; | ||
| 158 | + --ide-frame-selected-bg: color-mix(in srgb, var(--foreground) 8%, transparent); | ||
| 159 | + --ide-frame-selected-border: transparent; | ||
| 160 | + --ide-frame-selected-shadow: none; | ||
| 161 | + --ide-frame-header-height: 32px; | ||
| 162 | + --ide-frame-title-font: 600 12.5px / 1.2 var(--font-sans); | ||
| 163 | + --ide-frame-meta-font: 500 11px / 1.35 var(--font-mono); | ||
| 164 | + | ||
| 165 | + /* Floating Playback */ | ||
| 166 | + --floating-playback-bg: color-mix(in srgb, var(--background) 96%, black); | ||
| 167 | + --floating-playback-border: var(--border-default); | ||
| 168 | + --floating-playback-radius: 18px; | ||
| 169 | + --floating-playback-padding-y: 14px; | ||
| 170 | + --floating-playback-padding-x: 20px; | ||
| 171 | + --floating-playback-shadow: 0 16px 48px rgba(0, 0, 0, 0.60), 0 4px 16px rgba(0, 0, 0, 0.40); | ||
| 172 | + --floating-playback-inner-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 -1px 0 rgba(0, 0, 0, 0.50); | ||
| 173 | + --floating-playback-expanded-width: 1180px; | ||
| 174 | + --floating-playback-expanded-min-height: 60px; | ||
| 175 | + --floating-playback-collapsed-size: 44px; | ||
| 176 | + --floating-playback-collapsed-radius: var(--radius-lg); | ||
| 177 | + --floating-playback-collapsed-bg: var(--primary); | ||
| 178 | + --floating-playback-collapsed-border: var(--primary); | ||
| 179 | + --floating-playback-collapsed-fg: var(--primary-foreground); | ||
| 180 | + --floating-playback-collapsed-shadow: 0 10px 24px rgba(0, 0, 0, 0.38), inset 0 1px 0 rgba(255, 255, 255, 0.18); | ||
| 181 | + --floating-playback-toggle-fg: var(--foreground-secondary); | ||
| 182 | + --floating-playback-strong-fg: color-mix(in srgb, var(--foreground) 82%, var(--background)); | ||
| 183 | + --floating-playback-separator: var(--border-default); | ||
| 184 | + --floating-playback-scrubber-track: var(--border-strong); | ||
| 185 | + --floating-playback-scrubber-thumb-border: color-mix(in srgb, var(--primary) 80%, transparent); | ||
| 186 | + --floating-playback-scrubber-thumb-shadow: 0 0 8px color-mix(in srgb, var(--primary) 55%, transparent); | ||
| 187 | + --floating-playback-hover-bg: color-mix(in srgb, var(--background-elevated) 96%, black); | ||
| 188 | + --floating-playback-hover-border: var(--border-default); | ||
| 189 | + --floating-playback-button-height: 32px; | ||
| 190 | + --floating-playback-button-radius: var(--radius-md); | ||
| 191 | + --floating-playback-button-bg: var(--surface-disabled); | ||
| 192 | + --floating-playback-button-border: var(--border-subtle); | ||
| 193 | + --floating-playback-button-hover-bg: var(--state-hover); | ||
| 194 | + --floating-playback-button-hover-border: var(--border-default); | ||
| 195 | + --floating-playback-primary-bg: var(--primary); | ||
| 196 | + --floating-playback-primary-bg-hover: var(--primary-hover); | ||
| 197 | + --floating-playback-primary-border: var(--primary); | ||
| 198 | + --floating-playback-primary-fg: var(--primary-foreground); | ||
| 199 | + | ||
| 200 | + /* Inspector Rail */ | ||
| 201 | + --inspector-rail-padding-x: var(--space-3); | ||
| 202 | + --inspector-rail-padding-bottom: var(--space-3); | ||
| 203 | + --inspector-section-padding-y: var(--space-4); | ||
| 204 | + --inspector-section-gap: var(--space-3); | ||
| 205 | + --inspector-section-divider: var(--border-subtle); | ||
| 206 | + --inspector-soft-card-bg: color-mix(in srgb, var(--surface-2) 80%, transparent); | ||
| 207 | + --inspector-soft-card-bg-hover: color-mix(in srgb, var(--surface-3) 62%, transparent); | ||
| 208 | + --inspector-soft-card-radius: var(--radius-md); | ||
| 209 | + --inspector-soft-card-padding: var(--space-3); | ||
| 210 | + --inspector-soft-card-gap: var(--space-2); | ||
| 211 | +} | ||
| 212 | + | ||
| 213 | +:root[data-theme='light'] { | ||
| 214 | + --button-secondary-bg: color-mix(in srgb, var(--surface-1) 92%, var(--surface-2)); | ||
| 215 | + --button-secondary-bg-hover: color-mix(in srgb, var(--surface-1) 28%, var(--surface-2)); | ||
| 216 | + --button-secondary-bg-press: color-mix(in srgb, var(--surface-2) 84%, var(--surface-3)); | ||
| 217 | + --button-secondary-border: var(--border-default); | ||
| 218 | + --button-secondary-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78), 0 1px 2px rgba(0, 0, 0, 0.06); | ||
| 219 | + --ide-frame-page-bg: var(--background-elevated); | ||
| 220 | + --ide-frame-bg: var(--background); | ||
| 221 | + --ide-frame-pane-bg: var(--surface-2); | ||
| 222 | + --floating-playback-bg: color-mix(in srgb, var(--background-elevated) 92%, white); | ||
| 223 | + --floating-playback-shadow: 0 16px 42px rgba(0, 0, 0, 0.16), 0 4px 14px rgba(0, 0, 0, 0.08); | ||
| 224 | + --floating-playback-inner-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.86), inset 0 -1px 0 rgba(0, 0, 0, 0.04); | ||
| 225 | + --floating-playback-collapsed-shadow: 0 10px 22px rgba(0, 0, 0, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.30); | ||
| 226 | + --floating-playback-hover-bg: var(--background-elevated); | ||
| 227 | +} | ||
| 228 | + | ||
| 229 | +:root[data-theme='glass'] { | ||
| 230 | + --comp-toolbar-bg: var(--glass-surface-bg-strong); | ||
| 231 | + --comp-toolbar-border: var(--glass-surface-border); | ||
| 232 | + | ||
| 233 | + --button-solid-bg: rgba(255, 255, 255, 0.88); | ||
| 234 | + --button-solid-bg-hover: rgba(255, 255, 255, 0.96); | ||
| 235 | + --button-solid-bg-press: rgba(255, 255, 255, 0.78); | ||
| 236 | + --button-solid-fg: #08111f; | ||
| 237 | + --button-solid-shadow: var(--glass-surface-highlight), 0 8px 20px rgba(0, 0, 0, 0.18); | ||
| 238 | + --button-secondary-bg: rgba(255, 255, 255, 0.105); | ||
| 239 | + --button-secondary-bg-hover: rgba(255, 255, 255, 0.16); | ||
| 240 | + --button-secondary-bg-press: rgba(255, 255, 255, 0.08); | ||
| 241 | + --button-secondary-border: rgba(255, 255, 255, 0.12); | ||
| 242 | + --button-secondary-shadow: var(--glass-surface-highlight), 0 8px 24px rgba(0, 0, 0, 0.14); | ||
| 243 | + --button-ghost-bg-hover: rgba(255, 255, 255, 0.10); | ||
| 244 | + --button-ghost-bg-press: rgba(255, 255, 255, 0.15); | ||
| 245 | + | ||
| 246 | + --input-bg: rgba(255, 255, 255, 0.08); | ||
| 247 | + --input-border: rgba(255, 255, 255, 0.12); | ||
| 248 | + | ||
| 249 | + --panel-bg: color-mix(in srgb, var(--background-elevated) 88%, black 12%); | ||
| 250 | + --panel-border: var(--border-subtle); | ||
| 251 | + --card-bg: color-mix(in srgb, var(--background-elevated) 84%, black 16%); | ||
| 252 | + --card-border: var(--border-subtle); | ||
| 253 | + --card-shadow: none; | ||
| 254 | + --card-hover-border: var(--border-strong); | ||
| 255 | + --card-hover-bg: rgba(255, 255, 255, 0.08); | ||
| 256 | + --card-selected-bg: rgba(111, 161, 255, 0.18); | ||
| 257 | + | ||
| 258 | + --comp-tag-bg: rgba(255, 255, 255, 0.075); | ||
| 259 | + --comp-tag-border: rgba(255, 255, 255, 0.16); | ||
| 260 | + --comp-tag-fg: var(--foreground-secondary); | ||
| 261 | + | ||
| 262 | + --stat-chip-bg: rgba(255, 255, 255, 0.09); | ||
| 263 | + --stat-chip-border: rgba(255, 255, 255, 0.14); | ||
| 264 | + --segmented-control-bg: rgba(255, 255, 255, 0.065); | ||
| 265 | + --segmented-control-border: rgba(255, 255, 255, 0.14); | ||
| 266 | + | ||
| 267 | + --panel-shell-bg: var(--glass-surface-bg); | ||
| 268 | + --panel-shell-border: var(--glass-surface-border); | ||
| 269 | + --panel-shell-shadow: var(--glass-surface-highlight), var(--glass-surface-shadow); | ||
| 270 | + --panel-shell-backdrop-filter: var(--surface-backdrop-filter); | ||
| 271 | + --panel-shell-quiet-bg: var(--glass-surface-bg-soft); | ||
| 272 | + --panel-shell-quiet-border: rgba(255, 255, 255, 0.12); | ||
| 273 | + --panel-shell-quiet-shadow: var(--glass-surface-highlight); | ||
| 274 | + | ||
| 275 | + --workbench-frame-bg: color-mix(in srgb, var(--background-elevated) 88%, black 12%); | ||
| 276 | + --workbench-pane-bg: color-mix(in srgb, var(--background) 92%, var(--surface-2)); | ||
| 277 | + --workbench-pane-border: rgba(255, 255, 255, 0.09); | ||
| 278 | + --workbench-pane-body-bg: rgba(8, 10, 15, 0.78); | ||
| 279 | + | ||
| 280 | + --ide-frame-page-bg: var(--app-background); | ||
| 281 | + --ide-frame-bg: #0b1018; | ||
| 282 | + --ide-frame-chrome-bg: transparent; | ||
| 283 | + --ide-frame-pane-bg: #080c12; | ||
| 284 | + --ide-frame-pane-border: rgba(255, 255, 255, 0.08); | ||
| 285 | + --ide-frame-control-bg: var(--glass-surface-bg-soft); | ||
| 286 | + --ide-frame-control-border: rgba(255, 255, 255, 0.12); | ||
| 287 | + --ide-frame-control-shadow: var(--glass-surface-highlight); | ||
| 288 | + --ide-frame-selected-bg: var(--glass-surface-bg); | ||
| 289 | + --ide-frame-selected-border: rgba(255, 255, 255, 0.16); | ||
| 290 | + --ide-frame-selected-shadow: var(--glass-surface-highlight), 0 8px 24px rgba(0, 0, 0, 0.14); | ||
| 291 | + | ||
| 292 | + --floating-playback-bg: var(--glass-surface-bg-strong); | ||
| 293 | + --floating-playback-border: var(--glass-surface-border); | ||
| 294 | + --floating-playback-shadow: var(--glass-surface-shadow); | ||
| 295 | + --floating-playback-inner-highlight: var(--glass-surface-highlight); | ||
| 296 | + --floating-playback-collapsed-shadow: 0 12px 30px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.24); | ||
| 297 | + --floating-playback-hover-bg: var(--glass-surface-bg); | ||
| 298 | + --floating-playback-hover-border: rgba(255, 255, 255, 0.16); | ||
| 299 | + --floating-playback-button-bg: rgba(255, 255, 255, 0.09); | ||
| 300 | + --floating-playback-button-border: rgba(255, 255, 255, 0.12); | ||
| 301 | + --floating-playback-button-hover-bg: rgba(255, 255, 255, 0.14); | ||
| 302 | + --floating-playback-button-hover-border: rgba(255, 255, 255, 0.16); | ||
| 303 | + | ||
| 304 | + --inspector-soft-card-bg: rgba(255, 255, 255, 0.075); | ||
| 305 | + --inspector-soft-card-bg-hover: rgba(255, 255, 255, 0.115); | ||
| 306 | +} | ||