@xpert-ai/plugin-live-dashboard 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.xpertai-plugin/plugin.json +10 -2
  2. package/README.md +6 -0
  3. package/assets/branding/cover-v1.prompt.txt +27 -0
  4. package/assets/branding/cover-v1.webp +0 -0
  5. package/assets/previews/customer-brief.png +0 -0
  6. package/assets/previews/profit-atlas.png +0 -0
  7. package/assets/previews/trend-workspace.png +0 -0
  8. package/dist/index.js +6 -3
  9. package/dist/lib/agent.schema.d.ts +43 -37
  10. package/dist/lib/agent.schema.js +2 -1
  11. package/dist/lib/app-config.d.ts +62 -0
  12. package/dist/lib/app-config.js +38 -0
  13. package/dist/lib/artifact-type.provider.d.ts +15 -0
  14. package/dist/lib/artifact-type.provider.js +78 -0
  15. package/dist/lib/assistant-template.d.ts +1 -2
  16. package/dist/lib/assistant-template.js +3 -52
  17. package/dist/lib/content.provider.js +13 -5
  18. package/dist/lib/dashboard-navigation.d.ts +58 -14
  19. package/dist/lib/dashboard-navigation.js +11 -10
  20. package/dist/lib/dashboard-view.schema.d.ts +197 -46
  21. package/dist/lib/dashboard-view.schema.js +8 -1
  22. package/dist/lib/dashboard.service.d.ts +39 -2
  23. package/dist/lib/dashboard.service.js +52 -28
  24. package/dist/lib/document-libraries.d.ts +1 -1
  25. package/dist/lib/document-libraries.js +3 -2
  26. package/dist/lib/document-network.d.ts +1 -1
  27. package/dist/lib/document-network.js +2 -6
  28. package/dist/lib/editing-feedback.d.ts +5 -0
  29. package/dist/lib/editing-feedback.js +19 -0
  30. package/dist/lib/editing-patch.d.ts +22 -0
  31. package/dist/lib/editing-patch.js +46 -0
  32. package/dist/lib/editing-session.d.ts +167 -0
  33. package/dist/lib/editing-session.js +53 -0
  34. package/dist/lib/editing.d.ts +92 -60
  35. package/dist/lib/editing.js +36 -24
  36. package/dist/lib/html-guidance.d.ts +1 -1
  37. package/dist/lib/html-guidance.js +1 -1
  38. package/dist/lib/middleware.js +42 -18
  39. package/dist/lib/monitor.d.ts +70 -0
  40. package/dist/lib/monitor.js +101 -0
  41. package/dist/lib/plugin.js +4 -1
  42. package/dist/lib/project.d.ts +106 -0
  43. package/dist/lib/project.js +17 -0
  44. package/dist/lib/projects.schema.d.ts +49 -0
  45. package/dist/lib/projects.schema.js +7 -0
  46. package/dist/lib/projects.service.d.ts +23 -0
  47. package/dist/lib/projects.service.js +65 -0
  48. package/dist/lib/resource-card.js +1 -1
  49. package/dist/lib/runtime-context.d.ts +3 -0
  50. package/dist/lib/runtime-context.js +13 -0
  51. package/dist/lib/save-evidence.d.ts +1 -1
  52. package/dist/lib/templates.d.ts +2 -0
  53. package/dist/lib/templates.js +55 -0
  54. package/dist/lib/view.provider.d.ts +97 -7
  55. package/dist/lib/view.provider.js +39 -10
  56. package/dist/libraries/TREND-WORKSPACE-ICONS-LICENSE.txt +43 -0
  57. package/dist/libraries/catalog.json +1 -1
  58. package/dist/remote/app.css +1 -1
  59. package/dist/remote/app.js +33 -33
  60. package/dist/remote/build-manifest.json +115 -32
  61. package/dist/sandbox-actions/action.json +1 -1
  62. package/dist/sandbox-actions/bundle/host.js +2 -2
  63. package/dist/sandbox-actions/bundle/resource-cache.mjs +2 -1
  64. package/dist/sandbox-actions/bundle/validate.mjs +1 -1
  65. package/docs/docs.json +7 -0
  66. package/docs/frontend-libraries.mdx +12 -4
  67. package/docs/html-mode.md +3 -3
  68. package/docs/quick-start-acceptance.mdx +50 -0
  69. package/docs/quick-start.mdx +64 -0
  70. package/docs/superstore.mdx +38 -0
  71. package/docs/team-sharing.mdx +6 -2
  72. package/docs/trend-workspace-acceptance.mdx +43 -0
  73. package/docs/trend-workspace.mdx +30 -0
  74. package/docs/verification.mdx +4 -2
  75. package/docs/visual-styles-acceptance.mdx +40 -0
  76. package/docs/visual-styles.mdx +46 -0
  77. package/package.json +23 -17
  78. package/skills/live-dashboard/SKILL.md +27 -3
  79. package/skills/live-dashboard/examples/README.md +25 -0
  80. package/skills/live-dashboard/examples/catalog.json +79 -0
  81. package/skills/live-dashboard/examples/production-quality/README.md +25 -0
  82. package/skills/live-dashboard/examples/production-quality/dashboard.html +129 -0
  83. package/skills/live-dashboard/examples/production-quality/definition.json +67 -0
  84. package/skills/live-dashboard/examples/production-quality/demo.csv +7 -0
  85. package/skills/live-dashboard/examples/production-quality/demo.json +44 -0
  86. package/skills/live-dashboard/examples/production-quality/expected.json +26 -0
  87. package/skills/live-dashboard/examples/production-quality/preview.png +0 -0
  88. package/skills/live-dashboard/examples/production-quality/query.json +53 -0
  89. package/skills/live-dashboard/examples/regional-sales/README.md +25 -0
  90. package/skills/live-dashboard/examples/regional-sales/dashboard-create.json +35 -0
  91. package/skills/live-dashboard/examples/regional-sales/definition.json +35 -0
  92. package/skills/live-dashboard/examples/regional-sales/demo.csv +5 -0
  93. package/skills/live-dashboard/examples/regional-sales/demo.json +18 -0
  94. package/skills/live-dashboard/examples/regional-sales/expected.json +19 -0
  95. package/skills/live-dashboard/examples/regional-sales/preview.png +0 -0
  96. package/skills/live-dashboard/examples/regional-sales/query.json +45 -0
  97. package/skills/live-dashboard/examples/sales-overview/README.md +25 -0
  98. package/skills/live-dashboard/examples/sales-overview/dashboard.html +129 -0
  99. package/skills/live-dashboard/examples/sales-overview/definition.json +57 -0
  100. package/skills/live-dashboard/examples/sales-overview/demo.csv +7 -0
  101. package/skills/live-dashboard/examples/sales-overview/demo.json +38 -0
  102. package/skills/live-dashboard/examples/sales-overview/expected.json +22 -0
  103. package/skills/live-dashboard/examples/sales-overview/preview.png +0 -0
  104. package/skills/live-dashboard/examples/sales-overview/query.json +52 -0
  105. package/skills/live-dashboard/examples/superstore/README.md +64 -0
  106. package/skills/live-dashboard/examples/superstore/THIRD_PARTY_LICENSE.txt +207 -0
  107. package/skills/live-dashboard/examples/superstore/build_data.py +155 -0
  108. package/skills/live-dashboard/examples/superstore/build_pages.py +129 -0
  109. package/skills/live-dashboard/examples/superstore/checks.json +104 -0
  110. package/skills/live-dashboard/examples/superstore/customers-mobile-dark.png +0 -0
  111. package/skills/live-dashboard/examples/superstore/customers-select.png +0 -0
  112. package/skills/live-dashboard/examples/superstore/customers.html +316 -0
  113. package/skills/live-dashboard/examples/superstore/customers.png +0 -0
  114. package/skills/live-dashboard/examples/superstore/customers.queries.json +512 -0
  115. package/skills/live-dashboard/examples/superstore/expected.json +4069 -0
  116. package/skills/live-dashboard/examples/superstore/import.json +200 -0
  117. package/skills/live-dashboard/examples/superstore/orders.csv +10195 -0
  118. package/skills/live-dashboard/examples/superstore/overview-mobile-dark.png +0 -0
  119. package/skills/live-dashboard/examples/superstore/overview-select.png +0 -0
  120. package/skills/live-dashboard/examples/superstore/overview.html +310 -0
  121. package/skills/live-dashboard/examples/superstore/overview.png +0 -0
  122. package/skills/live-dashboard/examples/superstore/overview.queries.json +636 -0
  123. package/skills/live-dashboard/examples/superstore/page.js +109 -0
  124. package/skills/live-dashboard/examples/superstore/profitability-mobile-dark.png +0 -0
  125. package/skills/live-dashboard/examples/superstore/profitability-select.png +0 -0
  126. package/skills/live-dashboard/examples/superstore/profitability.html +307 -0
  127. package/skills/live-dashboard/examples/superstore/profitability.png +0 -0
  128. package/skills/live-dashboard/examples/superstore/profitability.queries.json +334 -0
  129. package/skills/live-dashboard/examples/superstore/provenance.json +24 -0
  130. package/skills/live-dashboard/examples/superstore/semantic-model.json +220 -0
  131. package/skills/live-dashboard/examples/superstore/test_data.py +62 -0
  132. package/skills/live-dashboard/references/html-runtime.md +1 -1
  133. package/skills/live-dashboard/themes/README.md +50 -0
  134. package/skills/live-dashboard/themes/customer-brief/README.md +47 -0
  135. package/skills/live-dashboard/themes/customer-brief/charts.js +17 -0
  136. package/skills/live-dashboard/themes/customer-brief/layout.html +22 -0
  137. package/skills/live-dashboard/themes/customer-brief/theme.css +108 -0
  138. package/skills/live-dashboard/themes/profit-atlas/README.md +43 -0
  139. package/skills/live-dashboard/themes/profit-atlas/charts.js +30 -0
  140. package/skills/live-dashboard/themes/profit-atlas/layout.html +19 -0
  141. package/skills/live-dashboard/themes/profit-atlas/theme.css +75 -0
  142. package/skills/live-dashboard/themes/trend-workspace/ICON_LICENSE.txt +43 -0
  143. package/skills/live-dashboard/themes/trend-workspace/README.md +144 -0
  144. package/skills/live-dashboard/themes/trend-workspace/charts.js +60 -0
  145. package/skills/live-dashboard/themes/trend-workspace/icons.js +11 -0
  146. package/skills/live-dashboard/themes/trend-workspace/layout.html +45 -0
  147. package/skills/live-dashboard/themes/trend-workspace/select.js +127 -0
  148. package/skills/live-dashboard/themes/trend-workspace/theme.css +137 -0
