feat: redesign market propagation toolbar

This commit is contained in:
wxs
2026-07-16 18:21:12 +08:00
parent ca365b485a
commit 2e248fa9a3
2 changed files with 1098 additions and 123 deletions
+621 -63
View File
@@ -16,17 +16,35 @@ export interface PluginToolbarHandlers {
}
interface SpreadMetricRuleDom {
detailsButton: HTMLButtonElement;
enabledInput: HTMLInputElement;
flowTypeSelect: HTMLSelectElement;
onlyAssignSelect: HTMLSelectElement;
rangeSelect: HTMLSelectElement;
removeButton: HTMLButtonElement;
root: HTMLElement;
secondaryControls: HTMLElement;
thresholdInput: HTMLInputElement;
typeSelect: HTMLSelectElement;
}
type SpreadMetricRuleDomMap = Record<SpreadFilterMetric, SpreadMetricRuleDom>;
interface MetricCatalogDom {
addButton: HTMLButtonElement;
closeButton: HTMLButtonElement;
metricActions: Record<SpreadFilterMetric, HTMLButtonElement>;
panel: HTMLElement;
root: HTMLElement;
searchInput: HTMLInputElement;
selectedCount: HTMLElement;
}
interface ToolbarResizeListener {
handler: EventListener;
window: Window;
}
const SPREAD_FILTER_DEFINITIONS: ReadonlyArray<{
label: string;
metric: SpreadFilterMetric;
@@ -35,6 +53,14 @@ const SPREAD_FILTER_DEFINITIONS: ReadonlyArray<{
{ label: "互动率", metric: "interactionRate" }
];
const DEFAULT_ENABLED_SPREAD_METRIC: SpreadFilterMetric = "finishRate";
const DEFAULT_SPREAD_METRIC_CONFIG: Readonly<SpreadInfoConfig> = {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
};
export interface PluginToolbarDom {
audienceProfileByIdExportButton: HTMLButtonElement;
audienceProfileExportButton: HTMLButtonElement;
@@ -44,18 +70,30 @@ export interface PluginToolbarDom {
exportCustomPagesInput: HTMLInputElement;
exportRangeSelect: HTMLSelectElement;
exportStatusText: HTMLElement;
metricCatalog: MetricCatalogDom;
spreadMetricRules: SpreadMetricRuleDomMap;
root: HTMLElement;
}
const PLUGIN_ACTION_BUTTON_STYLE_ID = "sces-plugin-action-button-style";
const TOOLBAR_RESIZE_LISTENER = Symbol("sces-toolbar-resize-listener");
type ToolbarRootWithResizeListener = HTMLElement & {
[TOOLBAR_RESIZE_LISTENER]?: ToolbarResizeListener;
};
export function isPluginToolbarMounted(
root: HTMLElement,
document: Document
): boolean {
const actionRow = findNativeActionRow(document);
return Boolean(actionRow && root.parentElement === actionRow && !root.hidden);
const isMounted = Boolean(
actionRow && root.parentElement === actionRow && !root.hidden
);
if (!isMounted) {
cleanupToolbarResizeListener(root);
}
return isMounted;
}
export function ensurePluginToolbar(
@@ -72,12 +110,18 @@ export function ensurePluginToolbar(
existingRoot.querySelector(
'[data-plugin-export-audience-profile-by-id="button"]'
) &&
existingRoot.querySelector('[data-plugin-spread-metric="finishRate"]')
existingRoot.querySelector(
'[data-plugin-toolbar-action-group="csv-export"]'
) &&
existingRoot.querySelector('[data-plugin-spread-metric-catalog-trigger="button"]')
) {
ensureToolbarMounted(existingRoot, document);
return readToolbarDom(existingRoot);
const toolbarDom = readToolbarDom(existingRoot);
ensureToolbarResizeListener(toolbarDom);
return toolbarDom;
}
cleanupToolbarResizeListener(existingRoot);
existingRoot.remove();
}
@@ -140,6 +184,11 @@ export function ensurePluginToolbar(
applyStatusStyles(exportStatusText);
const spreadMetricRules = createSpreadMetricRuleDoms(document);
const defaultSpreadMetricRule =
spreadMetricRules[DEFAULT_ENABLED_SPREAD_METRIC];
defaultSpreadMetricRule.enabledInput.checked = true;
applyDefaultSpreadMetricRuleState(defaultSpreadMetricRule);
const metricCatalog = createMetricCatalog(document);
const panel = document.createElement("div");
panel.dataset.pluginToolbarPanel = "root";
@@ -156,18 +205,33 @@ export function ensurePluginToolbar(
const dataGroup = document.createElement("div");
dataGroup.dataset.pluginToolbarGroup = "data";
applyToolbarGroupStyles(dataGroup);
dataGroup.append(
const csvExportGroup = createToolbarActionGroup(document, "csv-export", [
exportButton,
exportRangeSelect,
exportCustomPagesInput,
audienceProfileExportButton,
audienceProfileByIdExportButton,
audienceProfileFieldButton,
exportCustomPagesInput
]);
const selectedAudienceExportGroup = createToolbarActionGroup(
document,
"selected-audience-export",
[audienceProfileExportButton, audienceProfileFieldButton]
);
const idExportGroup = createToolbarActionGroup(document, "id-export", [
audienceProfileByIdExportButton
]);
const batchSubmitGroup = createToolbarActionGroup(document, "batch-submit", [
batchSubmitButton
]);
dataGroup.append(
csvExportGroup,
createToolbarActionDivider(document),
selectedAudienceExportGroup,
createToolbarActionDivider(document),
idExportGroup,
createToolbarActionDivider(document),
batchSubmitGroup
);
const thresholdTitle = createToolbarGroupTitle(document, "传播指标筛选");
const metricSelector = createSpreadMetricSelector(document, spreadMetricRules);
const rulesGroup = document.createElement("div");
rulesGroup.dataset.pluginSpreadRules = "root";
applySpreadRulesGroupStyles(rulesGroup);
@@ -179,7 +243,7 @@ export function ensurePluginToolbar(
const filterNote = createSpreadFilterNote(document);
firstRow.append(dataGroup, exportStatusText);
secondRow.append(thresholdTitle, metricSelector, rulesGroup, filterNote);
secondRow.append(thresholdTitle, metricCatalog.root, rulesGroup, filterNote);
panel.append(firstRow, secondRow);
root.append(panel);
@@ -222,6 +286,7 @@ export function ensurePluginToolbar(
exportCustomPagesInput,
exportRangeSelect,
exportStatusText,
metricCatalog,
spreadMetricRules,
root
} satisfies PluginToolbarDom;
@@ -230,6 +295,16 @@ export function ensurePluginToolbar(
syncCustomPagesInputVisibility(toolbarDom);
});
metricCatalog.addButton.addEventListener("click", () => {
setMetricCatalogOpen(metricCatalog, metricCatalog.panel.hidden);
});
metricCatalog.closeButton.addEventListener("click", () => {
setMetricCatalogOpen(metricCatalog, false);
});
metricCatalog.searchInput.addEventListener("input", () => {
syncMetricCatalog(toolbarDom);
});
SPREAD_FILTER_DEFINITIONS.forEach(({ metric }) => {
const rule = spreadMetricRules[metric];
rule.enabledInput.addEventListener("change", () => {
@@ -239,8 +314,27 @@ export function ensurePluginToolbar(
rule.typeSelect.addEventListener("change", () => {
syncSpreadMetricVideoConstraints(rule);
});
rule.removeButton.addEventListener("click", () => {
rule.enabledInput.checked = false;
syncAllSpreadMetricRules(toolbarDom);
});
rule.detailsButton.addEventListener("click", () => {
rule.root.dataset.pluginSpreadRuleDetails =
rule.root.dataset.pluginSpreadRuleDetails === "open" ? "closed" : "open";
syncSpreadMetricRuleSecondaryControls(rule);
});
metricCatalog.metricActions[metric].addEventListener("click", () => {
if (rule.enabledInput.checked) {
return;
}
rule.enabledInput.checked = true;
syncAllSpreadMetricRules(toolbarDom);
setMetricCatalogOpen(metricCatalog, false);
});
});
ensureToolbarResizeListener(toolbarDom);
syncCustomPagesInputVisibility(toolbarDom);
syncAllSpreadMetricRules(toolbarDom);
@@ -258,6 +352,30 @@ function appendOption(
select.appendChild(option);
}
function createToolbarActionGroup(
document: Document,
groupName:
| "batch-submit"
| "csv-export"
| "id-export"
| "selected-audience-export",
controls: HTMLElement[]
): HTMLElement {
const group = document.createElement("div");
group.dataset.pluginToolbarActionGroup = groupName;
applyToolbarActionGroupStyles(group);
group.append(...controls);
return group;
}
function createToolbarActionDivider(document: Document): HTMLElement {
const divider = document.createElement("span");
divider.dataset.pluginToolbarActionDivider = "divider";
divider.setAttribute("aria-hidden", "true");
applyToolbarActionDividerStyles(divider);
return divider;
}
function createSpreadMetricRuleDoms(
document: Document
): SpreadMetricRuleDomMap {
@@ -276,6 +394,7 @@ function createSpreadMetricRuleDom(
): SpreadMetricRuleDom {
const enabledInput = document.createElement("input");
enabledInput.type = "checkbox";
enabledInput.hidden = true;
enabledInput.dataset.pluginSpreadMetric = metric;
enabledInput.setAttribute("aria-label", `启用${label}筛选`);
@@ -332,44 +451,131 @@ function createSpreadMetricRuleDom(
unitText.textContent = "%";
thresholdControl.append(operator, thresholdInput, unitText);
root.append(
metricLabel,
thresholdControl,
typeSelect,
const primaryControls = document.createElement("div");
primaryControls.dataset.pluginSpreadRulePrimary = metric;
applySpreadMetricRulePrimaryStyles(primaryControls);
const detailsButton = document.createElement("button");
detailsButton.type = "button";
detailsButton.dataset.pluginSpreadRuleDetails = metric;
detailsButton.textContent = "更多选项";
detailsButton.setAttribute("aria-expanded", "false");
applySpreadMetricDetailsButtonStyles(detailsButton);
primaryControls.append(metricLabel, thresholdControl, typeSelect, detailsButton);
const secondaryControls = document.createElement("div");
secondaryControls.dataset.pluginSpreadRuleSecondary = metric;
applySpreadMetricRuleSecondaryStyles(secondaryControls);
const removeButton = document.createElement("button");
removeButton.type = "button";
removeButton.dataset.pluginSpreadRuleRemove = metric;
removeButton.textContent = "×";
removeButton.title = `删除${label}筛选`;
removeButton.setAttribute("aria-label", `删除${label}筛选`);
applySpreadMetricRemoveButtonStyles(removeButton);
secondaryControls.append(
onlyAssignSelect,
flowTypeSelect,
rangeSelect
rangeSelect,
removeButton
);
root.append(enabledInput, primaryControls, secondaryControls);
return {
detailsButton,
enabledInput,
flowTypeSelect,
onlyAssignSelect,
rangeSelect,
removeButton,
root,
secondaryControls,
thresholdInput,
typeSelect
};
}
function createSpreadMetricSelector(
document: Document,
rules: SpreadMetricRuleDomMap
): HTMLElement {
const selector = document.createElement("div");
selector.dataset.pluginSpreadMetrics = "root";
applySpreadMetricSelectorStyles(selector);
function createMetricCatalog(document: Document): MetricCatalogDom {
const root = document.createElement("div");
root.dataset.pluginSpreadMetricCatalog = "root";
applyMetricCatalogStyles(root);
SPREAD_FILTER_DEFINITIONS.forEach(({ label, metric }) => {
const option = document.createElement("label");
applySpreadMetricOptionStyles(option);
const labelText = document.createElement("span");
labelText.textContent = label;
option.append(rules[metric].enabledInput, labelText);
selector.appendChild(option);
});
const addButton = document.createElement("button");
addButton.type = "button";
addButton.dataset.pluginSpreadMetricCatalogTrigger = "button";
addButton.textContent = "添加筛选指标";
addButton.setAttribute("aria-expanded", "false");
applyMetricCatalogTriggerStyles(addButton);
return selector;
const selectedCount = document.createElement("span");
selectedCount.dataset.pluginSpreadMetricSelectedCount = "text";
selectedCount.setAttribute("aria-live", "polite");
applyMetricCatalogCountStyles(selectedCount);
const panel = document.createElement("div");
panel.dataset.pluginSpreadMetricCatalogPanel = "root";
panel.hidden = true;
applyMetricCatalogPanelStyles(panel);
const searchInput = document.createElement("input");
searchInput.type = "search";
searchInput.placeholder = "搜索指标";
searchInput.dataset.pluginSpreadMetricCatalogSearch = "input";
searchInput.setAttribute("aria-label", "搜索筛选指标");
applyMetricCatalogSearchStyles(searchInput);
const closeButton = document.createElement("button");
closeButton.type = "button";
closeButton.dataset.pluginSpreadMetricCatalogClose = "button";
closeButton.textContent = "×";
closeButton.title = "关闭指标目录";
closeButton.setAttribute("aria-label", "关闭指标目录");
applyMetricCatalogCloseButtonStyles(closeButton);
const panelHeader = document.createElement("div");
panelHeader.dataset.pluginSpreadMetricCatalogHeader = "root";
applyMetricCatalogHeaderStyles(panelHeader);
panelHeader.append(searchInput, closeButton);
const group = document.createElement("div");
group.dataset.pluginSpreadMetricCatalogGroup = "传播表现";
applyMetricCatalogGroupStyles(group);
const groupTitle = document.createElement("strong");
groupTitle.textContent = "传播表现";
applyMetricCatalogGroupTitleStyles(groupTitle);
group.appendChild(groupTitle);
const metricActions = Object.fromEntries(
SPREAD_FILTER_DEFINITIONS.map(({ label, metric }) => {
const item = document.createElement("div");
item.dataset.pluginSpreadMetricCatalogItem = metric;
applyMetricCatalogItemStyles(item);
const name = document.createElement("span");
name.textContent = label;
const action = document.createElement("button");
action.type = "button";
action.dataset.pluginSpreadMetricCatalogAction = metric;
applyMetricCatalogActionStyles(action);
item.append(name, action);
group.appendChild(item);
return [metric, action];
})
) as unknown as Record<SpreadFilterMetric, HTMLButtonElement>;
panel.append(panelHeader, group);
root.append(addButton, selectedCount, panel);
return {
addButton,
closeButton,
metricActions,
panel,
root,
searchInput,
selectedCount
};
}
function createSpreadFilterNote(document: Document): HTMLElement {
@@ -390,6 +596,9 @@ function readSpreadMetricRuleDoms(root: HTMLElement): SpreadMetricRuleDomMap {
return [
metric,
{
detailsButton: ruleRoot.querySelector(
`[data-plugin-spread-rule-details="${metric}"]`
) as HTMLButtonElement,
enabledInput: root.querySelector(
`[data-plugin-spread-metric="${metric}"]`
) as HTMLInputElement,
@@ -402,7 +611,13 @@ function readSpreadMetricRuleDoms(root: HTMLElement): SpreadMetricRuleDomMap {
rangeSelect: ruleRoot.querySelector(
'[data-plugin-spread-filter="range"]'
) as HTMLSelectElement,
removeButton: ruleRoot.querySelector(
`[data-plugin-spread-rule-remove="${metric}"]`
) as HTMLButtonElement,
root: ruleRoot,
secondaryControls: ruleRoot.querySelector(
`[data-plugin-spread-rule-secondary="${metric}"]`
) as HTMLElement,
thresholdInput: ruleRoot.querySelector(
`[data-plugin-spread-threshold="${metric}"]`
) as HTMLInputElement,
@@ -415,6 +630,37 @@ function readSpreadMetricRuleDoms(root: HTMLElement): SpreadMetricRuleDomMap {
) as unknown as SpreadMetricRuleDomMap;
}
function readMetricCatalogDom(root: HTMLElement): MetricCatalogDom {
return {
addButton: root.querySelector(
'[data-plugin-spread-metric-catalog-trigger="button"]'
) as HTMLButtonElement,
closeButton: root.querySelector(
'[data-plugin-spread-metric-catalog-close="button"]'
) as HTMLButtonElement,
metricActions: Object.fromEntries(
SPREAD_FILTER_DEFINITIONS.map(({ metric }) => [
metric,
root.querySelector(
`[data-plugin-spread-metric-catalog-action="${metric}"]`
) as HTMLButtonElement
])
) as unknown as Record<SpreadFilterMetric, HTMLButtonElement>,
panel: root.querySelector(
'[data-plugin-spread-metric-catalog-panel="root"]'
) as HTMLElement,
root: root.querySelector(
'[data-plugin-spread-metric-catalog="root"]'
) as HTMLElement,
searchInput: root.querySelector(
'[data-plugin-spread-metric-catalog-search="input"]'
) as HTMLInputElement,
selectedCount: root.querySelector(
'[data-plugin-spread-metric-selected-count="text"]'
) as HTMLElement
};
}
function readToolbarDom(root: HTMLElement): PluginToolbarDom {
const toolbarDom = {
audienceProfileByIdExportButton: root.querySelector(
@@ -441,6 +687,7 @@ function readToolbarDom(root: HTMLElement): PluginToolbarDom {
exportStatusText: root.querySelector(
'[data-plugin-export-status="text"]'
) as HTMLElement,
metricCatalog: readMetricCatalogDom(root),
spreadMetricRules: readSpreadMetricRuleDoms(root),
root
} satisfies PluginToolbarDom;
@@ -551,13 +798,19 @@ export function setToolbarBusyState(
toolbar.exportButton,
toolbar.exportRangeSelect,
toolbar.exportCustomPagesInput,
toolbar.metricCatalog.addButton,
toolbar.metricCatalog.closeButton,
toolbar.metricCatalog.searchInput,
...Object.values(toolbar.metricCatalog.metricActions),
...Object.values(toolbar.spreadMetricRules).flatMap((rule) => [
rule.detailsButton,
rule.enabledInput,
rule.thresholdInput,
rule.typeSelect,
rule.onlyAssignSelect,
rule.flowTypeSelect,
rule.rangeSelect
rule.rangeSelect,
rule.removeButton
])
].forEach((element) => {
element.disabled = isBusy;
@@ -579,22 +832,67 @@ function syncCustomPagesInputVisibility(toolbar: PluginToolbarDom): void {
toolbar.exportCustomPagesInput.hidden = toolbar.exportRangeSelect.value !== "custom";
}
function ensureToolbarResizeListener(toolbar: PluginToolbarDom): void {
const root = toolbar.root as ToolbarRootWithResizeListener;
const currentWindow = root.ownerDocument.defaultView;
const existingListener = root[TOOLBAR_RESIZE_LISTENER];
if (!currentWindow) {
cleanupToolbarResizeListener(root);
return;
}
if (existingListener?.window === currentWindow) {
return;
}
if (existingListener) {
existingListener.window.removeEventListener(
"resize",
existingListener.handler
);
delete root[TOOLBAR_RESIZE_LISTENER];
}
const handler = existingListener?.handler ?? (() => {
syncAllSpreadMetricRules(toolbar);
});
root[TOOLBAR_RESIZE_LISTENER] = { handler, window: currentWindow };
currentWindow.addEventListener("resize", handler);
}
function cleanupToolbarResizeListener(root: HTMLElement): void {
const toolbarRoot = root as ToolbarRootWithResizeListener;
const listener = toolbarRoot[TOOLBAR_RESIZE_LISTENER];
if (!listener) {
return;
}
listener.window.removeEventListener("resize", listener.handler);
delete toolbarRoot[TOOLBAR_RESIZE_LISTENER];
}
function syncAllSpreadMetricRules(toolbar: PluginToolbarDom): void {
Object.values(toolbar.spreadMetricRules).forEach(syncSpreadMetricRuleState);
syncMetricCatalog(toolbar);
syncSpreadFilterNote(toolbar);
}
function syncSpreadMetricRuleState(rule: SpreadMetricRuleDom): void {
rule.root.hidden = !rule.enabledInput.checked;
if (!rule.enabledInput.checked) {
rule.thresholdInput.value = "";
rule.typeSelect.value = "1";
rule.onlyAssignSelect.value = "false";
rule.flowTypeSelect.value = "0";
rule.rangeSelect.value = "2";
applyDefaultSpreadMetricRuleState(rule);
clearSpreadMetricRuleValidation(rule);
}
syncSpreadMetricVideoConstraints(rule);
syncSpreadMetricRuleSecondaryControls(rule);
}
function applyDefaultSpreadMetricRuleState(rule: SpreadMetricRuleDom): void {
rule.thresholdInput.value = "";
rule.typeSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.type);
rule.onlyAssignSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.onlyAssign);
rule.flowTypeSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.flowType);
rule.rangeSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.range);
}
function syncSpreadMetricVideoConstraints(rule: SpreadMetricRuleDom): void {
@@ -621,6 +919,52 @@ function syncSpreadFilterNote(toolbar: PluginToolbarDom): void {
note.hidden = enabledCount < 2;
}
function syncMetricCatalog(toolbar: PluginToolbarDom): void {
const searchTerm = toolbar.metricCatalog.searchInput.value.trim().toLowerCase();
const enabledCount = Object.values(toolbar.spreadMetricRules).filter(
(rule) => rule.enabledInput.checked
).length;
toolbar.metricCatalog.selectedCount.textContent = `已选 ${enabledCount}`;
SPREAD_FILTER_DEFINITIONS.forEach(({ label, metric }) => {
const action = toolbar.metricCatalog.metricActions[metric];
const isSelected = toolbar.spreadMetricRules[metric].enabledInput.checked;
const item = action.closest(
`[data-plugin-spread-metric-catalog-item="${metric}"]`
) as HTMLElement | null;
action.disabled = isSelected;
action.textContent = isSelected ? "已添加" : "添加";
if (item) {
item.hidden = Boolean(searchTerm) && !label.toLowerCase().includes(searchTerm);
}
});
}
function setMetricCatalogOpen(catalog: MetricCatalogDom, isOpen: boolean): void {
catalog.panel.hidden = !isOpen;
catalog.addButton.setAttribute("aria-expanded", String(isOpen));
if (!isOpen) {
catalog.searchInput.value = "";
Object.values(catalog.metricActions).forEach((action) => {
const item = action.closest("[data-plugin-spread-metric-catalog-item]") as
| HTMLElement
| null;
if (item) {
item.hidden = false;
}
});
}
}
function syncSpreadMetricRuleSecondaryControls(rule: SpreadMetricRuleDom): void {
const isNarrowViewport =
(rule.root.ownerDocument.defaultView?.innerWidth ?? Number.POSITIVE_INFINITY) <= 720;
const isOpen = rule.root.dataset.pluginSpreadRuleDetails === "open";
rule.secondaryControls.hidden = isNarrowViewport && !isOpen;
rule.detailsButton.hidden = !isNarrowViewport;
rule.detailsButton.setAttribute("aria-expanded", String(isOpen));
}
function readSpreadMetricConfig(rule: SpreadMetricRuleDom): SpreadInfoConfig {
const type = rule.typeSelect.value === "2" ? 2 : 1;
return {
@@ -850,23 +1194,115 @@ function applyToolbarGroupStyles(group: HTMLElement): void {
group.style.flexWrap = "nowrap";
}
function applySpreadMetricSelectorStyles(selector: HTMLElement): void {
selector.style.display = "flex";
selector.style.alignItems = "center";
selector.style.gap = "12px";
selector.style.flex = "0 0 auto";
selector.style.whiteSpace = "nowrap";
function applyToolbarActionGroupStyles(group: HTMLElement): void {
group.style.display = "flex";
group.style.alignItems = "center";
group.style.gap = "8px";
group.style.flex = "0 0 auto";
group.style.flexWrap = "nowrap";
}
function applySpreadMetricOptionStyles(option: HTMLElement): void {
option.style.display = "inline-flex";
option.style.alignItems = "center";
option.style.gap = "5px";
option.style.height = "32px";
option.style.color = "#344054";
option.style.fontSize = "12px";
option.style.fontWeight = "700";
option.style.whiteSpace = "nowrap";
function applyToolbarActionDividerStyles(divider: HTMLElement): void {
divider.style.width = "1px";
divider.style.height = "20px";
divider.style.background = "#d0d5dd";
divider.style.flex = "0 0 auto";
}
function applyMetricCatalogStyles(catalog: HTMLElement): void {
catalog.style.position = "relative";
catalog.style.display = "inline-flex";
catalog.style.alignItems = "center";
catalog.style.gap = "8px";
catalog.style.flex = "0 0 auto";
}
function applyMetricCatalogTriggerStyles(button: HTMLButtonElement): void {
button.style.height = "32px";
button.style.padding = "0 10px";
button.style.border = "1px solid #94a3b8";
button.style.borderRadius = "6px";
button.style.background = "#ffffff";
button.style.color = "#334155";
button.style.fontSize = "12px";
button.style.fontWeight = "700";
button.style.whiteSpace = "nowrap";
}
function applyMetricCatalogCountStyles(count: HTMLElement): void {
count.style.color = "#64748b";
count.style.fontSize = "12px";
count.style.fontWeight = "700";
count.style.whiteSpace = "nowrap";
}
function applyMetricCatalogPanelStyles(panel: HTMLElement): void {
panel.style.position = "absolute";
panel.style.top = "calc(100% + 6px)";
panel.style.left = "0";
panel.style.zIndex = "4";
panel.style.width = "260px";
panel.style.padding = "10px";
panel.style.border = "1px solid #cbd5e1";
panel.style.borderRadius = "6px";
panel.style.background = "#ffffff";
panel.style.boxShadow = "0 10px 20px rgba(15, 23, 42, 0.14)";
}
function applyMetricCatalogHeaderStyles(header: HTMLElement): void {
header.style.display = "flex";
header.style.alignItems = "center";
header.style.gap = "6px";
}
function applyMetricCatalogSearchStyles(input: HTMLInputElement): void {
input.style.width = "100%";
input.style.height = "30px";
input.style.padding = "0 8px";
input.style.border = "1px solid #cbd5e1";
input.style.borderRadius = "5px";
input.style.boxSizing = "border-box";
}
function applyMetricCatalogCloseButtonStyles(button: HTMLButtonElement): void {
button.style.width = "30px";
button.style.height = "30px";
button.style.padding = "0";
button.style.border = "0";
button.style.background = "transparent";
button.style.color = "#64748b";
button.style.fontSize = "20px";
}
function applyMetricCatalogGroupStyles(group: HTMLElement): void {
group.style.display = "flex";
group.style.flexDirection = "column";
group.style.gap = "4px";
group.style.marginTop = "10px";
}
function applyMetricCatalogGroupTitleStyles(title: HTMLElement): void {
title.style.color = "#64748b";
title.style.fontSize = "12px";
}
function applyMetricCatalogItemStyles(item: HTMLElement): void {
item.style.display = "flex";
item.style.alignItems = "center";
item.style.justifyContent = "space-between";
item.style.minHeight = "32px";
item.style.color = "#334155";
item.style.fontSize = "13px";
}
function applyMetricCatalogActionStyles(button: HTMLButtonElement): void {
button.style.minWidth = "56px";
button.style.height = "28px";
button.style.border = "1px solid #94a3b8";
button.style.borderRadius = "5px";
button.style.background = "#ffffff";
button.style.color = "#334155";
button.style.fontSize = "12px";
}
function applySpreadRulesGroupStyles(group: HTMLElement): void {
@@ -884,9 +1320,46 @@ function applySpreadMetricRuleStyles(rule: HTMLElement): void {
rule.style.display = "flex";
rule.style.alignItems = "center";
rule.style.gap = "7px";
rule.style.minWidth = "max-content";
rule.style.flexWrap = "wrap";
rule.style.minWidth = "0";
rule.style.minHeight = "32px";
rule.style.whiteSpace = "nowrap";
}
function applySpreadMetricRulePrimaryStyles(primary: HTMLElement): void {
primary.style.display = "flex";
primary.style.alignItems = "center";
primary.style.gap = "7px";
primary.style.minWidth = "0";
primary.style.whiteSpace = "nowrap";
}
function applySpreadMetricRuleSecondaryStyles(secondary: HTMLElement): void {
secondary.style.display = "flex";
secondary.style.alignItems = "center";
secondary.style.gap = "7px";
secondary.style.minWidth = "0";
secondary.style.whiteSpace = "nowrap";
}
function applySpreadMetricDetailsButtonStyles(button: HTMLButtonElement): void {
button.style.height = "28px";
button.style.padding = "0 8px";
button.style.border = "1px solid #cbd5e1";
button.style.borderRadius = "5px";
button.style.background = "#ffffff";
button.style.color = "#475569";
button.style.fontSize = "12px";
}
function applySpreadMetricRemoveButtonStyles(button: HTMLButtonElement): void {
button.style.width = "28px";
button.style.height = "28px";
button.style.padding = "0";
button.style.border = "0";
button.style.borderRadius = "4px";
button.style.background = "transparent";
button.style.color = "#b91c1c";
button.style.fontSize = "20px";
}
function applySpreadMetricRuleLabelStyles(label: HTMLElement): void {
@@ -953,16 +1426,18 @@ function applyNativeControlStyles(
controls.batchSubmitButton.className = nativeButton.className;
}
[
const secondaryButtons = [
controls.exportButton,
controls.audienceProfileExportButton,
controls.audienceProfileByIdExportButton,
controls.audienceProfileFieldButton,
controls.batchSubmitButton
].forEach((button) => {
applyPrimaryButtonStyles(button);
controls.audienceProfileFieldButton
];
secondaryButtons.forEach((button) => {
applySecondaryButtonStyles(button);
button.style.whiteSpace = "nowrap";
});
applyPrimaryButtonStyles(controls.batchSubmitButton);
controls.batchSubmitButton.style.whiteSpace = "nowrap";
const ruleControls = Object.values(controls.spreadMetricRules).flatMap(
(rule) => [
@@ -1037,6 +1512,19 @@ function applyPrimaryButtonStyles(
"background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease";
}
function applySecondaryButtonStyles(button: HTMLButtonElement): void {
button.style.backgroundColor = "#ffffff";
button.style.border = "1px solid #cbd5e1";
button.style.borderRadius = "8px";
button.style.color = "#344054";
button.style.height = "32px";
button.style.padding = "0 15px";
button.style.boxSizing = "border-box";
button.style.fontWeight = "600";
button.style.transition =
"background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease";
}
function applySpreadThresholdControlStyles(control: HTMLElement): void {
control.style.display = "grid";
control.style.gridTemplateColumns = "auto auto 1fr auto";
@@ -1072,7 +1560,11 @@ function ensurePluginActionButtonTheme(document: Document): void {
[data-plugin-export="button"]:hover:not(:disabled),
[data-plugin-export-audience-profile="button"]:hover:not(:disabled),
[data-plugin-export-audience-profile-by-id="button"]:hover:not(:disabled),
[data-plugin-audience-profile-fields="button"]:hover:not(:disabled),
[data-plugin-audience-profile-fields="button"]:hover:not(:disabled) {
background-color: #f8fafc !important;
border-color: #94a3b8 !important;
}
[data-plugin-batch-submit="button"]:hover:not(:disabled) {
background-color: #6d1627 !important;
border-color: #6d1627 !important;
@@ -1081,7 +1573,12 @@ function ensurePluginActionButtonTheme(document: Document): void {
[data-plugin-export="button"]:active:not(:disabled),
[data-plugin-export-audience-profile="button"]:active:not(:disabled),
[data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled),
[data-plugin-audience-profile-fields="button"]:active:not(:disabled),
[data-plugin-audience-profile-fields="button"]:active:not(:disabled) {
background-color: #f1f5f9 !important;
border-color: #94a3b8 !important;
transform: translateY(1px);
}
[data-plugin-batch-submit="button"]:active:not(:disabled) {
background-color: #58111f !important;
border-color: #58111f !important;
@@ -1091,7 +1588,11 @@ function ensurePluginActionButtonTheme(document: Document): void {
[data-plugin-export="button"]:focus-visible,
[data-plugin-export-audience-profile="button"]:focus-visible,
[data-plugin-export-audience-profile-by-id="button"]:focus-visible,
[data-plugin-audience-profile-fields="button"]:focus-visible,
[data-plugin-audience-profile-fields="button"]:focus-visible {
outline: none !important;
box-shadow: 0 0 0 3px rgba(52, 64, 84, 0.18) !important;
}
[data-plugin-batch-submit="button"]:focus-visible {
outline: none !important;
box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important;
@@ -1100,7 +1601,16 @@ function ensurePluginActionButtonTheme(document: Document): void {
[data-plugin-export="button"]:disabled,
[data-plugin-export-audience-profile="button"]:disabled,
[data-plugin-export-audience-profile-by-id="button"]:disabled,
[data-plugin-audience-profile-fields="button"]:disabled,
[data-plugin-audience-profile-fields="button"]:disabled {
background-color: #f8fafc !important;
border-color: #cbd5e1 !important;
color: #98a2b3 !important;
cursor: not-allowed !important;
opacity: 1 !important;
transform: none !important;
box-shadow: none !important;
}
[data-plugin-batch-submit="button"]:disabled {
background-color: #c89ca4 !important;
border-color: #c89ca4 !important;
@@ -1128,6 +1638,54 @@ function ensurePluginActionButtonTheme(document: Document): void {
border-color: #dc2626 !important;
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.12) !important;
}
@media (max-width: 720px) {
[data-plugin-toolbar='root'],
[data-plugin-toolbar-row='thresholds'] {
width: 100% !important;
}
[data-plugin-toolbar-row='thresholds'] {
flex-wrap: wrap !important;
align-items: flex-start !important;
}
[data-plugin-toolbar-group='data'],
[data-plugin-toolbar-action-group] {
flex-wrap: wrap !important;
}
[data-plugin-toolbar-action-divider] {
display: none !important;
}
[data-plugin-spread-metric-catalog='root'] {
width: 100% !important;
}
[data-plugin-spread-metric-catalog-panel='root'] {
position: static !important;
width: 100% !important;
margin-top: 6px !important;
box-sizing: border-box !important;
}
[data-plugin-spread-rules='root'] {
width: 100% !important;
overflow: visible !important;
}
[data-plugin-spread-rule] {
align-items: stretch !important;
flex-direction: column !important;
}
[data-plugin-spread-rule-primary],
[data-plugin-spread-rule-secondary] {
width: 100% !important;
flex-wrap: wrap !important;
}
}
`;
document.head.appendChild(style);
}