@xpert-ai/plugin-live-dashboard 0.3.0 → 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.
- package/.xpertai-plugin/plugin.json +10 -2
- package/README.md +70 -0
- package/assets/branding/cover-v1.prompt.txt +27 -0
- package/assets/branding/cover-v1.webp +0 -0
- package/assets/previews/customer-brief.png +0 -0
- package/assets/previews/profit-atlas.png +0 -0
- package/assets/previews/trend-workspace.png +0 -0
- package/dist/index.js +14 -1
- package/dist/lib/agent.schema.d.ts +43 -37
- package/dist/lib/agent.schema.js +2 -1
- package/dist/lib/app-config.d.ts +62 -0
- package/dist/lib/app-config.js +38 -0
- package/dist/lib/artifact-type.provider.d.ts +15 -0
- package/dist/lib/artifact-type.provider.js +78 -0
- package/dist/lib/assistant-template.d.ts +2 -0
- package/dist/lib/assistant-template.js +7 -0
- package/dist/lib/content.provider.js +13 -5
- package/dist/lib/dashboard-navigation.d.ts +58 -14
- package/dist/lib/dashboard-navigation.js +11 -10
- package/dist/lib/dashboard-view.schema.d.ts +197 -46
- package/dist/lib/dashboard-view.schema.js +8 -1
- package/dist/lib/dashboard.service.d.ts +39 -2
- package/dist/lib/dashboard.service.js +52 -28
- package/dist/lib/document-libraries.d.ts +1 -1
- package/dist/lib/document-libraries.js +3 -2
- package/dist/lib/document-network.d.ts +1 -1
- package/dist/lib/document-network.js +2 -6
- package/dist/lib/editing-feedback.d.ts +5 -0
- package/dist/lib/editing-feedback.js +19 -0
- package/dist/lib/editing-patch.d.ts +22 -0
- package/dist/lib/editing-patch.js +46 -0
- package/dist/lib/editing-session.d.ts +167 -0
- package/dist/lib/editing-session.js +53 -0
- package/dist/lib/editing.d.ts +92 -60
- package/dist/lib/editing.js +36 -24
- package/dist/lib/html-guidance.d.ts +1 -1
- package/dist/lib/html-guidance.js +1 -1
- package/dist/lib/middleware.js +48 -20
- package/dist/lib/monitor.d.ts +70 -0
- package/dist/lib/monitor.js +101 -0
- package/dist/lib/plugin.js +5 -1
- package/dist/lib/project.d.ts +106 -0
- package/dist/lib/project.js +17 -0
- package/dist/lib/projects.schema.d.ts +49 -0
- package/dist/lib/projects.schema.js +7 -0
- package/dist/lib/projects.service.d.ts +23 -0
- package/dist/lib/projects.service.js +65 -0
- package/dist/lib/prompt-workflows.d.ts +3 -2
- package/dist/lib/prompt-workflows.js +17 -3
- package/dist/lib/provider-options.controller.d.ts +10 -0
- package/dist/lib/provider-options.controller.js +49 -0
- package/dist/lib/resource-card.js +1 -1
- package/dist/lib/runtime-context.d.ts +3 -0
- package/dist/lib/runtime-context.js +13 -0
- package/dist/lib/save-evidence.d.ts +1 -1
- package/dist/lib/templates.d.ts +2 -0
- package/dist/lib/templates.js +55 -0
- package/dist/lib/view.provider.d.ts +97 -7
- package/dist/lib/view.provider.js +39 -10
- package/dist/libraries/TREND-WORKSPACE-ICONS-LICENSE.txt +43 -0
- package/dist/libraries/catalog.json +1 -1
- package/dist/remote/app.css +1 -1
- package/dist/remote/app.js +33 -33
- package/dist/remote/build-manifest.json +118 -32
- package/dist/sandbox-actions/action.json +1 -1
- package/dist/sandbox-actions/bundle/host.js +2 -2
- package/dist/sandbox-actions/bundle/resource-cache.mjs +2 -1
- package/dist/sandbox-actions/bundle/validate.mjs +1 -1
- package/docs/docs.json +7 -0
- package/docs/frontend-libraries.mdx +12 -4
- package/docs/html-mode.md +3 -3
- package/docs/quick-start-acceptance.mdx +50 -0
- package/docs/quick-start.mdx +64 -0
- package/docs/superstore.mdx +38 -0
- package/docs/team-sharing.mdx +6 -2
- package/docs/trend-workspace-acceptance.mdx +43 -0
- package/docs/trend-workspace.mdx +30 -0
- package/docs/verification.mdx +4 -2
- package/docs/visual-styles-acceptance.mdx +40 -0
- package/docs/visual-styles.mdx +46 -0
- package/package.json +25 -18
- package/scripts/import-prompts.mjs +108 -0
- package/skills/live-dashboard/SKILL.md +27 -3
- package/skills/live-dashboard/examples/README.md +25 -0
- package/skills/live-dashboard/examples/catalog.json +79 -0
- package/skills/live-dashboard/examples/production-quality/README.md +25 -0
- package/skills/live-dashboard/examples/production-quality/dashboard.html +129 -0
- package/skills/live-dashboard/examples/production-quality/definition.json +67 -0
- package/skills/live-dashboard/examples/production-quality/demo.csv +7 -0
- package/skills/live-dashboard/examples/production-quality/demo.json +44 -0
- package/skills/live-dashboard/examples/production-quality/expected.json +26 -0
- package/skills/live-dashboard/examples/production-quality/preview.png +0 -0
- package/skills/live-dashboard/examples/production-quality/query.json +53 -0
- package/skills/live-dashboard/examples/regional-sales/README.md +25 -0
- package/skills/live-dashboard/examples/regional-sales/dashboard-create.json +35 -0
- package/skills/live-dashboard/examples/regional-sales/definition.json +35 -0
- package/skills/live-dashboard/examples/regional-sales/demo.csv +5 -0
- package/skills/live-dashboard/examples/regional-sales/demo.json +18 -0
- package/skills/live-dashboard/examples/regional-sales/expected.json +19 -0
- package/skills/live-dashboard/examples/regional-sales/preview.png +0 -0
- package/skills/live-dashboard/examples/regional-sales/query.json +45 -0
- package/skills/live-dashboard/examples/sales-overview/README.md +25 -0
- package/skills/live-dashboard/examples/sales-overview/dashboard.html +129 -0
- package/skills/live-dashboard/examples/sales-overview/definition.json +57 -0
- package/skills/live-dashboard/examples/sales-overview/demo.csv +7 -0
- package/skills/live-dashboard/examples/sales-overview/demo.json +38 -0
- package/skills/live-dashboard/examples/sales-overview/expected.json +22 -0
- package/skills/live-dashboard/examples/sales-overview/preview.png +0 -0
- package/skills/live-dashboard/examples/sales-overview/query.json +52 -0
- package/skills/live-dashboard/examples/superstore/README.md +64 -0
- package/skills/live-dashboard/examples/superstore/THIRD_PARTY_LICENSE.txt +207 -0
- package/skills/live-dashboard/examples/superstore/build_data.py +155 -0
- package/skills/live-dashboard/examples/superstore/build_pages.py +129 -0
- package/skills/live-dashboard/examples/superstore/checks.json +104 -0
- package/skills/live-dashboard/examples/superstore/customers-mobile-dark.png +0 -0
- package/skills/live-dashboard/examples/superstore/customers-select.png +0 -0
- package/skills/live-dashboard/examples/superstore/customers.html +316 -0
- package/skills/live-dashboard/examples/superstore/customers.png +0 -0
- package/skills/live-dashboard/examples/superstore/customers.queries.json +512 -0
- package/skills/live-dashboard/examples/superstore/expected.json +4069 -0
- package/skills/live-dashboard/examples/superstore/import.json +200 -0
- package/skills/live-dashboard/examples/superstore/orders.csv +10195 -0
- package/skills/live-dashboard/examples/superstore/overview-mobile-dark.png +0 -0
- package/skills/live-dashboard/examples/superstore/overview-select.png +0 -0
- package/skills/live-dashboard/examples/superstore/overview.html +310 -0
- package/skills/live-dashboard/examples/superstore/overview.png +0 -0
- package/skills/live-dashboard/examples/superstore/overview.queries.json +636 -0
- package/skills/live-dashboard/examples/superstore/page.js +109 -0
- package/skills/live-dashboard/examples/superstore/profitability-mobile-dark.png +0 -0
- package/skills/live-dashboard/examples/superstore/profitability-select.png +0 -0
- package/skills/live-dashboard/examples/superstore/profitability.html +307 -0
- package/skills/live-dashboard/examples/superstore/profitability.png +0 -0
- package/skills/live-dashboard/examples/superstore/profitability.queries.json +334 -0
- package/skills/live-dashboard/examples/superstore/provenance.json +24 -0
- package/skills/live-dashboard/examples/superstore/semantic-model.json +220 -0
- package/skills/live-dashboard/examples/superstore/test_data.py +62 -0
- package/skills/live-dashboard/references/html-runtime.md +1 -1
- package/skills/live-dashboard/themes/README.md +50 -0
- package/skills/live-dashboard/themes/customer-brief/README.md +47 -0
- package/skills/live-dashboard/themes/customer-brief/charts.js +17 -0
- package/skills/live-dashboard/themes/customer-brief/layout.html +22 -0
- package/skills/live-dashboard/themes/customer-brief/theme.css +108 -0
- package/skills/live-dashboard/themes/profit-atlas/README.md +43 -0
- package/skills/live-dashboard/themes/profit-atlas/charts.js +30 -0
- package/skills/live-dashboard/themes/profit-atlas/layout.html +19 -0
- package/skills/live-dashboard/themes/profit-atlas/theme.css +75 -0
- package/skills/live-dashboard/themes/trend-workspace/ICON_LICENSE.txt +43 -0
- package/skills/live-dashboard/themes/trend-workspace/README.md +144 -0
- package/skills/live-dashboard/themes/trend-workspace/charts.js +60 -0
- package/skills/live-dashboard/themes/trend-workspace/icons.js +11 -0
- package/skills/live-dashboard/themes/trend-workspace/layout.html +45 -0
- package/skills/live-dashboard/themes/trend-workspace/select.js +127 -0
- 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
|
+
}
|