已合并
feat: add CANN performance UI report skill #517
feat: add CANN performance UI report skill #517
已合并
unstain创建于 8月20日
共 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: local31 - repo: local
29 hooks:32 hooks:
30 - id: oat-check33 - id: oat-check
31 name: OAT Compliance Check34 name: OAT Compliance Check
32 entry: bash scripts/oat_check.sh35 entry: bash scripts/oat_check.sh
33 language: system36 language: system
34- pass_filenames: true37+ pass_filenames: false
35- types: [file]38+ require_serial: true
36 stages: [pre-commit]39 stages: [pre-commit]
37 verbose: true40 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."
@@ -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+}