@@ -0,0 +1,60 @@
1
+ // Generic ECharts recipes: business fields/labels are explicit arguments, never inferred from names.
2
+ const TrendWorkspaceCharts = (() => {
3
+ function tokens() {
4
+ const style = getComputedStyle(document.body);
5
+ return Object.fromEntries(['ink', 'muted', 'line', 'blue', 'teal', 'red'].map(key => [key, style.getPropertyValue('--' + key).trim()]));
6
+ }
7
+ const compact = value => Math.abs(value) >= 1000000 ? value / 1000000 + 'm' : Math.abs(value) >= 1000 ? value / 1000 + 'k' : String(value);
8
+ const amount = value => new Intl.NumberFormat('en-US', {maximumFractionDigits: 2}).format(value);
9
+ function axis() {
10
+ const t = tokens();
11
+ return {axisLabel: {color: t.muted, fontSize: 11}, nameTextStyle: {color: t.muted}, axisTick: {show: false},
12
+ axisLine: {lineStyle: {color: t.line}}, splitLine: {lineStyle: {color: t.line}}};
13
+ }
14
+ function line(chart, rows, {dimension, measure, label, color = 'blue'}) {
15
+ const t = tokens(), a = axis();
16
+ chart.setOption({animation: false, aria: {enabled: true}, textStyle: {fontFamily: 'system-ui, sans-serif'},
17
+ grid: {left: 48, right: 34, top: 14, bottom: 28}, color: [t[color]], tooltip: {trigger: 'axis', confine: true},
18
+ xAxis: {...a, type: 'category', boundaryGap: false, data: rows.map(row => row[dimension]), splitLine: {show: false},
19
+ axisLabel: {...a.axisLabel, hideOverlap: true, showMaxLabel: true}},
20
+ yAxis: {...a, type: 'value', axisLabel: {...a.axisLabel, formatter: compact}},
21
+ series: [{id: measure, name: label, type: 'line', smooth: false, showSymbol: false, lineStyle: {width: 2},
22
+ ...(color === 'blue' ? {areaStyle: {opacity: .045}} : {}), data: rows.map(row => Number(row[measure])),
23
+ ...(rows.some(row => Number(row[measure]) < 0) ? {markLine: {silent: true, symbol: 'none', label: {show: false},
24
+ lineStyle: {color: t.muted, type: 'dashed', width: 1}, data: [{yAxis: 0}]}} : {})}]
25
+ }, {notMerge: true});
26
+ }
27
+ function horizontal(chart, rows, {dimension, measure, label, color = 'blue', decimals = 2}) {
28
+ const t = tokens(), a = axis();
29
+ const narrow = chart.getWidth() < 450;
30
+ chart.setOption({animation: false, aria: {enabled: true}, textStyle: {fontFamily: 'system-ui, sans-serif'},
31
+ grid: {left: narrow ? 78 : 96, right: narrow ? 70 : 92, top: 8, bottom: 26},
32
+ tooltip: {trigger: 'axis', confine: true, axisPointer: {type: 'shadow'}},
33
+ xAxis: {...a, type: 'value', splitNumber: 4, axisLabel: {...a.axisLabel, formatter: compact}},
34
+ yAxis: {...a, type: 'category', inverse: true, data: rows.map(row => row[dimension]), splitLine: {show: false},
35
+ axisLine: {show: false}, axisLabel: {...a.axisLabel, width: narrow ? 72 : 90, overflow: 'truncate'}},
36
+ series: [{id: measure, name: label, type: 'bar', barMaxWidth: 20,
37
+ label: {show: true, position: 'right', color: t.ink, fontSize: 11,
38
+ formatter: item => new Intl.NumberFormat('en-US', {minimumFractionDigits: decimals, maximumFractionDigits: decimals}).format(item.value)},
39
+ data: rows.map(row => ({value: Number(row[measure]), itemStyle: {color: Number(row[measure]) < 0 ? t.red : t[color]},
40
+ ...(Number(row[measure]) < 0 ? {label: {position: 'right', color: t.red}} : {})}))}]
41
+ }, {notMerge: true});
42
+ }
43
+ function scatter(chart, rows, {dimension, x, y, xLabel, yLabel}) {
44
+ const t = tokens(), a = axis();
45
+ chart.setOption({animation: false, aria: {enabled: true}, grid: {left: 54, right: 24, top: 28, bottom: 48},
46
+ tooltip: {trigger: 'item', confine: true, formatter: item => {
47
+ const row = rows[item.dataIndex];
48
+ return String(row[dimension]).replace(/[<>&"']/g, '') + '<br>' + xLabel + ': ' + amount(Number(row[x])) + '<br>' + yLabel + ': ' + amount(Number(row[y]));
49
+ }},
50
+ xAxis: {...a, name: xLabel, nameLocation: 'middle', nameGap: 30, type: 'value', axisLabel: {...a.axisLabel, formatter: compact}},
51
+ yAxis: {...a, name: yLabel, type: 'value', axisLabel: {...a.axisLabel, formatter: compact}},
52
+ series: [{id: 'scatter', name: dimension, type: 'scatter', symbolSize: 11,
53
+ data: rows.map(row => ({value: [Number(row[x]), Number(row[y])], name: row[dimension],
54
+ itemStyle: {color: Number(row[y]) < 0 ? t.red : t.teal, opacity: .8},
55
+ label: {show: Number(row[y]) < 0, formatter: String(row[dimension]), position: 'bottom', color: t.red, fontSize: 11}})),
56
+ markLine: {silent: true, symbol: 'none', label: {show: false}, lineStyle: {color: t.muted, type: 'dashed'}, data: [{yAxis: 0}]}}]
57
+ }, {notMerge: true});
58
+ }
59
+ return {line, horizontal, scatter};
60
+ })();
@@ -0,0 +1,11 @@
1
+ // Exact SVG output from Lucide React 1.24.0, ISC license in ICON_LICENSE.txt.
2
+ // Library icons, not hand-drawn approximations. No runtime React dependency.
3
+ const dashboardThemeIcons = {
4
+ "chevronDown": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-chevron-down\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m6 9 6 6 6-6\"></path></svg>",
5
+ "check": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-check\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M20 6 9 17l-5-5\"></path></svg>"
6
+ };
7
+ function dashboardThemeIcon(name) {
8
+ const template = document.createElement('template');
9
+ template.innerHTML = dashboardThemeIcons[name];
10
+ return template.content.firstElementChild.cloneNode(true);
11
+ }
@@ -0,0 +1,45 @@
1
+ <!-- Structural reference, not a runnable dashboard. Supply real verified bindings and renderers. -->
2
+ <!-- Inline theme.css in <style>; inline icons.js, select.js, charts.js before business runtime. -->
3
+ <main class="workspace">
4
+ <aside class="metric-rail">
5
+ <header>
6
+ <span class="demo"><!-- Verified source / explicit demo label --></span>
7
+ <h1><!-- Business question --></h1>
8
+ <p class="description"><!-- Scope and useful context --></p>
9
+ </header>
10
+ <section class="metrics" aria-label="当前筛选主要指标">
11
+ <!-- Repeat up to five .metric elements, using stable IDs and backend totals. -->
12
+ <div class="metric"><p><!-- Label and unit --></p><output id="metric-0">—</output><small><!-- Formula --></small></div>
13
+ </section>
14
+ </aside>
15
+ <div class="analysis">
16
+ <div class="filters" aria-label="分析筛选">
17
+ <!-- Repeat for actual verified controls; data-dashboard-control must match the saved contract. -->
18
+ <div class="filter-field">
19
+ <label for="filter-region">区域</label>
20
+ <select id="filter-region" data-dashboard-control="region" disabled></select>
21
+ </div>
22
+ </div>
23
+ <section class="hero">
24
+ <h2><!-- Main analysis heading --></h2><p class="note"><!-- Period and units --></p>
25
+ <h3 class="chart-heading"><!-- First measure/unit --></h3>
26
+ <div id="chart-primary" class="chart" role="img" aria-label="主趋势"></div>
27
+ <h3 class="chart-heading profit"><!-- Second measure/unit with independent scale --></h3>
28
+ <div id="chart-profit" class="chart chart-profit" role="img" aria-label="辅助趋势"></div>
29
+ </section>
30
+ <div class="lower-grid">
31
+ <section class="region-panel">
32
+ <h2><!-- Contribution heading --></h2><p class="note"><!-- Units and sort --></p>
33
+ <div id="chart-secondary" class="chart" role="img" aria-label="排名"></div>
34
+ </section>
35
+ <section class="details">
36
+ <h2><!-- Detail heading --></h2>
37
+ <div class="table-scroll" tabindex="0" role="region" aria-label="分析明细">
38
+ <table><thead><!-- Actual column headers --></thead><tbody id="details-rows"></tbody></table>
39
+ </div>
40
+ </section>
41
+ </div>
42
+ <p id="status" class="status" role="status">正在加载…</p>
43
+ </div>
44
+ <footer><span><!-- Actual source and period --></span><span id="freshness">尚无当前查询结果</span></footer>
45
+ </main>
@@ -0,0 +1,127 @@
1
+ // Accessible, select-only combobox. The native select remains the typed data-control contract.
2
+ // Read theme.css and icons.js; inline all three in the saved HTML (no relative asset URLs).
3
+ function enhanceDashboardSelect(select) {
4
+ const field = select.closest('.filter-field');
5
+ const label = field.querySelector('label').textContent;
6
+ const button = document.createElement('button');
7
+ button.type = 'button';
8
+ button.id = select.id + '-trigger';
9
+ button.className = 'filter-trigger';
10
+ button.setAttribute('role', 'combobox');
11
+ button.setAttribute('aria-haspopup', 'listbox');
12
+ button.setAttribute('aria-expanded', 'false');
13
+ const copy = document.createElement('span');
14
+ copy.className = 'filter-copy';
15
+ const caption = document.createElement('span');
16
+ caption.className = 'filter-caption';
17
+ caption.textContent = label;
18
+ const value = document.createElement('span');
19
+ value.className = 'filter-value';
20
+ copy.append(caption, value);
21
+ button.append(copy, dashboardThemeIcon('chevronDown'));
22
+ const menu = document.createElement('div');
23
+ menu.id = select.id + '-listbox';
24
+ menu.className = 'filter-menu';
25
+ menu.hidden = true;
26
+ menu.setAttribute('role', 'listbox');
27
+ menu.setAttribute('aria-label', label);
28
+ button.setAttribute('aria-controls', menu.id);
29
+ let active = 0, search = '', searchTimer;
30
+ const options = [...select.options].map((option, index) => {
31
+ const item = document.createElement('div');
32
+ item.id = select.id + '-option-' + index;
33
+ item.className = 'filter-option';
34
+ item.setAttribute('role', 'option');
35
+ const text = document.createElement('span');
36
+ text.textContent = option.textContent;
37
+ item.append(text, dashboardThemeIcon('check'));
38
+ item.addEventListener('pointerdown', event => event.preventDefault());
39
+ item.addEventListener('pointermove', () => highlight(index));
40
+ item.addEventListener('click', () => choose(index));
41
+ menu.append(item);
42
+ return item;
43
+ });
44
+ function sync() {
45
+ value.textContent = select.selectedOptions[0]?.textContent ?? '';
46
+ button.setAttribute('aria-label', label + ':' + value.textContent);
47
+ button.disabled = select.disabled;
48
+ options.forEach((item, index) => item.setAttribute('aria-selected', String(index === select.selectedIndex)));
49
+ if (select.disabled) close(false);
50
+ }
51
+ function position() {
52
+ if (menu.hidden) return;
53
+ const rect = button.getBoundingClientRect();
54
+ const width = Math.min(Math.max(rect.width, 190), innerWidth - 16);
55
+ menu.style.width = width + 'px';
56
+ menu.style.left = Math.min(Math.max(8, rect.left), innerWidth - width - 8) + 'px';
57
+ menu.style.maxHeight = Math.min(280, innerHeight - 16) + 'px';
58
+ const height = menu.offsetHeight;
59
+ const top = rect.bottom + 6 + height <= innerHeight - 8 ? rect.bottom + 6 : Math.max(8, rect.top - height - 6);
60
+ menu.style.top = top + 'px';
61
+ }
62
+ function highlight(index) {
63
+ active = Math.max(0, Math.min(index, options.length - 1));
64
+ options.forEach((item, i) => item.dataset.active = String(i === active));
65
+ button.setAttribute('aria-activedescendant', options[active].id);
66
+ options[active].scrollIntoView({block: 'nearest'});
67
+ }
68
+ function open(index = select.selectedIndex) {
69
+ if (button.disabled || !options.length) return;
70
+ document.dispatchEvent(new CustomEvent('dashboard-select-open', {detail: button.id}));
71
+ menu.hidden = false;
72
+ button.setAttribute('aria-expanded', 'true');
73
+ position();
74
+ highlight(index);
75
+ }
76
+ function close(restoreFocus = false) {
77
+ menu.hidden = true;
78
+ button.setAttribute('aria-expanded', 'false');
79
+ button.removeAttribute('aria-activedescendant');
80
+ if (restoreFocus && !button.disabled) button.focus();
81
+ }
82
+ function choose(index) {
83
+ if (select.disabled || select.options[index].disabled) return;
84
+ const changed = select.selectedIndex !== index;
85
+ select.selectedIndex = index;
86
+ sync();
87
+ close(true);
88
+ if (changed) select.dispatchEvent(new Event('change', {bubbles: true}));
89
+ }
90
+ button.addEventListener('click', () => menu.hidden ? open() : close());
91
+ button.addEventListener('keydown', event => {
92
+ const {key} = event;
93
+ if (key === 'Tab') { close(); return; }
94
+ if (key === 'Escape') { if (!menu.hidden) { event.preventDefault(); close(true); } return; }
95
+ if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key)) {
96
+ event.preventDefault();
97
+ if (menu.hidden) open(key === 'Home' ? 0 : key === 'End' ? options.length - 1 : select.selectedIndex);
98
+ else highlight(key === 'Home' ? 0 : key === 'End' ? options.length - 1 : active + (key === 'ArrowDown' ? 1 : -1));
99
+ return;
100
+ }
101
+ if (key === 'Enter' || key === ' ') {
102
+ event.preventDefault();
103
+ menu.hidden ? open() : choose(active);
104
+ return;
105
+ }
106
+ if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
107
+ search += key.toLocaleLowerCase();
108
+ clearTimeout(searchTimer);
109
+ searchTimer = setTimeout(() => { search = ''; }, 600);
110
+ const index = [...select.options].findIndex(option => option.textContent.toLocaleLowerCase().startsWith(search));
111
+ if (index >= 0) { event.preventDefault(); menu.hidden ? open(index) : highlight(index); }
112
+ }
113
+ });
114
+ select.addEventListener('change', sync);
115
+ document.addEventListener('pointerdown', event => { if (!field.contains(event.target) && !menu.contains(event.target)) close(); });
116
+ document.addEventListener('dashboard-select-open', event => { if (event.detail !== button.id) close(); });
117
+ document.addEventListener('focusin', event => { if (!field.contains(event.target) && !menu.contains(event.target)) close(); });
118
+ window.addEventListener('resize', position);
119
+ window.addEventListener('scroll', position, true);
120
+ new MutationObserver(sync).observe(select, {attributes: true, attributeFilter: ['disabled']});
121
+ field.append(button);
122
+ document.body.append(menu);
123
+ field.classList.add('is-enhanced');
124
+ select.hidden = true;
125
+ sync();
126
+ return {sync, close};
127
+ }
@@ -0,0 +1,137 @@
1
+ /* Trend Workspace v1: shared visual contract for Agent-authored business pages. */
2
+ :root {
3
+ color-scheme: light;
4
+ --paper: #ffffff;
5
+ --rail: #f1f6fc;
6
+ --control: #f7f9fc;
7
+ --ink: #182d44;
8
+ --muted: #566a7e;
9
+ --line: #dce4ed;
10
+ --blue: #2868b2;
11
+ --teal: #137c71;
12
+ --red: #b83c47;
13
+ --selected: #eaf2fc;
14
+ --focus: #2563eb;
15
+ --shadow: 0 8px 24px #182d441a;
16
+ --radius: 10px;
17
+ }
18
+ body.dark {
19
+ color-scheme: dark;
20
+ --paper: #142231;
21
+ --rail: #1b2e40;
22
+ --control: #203448;
23
+ --ink: #edf3f9;
24
+ --muted: #b3c3d4;
25
+ --line: #3e5368;
26
+ --blue: #8bb9f0;
27
+ --teal: #72cabc;
28
+ --red: #ff9da6;
29
+ --selected: #294662;
30
+ --focus: #9bc1ff;
31
+ --shadow: 0 8px 24px #0005;
32
+ }
33
+ * { box-sizing: border-box; }
34
+ body { margin: 0; background: var(--paper); color: var(--ink); font: 14px/1.5 system-ui, "PingFang SC", sans-serif; }
35
+ button, select { font: inherit; }
36
+ button { color: inherit; }
37
+ p, h1, h2 { margin: 0; }
38
+ h1 { font-size: 26px; font-weight: 650; line-height: 1.35; letter-spacing: -.5px; }
39
+ h2, caption { font-size: 18px; line-height: 1.4; font-weight: 650; letter-spacing: -.25px; }
40
+ .workspace { display: grid; grid-template-columns: minmax(240px, 22%) minmax(0, 1fr); min-height: 100vh; }
41
+ .metric-rail { min-width: 0; padding: 28px 26px; background: var(--rail); border-right: 1px solid var(--line); }
42
+ .demo { display: block; margin-bottom: 10px; font-size: 12px; color: var(--muted); }
43
+ .description { margin-top: 10px; color: var(--muted); }
44
+ .metrics { display: grid; grid-template-columns: 1fr 1fr; margin-top: 28px; }
45
+ .metric { grid-column: 1 / -1; min-width: 0; padding: 24px 0; border-top: 1px solid var(--line); }
46
+ .metric p { font-size: 14px; font-weight: 550; color: var(--muted); }
47
+ .metric output { display: block; margin: 5px 0; font-size: clamp(28px, 2.7vw, 40px); font-weight: 650; line-height: 1.2; letter-spacing: -1px; font-variant-numeric: tabular-nums; }
48
+ .metric small { display: block; font-size: 12px; line-height: 1.5; color: var(--muted); }
49
+ .metric:nth-last-child(-n+2) { grid-column: auto; padding-right: 12px; }
50
+ .metric:nth-last-child(-n+2) output { font-size: 28px; }
51
+ .metric:last-child { padding-left: 16px; padding-right: 0; }
52
+ .analysis { min-width: 0; }
53
+ .filters { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 12px; padding: 14px 28px 18px; }
54
+ .filter-field { min-width: 0; width: 150px; }
55
+ .filter-field > label { font-size: 12px; color: var(--muted); }
56
+ .filter-field > select { width: 100%; min-height: 44px; }
57
+ .filter-field.is-enhanced > label, .filter-field.is-enhanced > select { display: none; }
58
+ .filter-trigger { display: flex; align-items: center; gap: 12px; width: 100%; min-height: 58px; padding: 8px 13px; text-align: left; border: 1px solid var(--line); border-radius: var(--radius); background: var(--control); cursor: pointer; transition: border-color .14s, background .14s; }
59
+ .filter-copy { display: grid; gap: 3px; min-width: 0; flex: 1; }
60
+ .filter-caption { color: var(--muted); font-size: 12px; line-height: 1.3; }
61
+ .filter-value { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
62
+ .filter-trigger svg { flex: none; width: 16px; height: 16px; transition: transform .14s; }
63
+ .filter-trigger[aria-expanded="true"] { border-color: var(--focus); box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus) 12%, transparent); }
64
+ .filter-trigger[aria-expanded="true"] > svg { transform: rotate(180deg); }
65
+ .filter-trigger:hover:not(:disabled) { border-color: var(--blue); }
66
+ .filter-trigger:disabled { opacity: .6; cursor: wait; }
67
+ button:focus-visible, summary:focus-visible, .table-scroll:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
68
+ .filter-menu { position: fixed; z-index: 100; width: 200px; max-height: 280px; overflow-y: auto; padding: 6px; background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
69
+ .filter-menu[hidden] { display: none; }
70
+ .filter-option { display: flex; align-items: center; gap: 10px; min-height: 40px; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; }
71
+ .filter-option > span { flex: 1; }
72
+ .filter-option svg { width: 16px; height: 16px; opacity: 0; }
73
+ .filter-option[aria-selected="true"] { color: var(--blue); font-weight: 600; background: var(--selected); }
74
+ .filter-option[aria-selected="true"] svg { opacity: 1; }
75
+ .filter-option[data-active="true"] { outline: 1px solid var(--blue); outline-offset: -1px; }
76
+ .filter-option:first-child { margin-bottom: 5px; }
77
+ .hero { padding: 10px 28px 22px; }
78
+ .note { margin-top: 5px; color: var(--muted); font-size: 12px; }
79
+ .chart-heading { margin: 18px 0 2px; color: var(--blue); font-size: 14px; font-weight: 600; }
80
+ .chart-heading.profit { color: var(--teal); }
81
+ .chart { min-width: 0; height: 230px; }
82
+ .chart-profit { height: 180px; }
83
+ .lower-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); border-top: 1px solid var(--line); }
84
+ .region-panel, .details { padding: 20px 28px; min-width: 0; }
85
+ .lower-grid > .details { border-left: 1px solid var(--line); }
86
+ .region-panel .chart { height: 220px; margin-top: 12px; }
87
+ .plots { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 28px; padding: 12px 28px 24px; }
88
+ .panel { min-width: 0; }
89
+ .plots .chart { height: 330px; margin-top: 16px; }
90
+ body[data-page="profitability"] .plots .chart { height: 480px; }
91
+ .full-details { border-top: 1px solid var(--line); }
92
+ .table-scroll { overflow: auto; max-height: 380px; margin-top: 16px; }
93
+ table { width: 100%; border-collapse: collapse; font-size: 13px; white-space: nowrap; font-variant-numeric: tabular-nums; }
94
+ caption { text-align: left; margin-bottom: 14px; }
95
+ th, td { padding: 12px 10px; border-bottom: 1px solid var(--line); text-align: right; }
96
+ th { color: var(--muted); font-size: 12px; font-weight: 500; background: var(--control); position: sticky; top: 0; z-index: 1; }
97
+ th:first-child, td:first-child { text-align: left; }
98
+ td:first-child { font-weight: 500; }
99
+ tbody tr:hover { background: var(--control); }
100
+ .negative { color: var(--red); }
101
+ .table-bar { display: block; height: 3px; margin-top: 5px; background: var(--blue); border-radius: 2px; }
102
+ .status { padding: 0 28px; min-height: 22px; color: var(--muted); font-size: 12px; }
103
+ .status.error { color: var(--red); }
104
+ .methodology { padding: 6px 28px 12px; color: var(--muted); font-size: 12px; }
105
+ .methodology summary { cursor: pointer; width: fit-content; }
106
+ .methodology p { margin-top: 8px; max-width: 80ch; }
107
+ footer { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 16px; padding: 10px 24px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; }
108
+ @media (max-width: 1100px) {
109
+ .workspace { grid-template-columns: 218px minmax(0, 1fr); }
110
+ .metric-rail { padding: 24px 20px; }
111
+ .metric output { font-size: 29px; }
112
+ .lower-grid { grid-template-columns: 1fr; }
113
+ .lower-grid > .details { border-left: 0; border-top: 1px solid var(--line); }
114
+ .plots { grid-template-columns: 1fr; }
115
+ }
116
+ @media (max-width: 680px) {
117
+ .workspace { display: block; }
118
+ .metric-rail { padding: 20px 16px; border-right: 0; border-bottom: 1px solid var(--line); }
119
+ h1 { font-size: 24px; }
120
+ .metrics { grid-template-columns: 1fr 1fr; margin-top: 18px; gap: 0 18px; }
121
+ .metric { grid-column: auto; padding: 16px 0; }
122
+ .metric:first-child { grid-column: 1 / -1; }
123
+ .metric:first-child output { font-size: 36px; }
124
+ .metric output, .metric:nth-last-child(-n+2) output { font-size: 26px; }
125
+ .metric:last-child { padding-left: 0; }
126
+ .filters { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; padding: 16px; }
127
+ .filter-field { width: auto; }
128
+ .filter-trigger { padding: 8px; gap: 4px; min-height: 58px; }
129
+ .filter-value { font-size: 13px; }
130
+ .hero, .region-panel, .details { padding: 16px; }
131
+ .plots { padding: 16px; gap: 24px; }
132
+ .status, .methodology { padding-left: 16px; padding-right: 16px; }
133
+ footer { flex-direction: column; gap: 4px; padding: 12px 16px; }
134
+ }
135
+ @media (prefers-reduced-motion: reduce) {
136
+ *, *::before, *::after { transition-duration: 0s !important; scroll-behavior: auto !important; }
137
+ }