feat(seo): refine keyword curation workspace
This commit is contained in:
parent
a1d1a4980f
commit
2a0e72543e
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,246 @@
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
export type NdcGlassSelectOption<T extends string = string> = {
|
||||||
|
value: T;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
actions?: Array<{
|
||||||
|
ariaLabel: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
icon: ReactNode;
|
||||||
|
onClick: () => void;
|
||||||
|
title?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NdcGlassSelectProps<T extends string = string> = {
|
||||||
|
value: T;
|
||||||
|
options: NdcGlassSelectOption<T>[];
|
||||||
|
ariaLabel?: string;
|
||||||
|
className?: string;
|
||||||
|
menuClassName?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NdcGlassSelect<T extends string = string>({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
ariaLabel,
|
||||||
|
className,
|
||||||
|
menuClassName,
|
||||||
|
disabled = false,
|
||||||
|
onChange
|
||||||
|
}: NdcGlassSelectProps<T>) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [menuRect, setMenuRect] = useState({ top: 0, left: 0, width: 0 });
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const selectId = useMemo(() => `ndc-select-${Math.random().toString(16).slice(2)}`, []);
|
||||||
|
const selected = options.find((option) => option.value === value) ?? options[0];
|
||||||
|
|
||||||
|
const updateMenuRect = () => {
|
||||||
|
const root = rootRef.current;
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = root.getBoundingClientRect();
|
||||||
|
const valueRect = root.querySelector<HTMLElement>(".nodedc-select__value")?.getBoundingClientRect();
|
||||||
|
const menuMaxHeight = 232;
|
||||||
|
const bottomTop = rect.bottom + 6;
|
||||||
|
const top =
|
||||||
|
bottomTop + menuMaxHeight > window.innerHeight - 12
|
||||||
|
? Math.max(12, rect.top - menuMaxHeight - 6)
|
||||||
|
: bottomTop;
|
||||||
|
|
||||||
|
setMenuRect({
|
||||||
|
top,
|
||||||
|
left: valueRect?.left ?? rect.left,
|
||||||
|
width: Math.max(180, valueRect?.width ?? rect.width)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMenuRect();
|
||||||
|
|
||||||
|
const handlePointerDown = (event: MouseEvent) => {
|
||||||
|
const root = rootRef.current;
|
||||||
|
const menu = menuRef.current;
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.target instanceof Node &&
|
||||||
|
root &&
|
||||||
|
!root.contains(event.target) &&
|
||||||
|
(!menu || !menu.contains(event.target))
|
||||||
|
) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleOtherSelectOpen = (event: Event) => {
|
||||||
|
const detail = (event as CustomEvent<{ id?: string }>).detail;
|
||||||
|
|
||||||
|
if (detail?.id !== selectId) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleViewportChange = () => updateMenuRect();
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handlePointerDown);
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
window.addEventListener("nodedc-select-open", handleOtherSelectOpen as EventListener);
|
||||||
|
window.addEventListener("resize", handleViewportChange);
|
||||||
|
window.addEventListener("scroll", handleViewportChange, true);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handlePointerDown);
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
window.removeEventListener("nodedc-select-open", handleOtherSelectOpen as EventListener);
|
||||||
|
window.removeEventListener("resize", handleViewportChange);
|
||||||
|
window.removeEventListener("scroll", handleViewportChange, true);
|
||||||
|
};
|
||||||
|
}, [open, selectId]);
|
||||||
|
|
||||||
|
const openMenu = () => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen((current) => {
|
||||||
|
if (!current) {
|
||||||
|
updateMenuRect();
|
||||||
|
window.dispatchEvent(new CustomEvent("nodedc-select-open", { detail: { id: selectId } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return !current;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={rootRef} className={["nodedc-select", className].filter(Boolean).join(" ")}>
|
||||||
|
<div className="nodedc-select__control">
|
||||||
|
<div className="nodedc-select__value">{selected?.label}</div>
|
||||||
|
<button
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className="nodedc-select__toggle"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={openMenu}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" className="nodedc-select__chevron" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{open
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className={["nodedc-dropdown-surface nodedc-select__menu", menuClassName].filter(Boolean).join(" ")}
|
||||||
|
role="listbox"
|
||||||
|
style={{
|
||||||
|
left: menuRect.left,
|
||||||
|
position: "fixed",
|
||||||
|
top: menuRect.top,
|
||||||
|
width: menuRect.width
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<div
|
||||||
|
aria-disabled={option.disabled === true}
|
||||||
|
aria-selected={option.value === value}
|
||||||
|
className="nodedc-dropdown-option nodedc-select__option"
|
||||||
|
data-disabled={option.disabled === true ? "true" : undefined}
|
||||||
|
data-selected={option.value === value ? "true" : undefined}
|
||||||
|
data-value={option.value}
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => {
|
||||||
|
if (option.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(option.value);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (option.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(option.value);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
role="option"
|
||||||
|
tabIndex={option.disabled ? -1 : 0}
|
||||||
|
>
|
||||||
|
<span className="nodedc-select__option-label">{option.label}</span>
|
||||||
|
{option.actions?.length ? (
|
||||||
|
<span className="nodedc-select__option-actions" aria-hidden={false}>
|
||||||
|
{option.actions.map((action) => (
|
||||||
|
<span
|
||||||
|
aria-label={action.ariaLabel}
|
||||||
|
className="nodedc-select__option-action"
|
||||||
|
data-disabled={action.disabled ? "true" : undefined}
|
||||||
|
key={action.ariaLabel}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!action.disabled) {
|
||||||
|
action.onClick();
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!action.disabled) {
|
||||||
|
action.onClick();
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={action.disabled ? -1 : 0}
|
||||||
|
title={action.title ?? action.ariaLabel}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -336,6 +336,46 @@ export type SemanticAnalysisRun = {
|
||||||
wordCount: number;
|
wordCount: number;
|
||||||
matchedClusters: string[];
|
matchedClusters: string[];
|
||||||
}>;
|
}>;
|
||||||
|
textCorpus: {
|
||||||
|
schemaVersion: "seo-text-corpus.v1";
|
||||||
|
extraction: {
|
||||||
|
chunkCount: number;
|
||||||
|
documentCount: number;
|
||||||
|
mode: "clean_visible_text";
|
||||||
|
sourceDocumentCount: number;
|
||||||
|
totalChars: number;
|
||||||
|
totalWords: number;
|
||||||
|
};
|
||||||
|
documents: Array<{
|
||||||
|
chunkIds: string[];
|
||||||
|
id: string;
|
||||||
|
kind: "page" | "content_json";
|
||||||
|
scope: "indexable_page" | "template_block" | "admin_page" | "technical_page" | "content_source";
|
||||||
|
selected: boolean;
|
||||||
|
sourcePath: string;
|
||||||
|
textCharCount: number;
|
||||||
|
title: string;
|
||||||
|
urlPath: string | null;
|
||||||
|
usedForCoverage: boolean;
|
||||||
|
wordCount: number;
|
||||||
|
}>;
|
||||||
|
chunks: Array<{
|
||||||
|
charEnd: number;
|
||||||
|
charStart: number;
|
||||||
|
chunkIndex: number;
|
||||||
|
documentId: string;
|
||||||
|
id: string;
|
||||||
|
kind: "page" | "content_json";
|
||||||
|
scope: "indexable_page" | "template_block" | "admin_page" | "technical_page" | "content_source";
|
||||||
|
selected: boolean;
|
||||||
|
sourcePath: string;
|
||||||
|
text: string;
|
||||||
|
title: string;
|
||||||
|
totalChunks: number;
|
||||||
|
urlPath: string | null;
|
||||||
|
usedForCoverage: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
legacyFindings: Array<{
|
legacyFindings: Array<{
|
||||||
markerId: string;
|
markerId: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -556,7 +596,7 @@ export type SeoContextReviewRun = {
|
||||||
runId: string;
|
runId: string;
|
||||||
status: string;
|
status: string;
|
||||||
provider: {
|
provider: {
|
||||||
mode: "codex_manual" | "deterministic_fallback";
|
mode: "codex_manual" | "codex_workspace" | "deterministic_fallback";
|
||||||
modelRequired: true;
|
modelRequired: true;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
@ -616,7 +656,7 @@ export type SeoNormalizationContract = {
|
||||||
generatedAt: string;
|
generatedAt: string;
|
||||||
state: "not_ready" | "ready";
|
state: "not_ready" | "ready";
|
||||||
provider: {
|
provider: {
|
||||||
mode: "codex_manual" | "deterministic_fallback";
|
mode: "codex_manual" | "codex_workspace" | "deterministic_fallback";
|
||||||
modelRequired: boolean;
|
modelRequired: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
@ -737,9 +777,11 @@ export type AnchorReviewDecision = {
|
||||||
sendToSerp: boolean;
|
sendToSerp: boolean;
|
||||||
sendToWordstat: boolean;
|
sendToWordstat: boolean;
|
||||||
source: "human" | "model" | "system";
|
source: "human" | "model" | "system";
|
||||||
status: "approved" | "disabled" | "pending";
|
status: "approved" | "deleted" | "disabled" | "pending";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DemandCollectionProfile = "contextual" | "market_wide";
|
||||||
|
|
||||||
export type AnchorReviewContract = {
|
export type AnchorReviewContract = {
|
||||||
schemaVersion: "anchor-review.v1";
|
schemaVersion: "anchor-review.v1";
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|
@ -747,6 +789,7 @@ export type AnchorReviewContract = {
|
||||||
projectOntologyVersionId: string | null;
|
projectOntologyVersionId: string | null;
|
||||||
generatedAt: string;
|
generatedAt: string;
|
||||||
state: "approved" | "empty" | "needs_review" | "not_ready";
|
state: "approved" | "empty" | "needs_review" | "not_ready";
|
||||||
|
demandCollectionProfile: DemandCollectionProfile;
|
||||||
source: {
|
source: {
|
||||||
normalizationGeneratedAt: string | null;
|
normalizationGeneratedAt: string | null;
|
||||||
normalizationProviderMode: SeoNormalizationContract["provider"]["mode"] | null;
|
normalizationProviderMode: SeoNormalizationContract["provider"]["mode"] | null;
|
||||||
|
|
@ -794,6 +837,13 @@ export type AnchorReviewContract = {
|
||||||
};
|
};
|
||||||
decision: AnchorReviewDecision;
|
decision: AnchorReviewDecision;
|
||||||
}>;
|
}>;
|
||||||
|
deletedAnchors: Array<{
|
||||||
|
deletedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
phrase: string;
|
||||||
|
reason: string;
|
||||||
|
}>;
|
||||||
wordstatQueue: Array<{
|
wordstatQueue: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
anchorId: string;
|
anchorId: string;
|
||||||
|
|
@ -845,6 +895,7 @@ export type MarketEnrichmentContract = {
|
||||||
state: "not_ready" | "not_collected" | "not_configured" | "partial_ready";
|
state: "not_ready" | "not_collected" | "not_configured" | "partial_ready";
|
||||||
normalization: SeoNormalizationContract;
|
normalization: SeoNormalizationContract;
|
||||||
anchorReview: AnchorReviewContract;
|
anchorReview: AnchorReviewContract;
|
||||||
|
demandCollectionProfile: DemandCollectionProfile;
|
||||||
readiness: {
|
readiness: {
|
||||||
anchorApprovedCount: number;
|
anchorApprovedCount: number;
|
||||||
anchorPendingCount: number;
|
anchorPendingCount: number;
|
||||||
|
|
@ -995,6 +1046,115 @@ export type MarketEnrichmentContract = {
|
||||||
nextActions: string[];
|
nextActions: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WordstatProbePlanSource =
|
||||||
|
| "approved_anchor"
|
||||||
|
| "commercial_demand"
|
||||||
|
| "market_anchor_expansion"
|
||||||
|
| "market_frontier_discovery"
|
||||||
|
| "previous_expansion";
|
||||||
|
|
||||||
|
export type WordstatProbePlanRejectedStatus =
|
||||||
|
| "already_collected"
|
||||||
|
| "already_requested"
|
||||||
|
| "duplicate_candidate"
|
||||||
|
| "invalid_phrase"
|
||||||
|
| "not_selected_budget_or_balance"
|
||||||
|
| "suppressed";
|
||||||
|
|
||||||
|
export type WordstatProbePlanSelectedProbe = {
|
||||||
|
phrase: string;
|
||||||
|
normalizedPhrase: string;
|
||||||
|
clusterId: string;
|
||||||
|
clusterTitle: string;
|
||||||
|
priority: "high" | "medium" | "low";
|
||||||
|
source: WordstatProbePlanSource;
|
||||||
|
score: number;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WordstatProbePlanWarning = {
|
||||||
|
code:
|
||||||
|
| "frontier_missing"
|
||||||
|
| "high_reuse_blocking"
|
||||||
|
| "low_diversity"
|
||||||
|
| "low_selected_count"
|
||||||
|
| "weak_generic_probes";
|
||||||
|
level: "info" | "warning";
|
||||||
|
message: string;
|
||||||
|
examples: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WordstatProbePlanPreviewContract = {
|
||||||
|
schemaVersion: "wordstat-probe-plan-preview.v1";
|
||||||
|
projectId: string;
|
||||||
|
semanticRunId: string | null;
|
||||||
|
generatedAt: string;
|
||||||
|
demandCollectionProfile: DemandCollectionProfile;
|
||||||
|
orchestration: {
|
||||||
|
mode: "backend_orchestrated_contract_pipeline";
|
||||||
|
executionMode: "preview_only";
|
||||||
|
modelCanCallWordstat: false;
|
||||||
|
steps: string[];
|
||||||
|
};
|
||||||
|
provider: {
|
||||||
|
wordstatStatus: MarketEnrichmentContract["providers"][number]["status"];
|
||||||
|
wordstatMode: string;
|
||||||
|
};
|
||||||
|
readiness: {
|
||||||
|
canExecute: boolean;
|
||||||
|
blockers: string[];
|
||||||
|
probeBudget: number;
|
||||||
|
candidateCount: number;
|
||||||
|
selectedCount: number;
|
||||||
|
};
|
||||||
|
memory: {
|
||||||
|
requestedPhraseCount: number;
|
||||||
|
collectedPhraseCount: number;
|
||||||
|
suppressedPhraseCount: number;
|
||||||
|
frontierSourceTaskId: string | null;
|
||||||
|
frontierProbeSeedCount: number;
|
||||||
|
};
|
||||||
|
selectedProbes: WordstatProbePlanSelectedProbe[];
|
||||||
|
warnings: WordstatProbePlanWarning[];
|
||||||
|
candidateSourceCounts: Record<WordstatProbePlanSource, number>;
|
||||||
|
rejectedSummary: Record<WordstatProbePlanRejectedStatus, number>;
|
||||||
|
rejectedSamples: Array<WordstatProbePlanSelectedProbe & {
|
||||||
|
status: WordstatProbePlanRejectedStatus | "selected";
|
||||||
|
statusLabel: string;
|
||||||
|
}>;
|
||||||
|
nextActions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KeywordAnalysisWorkflowContract = {
|
||||||
|
schemaVersion: "keyword-analysis-workflow.v1";
|
||||||
|
runId: string;
|
||||||
|
projectId: string;
|
||||||
|
semanticRunId: string | null;
|
||||||
|
generatedAt: string;
|
||||||
|
status: "blocked" | "completed" | "failed" | "queued" | "running" | "waiting";
|
||||||
|
statusLabel: string;
|
||||||
|
activeStepId: "demand_collection" | "keyword_cleaning" | "keyword_proposal" | "project_context" | "semantic_basis" | null;
|
||||||
|
activeStepLabel: string | null;
|
||||||
|
summary: string;
|
||||||
|
reason: string | null;
|
||||||
|
steps: Array<{
|
||||||
|
id: "demand_collection" | "keyword_cleaning" | "keyword_proposal" | "project_context" | "semantic_basis";
|
||||||
|
label: string;
|
||||||
|
status: "blocked" | "completed" | "failed" | "pending" | "running" | "waiting";
|
||||||
|
detail: string;
|
||||||
|
reason: string | null;
|
||||||
|
startedAt: string | null;
|
||||||
|
completedAt: string | null;
|
||||||
|
}>;
|
||||||
|
artifacts: {
|
||||||
|
keywordCleaningRunId: string | null;
|
||||||
|
marketGeneratedAt: string | null;
|
||||||
|
modelTaskRunId: string | null;
|
||||||
|
wordstatJobId: string | null;
|
||||||
|
};
|
||||||
|
nextActions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type KeywordCleaningItem = {
|
export type KeywordCleaningItem = {
|
||||||
id: string;
|
id: string;
|
||||||
phrase: string;
|
phrase: string;
|
||||||
|
|
@ -1029,7 +1189,7 @@ export type KeywordCleaningContract = {
|
||||||
generatedAt: string;
|
generatedAt: string;
|
||||||
state: "not_ready" | "ready";
|
state: "not_ready" | "ready";
|
||||||
provider: {
|
provider: {
|
||||||
mode: "codex_manual" | "deterministic_fallback";
|
mode: "codex_manual" | "codex_workspace" | "deterministic_fallback";
|
||||||
modelRequired: boolean;
|
modelRequired: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
@ -1093,8 +1253,11 @@ export type KeywordCleaningContract = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SeoModelTaskType =
|
export type SeoModelTaskType =
|
||||||
|
| "seo.business_synthesis"
|
||||||
|
| "seo.commercial_demand"
|
||||||
| "seo.context_review"
|
| "seo.context_review"
|
||||||
| "seo.keyword_cleaning"
|
| "seo.keyword_cleaning"
|
||||||
|
| "seo.market_frontier_discovery"
|
||||||
| "seo.normalization"
|
| "seo.normalization"
|
||||||
| "seo.serp_interpretation"
|
| "seo.serp_interpretation"
|
||||||
| "seo.strategy_quality_review"
|
| "seo.strategy_quality_review"
|
||||||
|
|
@ -1167,7 +1330,10 @@ export type SeoModelTaskPersistedResult = {
|
||||||
schemaVersion: "seo-model-persisted-result.v1";
|
schemaVersion: "seo-model-persisted-result.v1";
|
||||||
status: "promoted_to_domain_evidence" | "stored_as_model_task_evidence";
|
status: "promoted_to_domain_evidence" | "stored_as_model_task_evidence";
|
||||||
runType:
|
runType:
|
||||||
|
| "seo_business_synthesis"
|
||||||
|
| "seo_commercial_demand"
|
||||||
| "keyword_cleaning"
|
| "keyword_cleaning"
|
||||||
|
| "seo_context_review"
|
||||||
| "seo_model_task"
|
| "seo_model_task"
|
||||||
| "seo_normalization"
|
| "seo_normalization"
|
||||||
| "seo_strategy_quality_review"
|
| "seo_strategy_quality_review"
|
||||||
|
|
@ -1322,7 +1488,7 @@ export type SerpInterpretationContract = {
|
||||||
generatedAt: string;
|
generatedAt: string;
|
||||||
state: "not_ready" | "ready";
|
state: "not_ready" | "ready";
|
||||||
provider: {
|
provider: {
|
||||||
mode: "codex_manual" | "deterministic_fallback";
|
mode: "codex_manual" | "codex_workspace" | "deterministic_fallback";
|
||||||
modelRequired: true;
|
modelRequired: true;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
@ -1466,6 +1632,31 @@ export type KeywordMapRoleOverrideInput = {
|
||||||
role: KeywordMapRole;
|
role: KeywordMapRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type KeywordContextSnapshotSummary = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
source: "expansion_checkpoint" | "manual_save";
|
||||||
|
createdAt: string;
|
||||||
|
semanticRunId: string | null;
|
||||||
|
keywordMapGeneratedAt: string | null;
|
||||||
|
keywordCount: number;
|
||||||
|
keywordPhrases: string[];
|
||||||
|
suppressedCount: number;
|
||||||
|
roleOverrideCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KeywordContextSnapshotInput = {
|
||||||
|
label?: string;
|
||||||
|
note?: string;
|
||||||
|
source?: "expansion_checkpoint" | "manual_save";
|
||||||
|
curation?: {
|
||||||
|
roleOverrides?: KeywordMapRoleOverrideInput[];
|
||||||
|
suppressedItemIds?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KeywordContextSnapshotCurationInput = NonNullable<KeywordContextSnapshotInput["curation"]>;
|
||||||
|
|
||||||
export type SeoStrategyContract = {
|
export type SeoStrategyContract = {
|
||||||
schemaVersion: "seo-strategy.v1";
|
schemaVersion: "seo-strategy.v1";
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|
@ -1650,6 +1841,16 @@ export type SeoStrategyContract = {
|
||||||
nextActions: string[];
|
nextActions: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StrategyPhaseAction = {
|
||||||
|
role?: string;
|
||||||
|
title: string;
|
||||||
|
action?: string;
|
||||||
|
reason?: string;
|
||||||
|
priority?: "high" | "low" | "medium";
|
||||||
|
targetPath?: string | null;
|
||||||
|
evidenceRefs?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type StrategySynthesisContract = {
|
export type StrategySynthesisContract = {
|
||||||
schemaVersion: "seo-strategy-synthesis.v1";
|
schemaVersion: "seo-strategy-synthesis.v1";
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|
@ -1658,7 +1859,7 @@ export type StrategySynthesisContract = {
|
||||||
generatedAt: string;
|
generatedAt: string;
|
||||||
state: "not_ready" | "ready";
|
state: "not_ready" | "ready";
|
||||||
provider: {
|
provider: {
|
||||||
mode: "codex_manual" | "deterministic_fallback";
|
mode: "codex_manual" | "codex_workspace" | "deterministic_fallback";
|
||||||
modelRequired: true;
|
modelRequired: true;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
@ -1697,7 +1898,13 @@ export type StrategySynthesisContract = {
|
||||||
promotionSignals: string[];
|
promotionSignals: string[];
|
||||||
phaseRoadmap: SeoStrategyContract["phasePlan"];
|
phaseRoadmap: SeoStrategyContract["phasePlan"];
|
||||||
};
|
};
|
||||||
phaseStrategies: SeoStrategyContract["phaseStrategies"];
|
phaseStrategies: Array<
|
||||||
|
Omit<SeoStrategyContract["phaseStrategies"][number], "doNow" | "hold" | "prepareNext"> & {
|
||||||
|
doNow: Array<string | StrategyPhaseAction>;
|
||||||
|
hold: Array<string | StrategyPhaseAction>;
|
||||||
|
prepareNext: Array<string | StrategyPhaseAction>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
recommendedPath: {
|
recommendedPath: {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -1759,7 +1966,7 @@ export type StrategyQualityReviewContract = {
|
||||||
generatedAt: string;
|
generatedAt: string;
|
||||||
state: "not_ready" | "ready";
|
state: "not_ready" | "ready";
|
||||||
provider: {
|
provider: {
|
||||||
mode: "codex_manual" | "deterministic_fallback";
|
mode: "codex_manual" | "codex_workspace" | "deterministic_fallback";
|
||||||
modelRequired: true;
|
modelRequired: true;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
@ -2874,6 +3081,16 @@ export async function fetchLatestMarketEnrichment(projectId: string) {
|
||||||
return response.json() as Promise<{ marketEnrichment: MarketEnrichmentContract }>;
|
return response.json() as Promise<{ marketEnrichment: MarketEnrichmentContract }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchWordstatProbePlanPreview(projectId: string) {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/market-enrichment/probe-preview`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getErrorMessage(response, `Не удалось построить Wordstat probe preview: ${response.status}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<{ probePreview: WordstatProbePlanPreviewContract }>;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchLatestSeoContextReview(projectId: string) {
|
export async function fetchLatestSeoContextReview(projectId: string) {
|
||||||
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/context-review/latest`);
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/context-review/latest`);
|
||||||
|
|
||||||
|
|
@ -2981,9 +3198,26 @@ export async function fetchLatestAnchorReview(projectId: string) {
|
||||||
return response.json() as Promise<{ anchorReview: AnchorReviewContract }>;
|
return response.json() as Promise<{ anchorReview: AnchorReviewContract }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveAnchorReviewDecisions(projectId: string, decisions?: AnchorReviewDecisionInput[]) {
|
export async function saveAnchorReviewDecisions(
|
||||||
|
projectId: string,
|
||||||
|
decisions?: AnchorReviewDecisionInput[],
|
||||||
|
demandCollectionProfile?: DemandCollectionProfile
|
||||||
|
) {
|
||||||
|
const payload: {
|
||||||
|
decisions?: AnchorReviewDecisionInput[];
|
||||||
|
demandCollectionProfile?: DemandCollectionProfile;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
if (decisions) {
|
||||||
|
payload.decisions = decisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (demandCollectionProfile) {
|
||||||
|
payload.demandCollectionProfile = demandCollectionProfile;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/anchor-review/decisions`, {
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/anchor-review/decisions`, {
|
||||||
body: JSON.stringify({ decisions }),
|
body: JSON.stringify(payload),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
|
|
@ -3051,6 +3285,28 @@ export async function runMarketEnrichment(projectId: string) {
|
||||||
return response.json() as Promise<{ marketEnrichment: MarketEnrichmentContract }>;
|
return response.json() as Promise<{ marketEnrichment: MarketEnrichmentContract }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchLatestKeywordAnalysisWorkflow(projectId: string) {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/keyword-analysis-workflow/latest`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getErrorMessage(response, `Не удалось загрузить workflow анализа ключей: ${response.status}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<{ workflow: KeywordAnalysisWorkflowContract | null }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startKeywordAnalysisWorkflow(projectId: string) {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/keyword-analysis-workflow`, {
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getErrorMessage(response, `Не удалось запустить workflow анализа ключей: ${response.status}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<{ workflow: KeywordAnalysisWorkflowContract }>;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchLatestYandexEvidence(projectId: string) {
|
export async function fetchLatestYandexEvidence(projectId: string) {
|
||||||
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/yandex-evidence/latest`);
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/yandex-evidence/latest`);
|
||||||
|
|
||||||
|
|
@ -3168,6 +3424,7 @@ export async function approveKeywordMapDecisions(
|
||||||
input?: {
|
input?: {
|
||||||
itemIds?: string[];
|
itemIds?: string[];
|
||||||
roleOverrides?: KeywordMapRoleOverrideInput[];
|
roleOverrides?: KeywordMapRoleOverrideInput[];
|
||||||
|
suppressedItemIds?: string[];
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/keyword-map/decisions`, {
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/keyword-map/decisions`, {
|
||||||
|
|
@ -3190,6 +3447,78 @@ export async function approveKeywordMapDecisions(
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchKeywordContextSnapshots(projectId: string) {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/keyword-context-snapshots`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getErrorMessage(response, `Не удалось загрузить snapshot-профили ключей: ${response.status}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<{ snapshots: KeywordContextSnapshotSummary[] }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveKeywordContextSnapshot(projectId: string, input: KeywordContextSnapshotInput) {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/keyword-context-snapshots`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(input)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getErrorMessage(response, `Не удалось сохранить snapshot ключей: ${response.status}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<{ snapshot: KeywordContextSnapshotSummary }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renameKeywordContextSnapshot(projectId: string, snapshotId: string, label: string) {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/keyword-context-snapshots/${snapshotId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ label })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getErrorMessage(response, `Не удалось переименовать snapshot ключей: ${response.status}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<{ snapshot: KeywordContextSnapshotSummary }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateKeywordContextSnapshotCuration(
|
||||||
|
projectId: string,
|
||||||
|
snapshotId: string,
|
||||||
|
curation: KeywordContextSnapshotCurationInput
|
||||||
|
) {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/keyword-context-snapshots/${snapshotId}/curation`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(curation)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getErrorMessage(response, `Не удалось сохранить раскладку ключей: ${response.status}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<{ snapshot: KeywordContextSnapshotSummary }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteKeywordContextSnapshot(projectId: string, snapshotId: string) {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/keyword-context-snapshots/${snapshotId}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getErrorMessage(response, `Не удалось удалить snapshot ключей: ${response.status}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchLatestRewritePlan(projectId: string) {
|
export async function fetchLatestRewritePlan(projectId: string) {
|
||||||
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/rewrite-plan/latest`);
|
const response = await fetch(`${apiBaseUrl}/projects/${projectId}/rewrite-plan/latest`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,49 @@
|
||||||
import { StrictMode } from "react";
|
import { Component, StrictMode, type ErrorInfo, type ReactNode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
|
|
||||||
|
type AppErrorBoundaryProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AppErrorBoundaryState = {
|
||||||
|
error: Error | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AppErrorBoundary extends Component<AppErrorBoundaryProps, AppErrorBoundaryState> {
|
||||||
|
state: AppErrorBoundaryState = { error: null };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): AppErrorBoundaryState {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error("NDC SEO UI render error", error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
return (
|
||||||
|
<main className="app-fatal-state">
|
||||||
|
<section>
|
||||||
|
<strong>Интерфейс SEO Mode упал при отрисовке</strong>
|
||||||
|
<p>{this.state.error.message}</p>
|
||||||
|
<button onClick={() => window.location.reload()} type="button">
|
||||||
|
Перезагрузить интерфейс
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<AppErrorBoundary>
|
||||||
|
<App />
|
||||||
|
</AppErrorBoundary>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2748,6 +2748,11 @@ input {
|
||||||
line-height: 1.38;
|
line-height: 1.38;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stage-6 .market-heading .strategy-stage-run-date {
|
||||||
|
color: var(--ink);
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
.stage-6 .market-heading > svg {
|
.stage-6 .market-heading > svg {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
|
|
@ -2760,6 +2765,51 @@ input {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-fatal-state {
|
||||||
|
display: grid;
|
||||||
|
min-height: 100vh;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: linear-gradient(135deg, #f2f2f2, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-fatal-state section {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
width: min(520px, 100%);
|
||||||
|
padding: 22px;
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-fatal-state strong {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-fatal-state p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 740;
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-fatal-state button {
|
||||||
|
justify-self: start;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 14px;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--ink);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 900;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.market-state-card {
|
.market-state-card {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
@ -2882,6 +2932,18 @@ input {
|
||||||
box-shadow: 0 14px 42px rgba(24, 32, 29, 0.055);
|
box-shadow: 0 14px 42px rgba(24, 32, 29, 0.055);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyword-stage-section.collapsed {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.keyword-stage-heading {
|
.keyword-stage-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
@ -2949,6 +3011,20 @@ input {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyword-stage-icon-action.active {
|
||||||
|
color: #fff;
|
||||||
|
background: #e83e8c;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 14px 34px rgba(232, 62, 140, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-toggle svg {
|
||||||
|
transition: transform 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-toggle.expanded svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
.keyword-stage-icon-action:disabled {
|
.keyword-stage-icon-action:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.52;
|
opacity: 0.52;
|
||||||
|
|
@ -2970,6 +3046,145 @@ input {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyword-stage-head > .keyword-stage-heading {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-process-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 14px;
|
||||||
|
background: rgba(24, 32, 29, 0.035);
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.07);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-process-card.active {
|
||||||
|
background: rgba(232, 62, 140, 0.045);
|
||||||
|
border-color: rgba(232, 62, 140, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-process-card.blocked {
|
||||||
|
background: rgba(139, 47, 47, 0.035);
|
||||||
|
border-color: rgba(139, 47, 47, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-process-card > svg {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #e83e8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-process-card.blocked > svg {
|
||||||
|
color: #8b2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-process-card > div {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-process-card span {
|
||||||
|
justify-self: start;
|
||||||
|
min-height: 22px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 9px;
|
||||||
|
color: #2e5a67;
|
||||||
|
background: rgba(46, 90, 103, 0.08);
|
||||||
|
border: 1px solid rgba(46, 90, 103, 0.14);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-process-card strong {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-stage-process-card small {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 740;
|
||||||
|
line-height: 1.38;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-workflow-step-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-workflow-step-list > div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(160px, 0.5fr) auto minmax(220px, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.07);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-workflow-step-list span,
|
||||||
|
.keyword-workflow-step-list strong,
|
||||||
|
.keyword-workflow-step-list small {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-workflow-step-list span {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-workflow-step-list strong {
|
||||||
|
justify-self: start;
|
||||||
|
min-height: 20px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(46, 90, 103, 0.08);
|
||||||
|
color: #2e5a67;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-workflow-step-list small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 740;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-workflow-step-list .completed strong {
|
||||||
|
background: rgba(47, 122, 89, 0.1);
|
||||||
|
color: #2f7a59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-workflow-step-list .blocked strong,
|
||||||
|
.keyword-workflow-step-list .failed strong {
|
||||||
|
background: rgba(139, 47, 47, 0.08);
|
||||||
|
color: #8b2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-workflow-step-list .running strong,
|
||||||
|
.keyword-workflow-step-list .waiting strong {
|
||||||
|
background: rgba(232, 62, 140, 0.1);
|
||||||
|
color: #bc2f72;
|
||||||
|
}
|
||||||
|
|
||||||
.anchor-review-row > div:first-child span {
|
.anchor-review-row > div:first-child span {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
|
|
@ -3029,11 +3244,11 @@ input {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyword-curation-card > span {
|
.keyword-curation-card-meta > span:last-child {
|
||||||
justify-self: start;
|
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
padding: 0 9px;
|
padding: 0 9px;
|
||||||
color: rgba(8, 8, 10, 0.78);
|
color: rgba(8, 8, 10, 0.78);
|
||||||
background: rgba(8, 8, 10, 0.07);
|
background: rgba(8, 8, 10, 0.07);
|
||||||
|
|
@ -3041,6 +3256,7 @@ input {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyword-curation-column-head strong,
|
.keyword-curation-column-head strong,
|
||||||
|
|
@ -3146,10 +3362,11 @@ input {
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyword-curation-card {
|
.keyword-curation-card {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 10px;
|
padding: 10px 42px 10px 10px;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: rgba(255, 255, 255, 0.92);
|
||||||
border: 1px solid rgba(24, 32, 29, 0.08);
|
border: 1px solid rgba(24, 32, 29, 0.08);
|
||||||
border-radius: var(--keyword-curation-radius);
|
border-radius: var(--keyword-curation-radius);
|
||||||
|
|
@ -3157,6 +3374,40 @@ input {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyword-curation-trash {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: grid;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
color: #d52978;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1.5px solid rgba(213, 41, 120, 0.72);
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: 0 8px 20px rgba(232, 62, 140, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 140ms ease,
|
||||||
|
border-color 140ms ease,
|
||||||
|
color 140ms ease,
|
||||||
|
transform 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-curation-trash:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #e83e8c;
|
||||||
|
border-color: #e83e8c;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-curation-trash:focus-visible {
|
||||||
|
outline: 2px solid rgba(232, 62, 140, 0.32);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.keyword-curation-drag-preview {
|
.keyword-curation-drag-preview {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: var(--keyword-drag-x);
|
left: var(--keyword-drag-x);
|
||||||
|
|
@ -3173,6 +3424,29 @@ input {
|
||||||
box-shadow: 0 24px 54px rgba(24, 32, 29, 0.22);
|
box-shadow: 0 24px 54px rgba(24, 32, 29, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyword-curation-card-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-curation-freshness {
|
||||||
|
display: inline-block;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
flex: 0 0 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-curation-freshness.old {
|
||||||
|
background: #d7a11a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-curation-freshness.new {
|
||||||
|
background: #18a957;
|
||||||
|
}
|
||||||
|
|
||||||
.keyword-curation-empty {
|
.keyword-curation-empty {
|
||||||
min-height: 88px;
|
min-height: 88px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -3187,6 +3461,115 @@ input {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(24, 32, 29, 0.035);
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.08);
|
||||||
|
border-radius: var(--keyword-curation-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-head > div:first-child {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-head span,
|
||||||
|
.wordstat-probe-preview-head small,
|
||||||
|
.wordstat-probe-preview-empty,
|
||||||
|
.wordstat-probe-preview-hash {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-head strong {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 920;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-head .ready,
|
||||||
|
.wordstat-probe-preview-head .blocked {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-head .ready {
|
||||||
|
color: #0a6a42;
|
||||||
|
background: rgba(20, 148, 92, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-head .blocked {
|
||||||
|
color: #9c2f2f;
|
||||||
|
background: rgba(210, 66, 66, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-stats,
|
||||||
|
.wordstat-probe-preview-warnings,
|
||||||
|
.wordstat-probe-preview-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-stats span,
|
||||||
|
.wordstat-probe-preview-warnings span,
|
||||||
|
.wordstat-probe-preview-list span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: rgba(8, 8, 10, 0.78);
|
||||||
|
background: rgba(255, 255, 255, 0.74);
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 840;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-warnings span {
|
||||||
|
color: #735018;
|
||||||
|
background: rgba(255, 202, 96, 0.14);
|
||||||
|
border-color: rgba(170, 115, 20, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-warnings span.warning {
|
||||||
|
color: #8d2f24;
|
||||||
|
background: rgba(220, 74, 56, 0.1);
|
||||||
|
border-color: rgba(184, 58, 43, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordstat-probe-preview-list span {
|
||||||
|
color: var(--ink);
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
.keyword-curation-footer {
|
.keyword-curation-footer {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
|
|
@ -3236,6 +3619,228 @@ input {
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.anchor-demand-profile-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(280px, 380px);
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(24, 32, 29, 0.034);
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.07);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor-demand-profile-card > div:first-child {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor-demand-profile-card span {
|
||||||
|
color: #2e5a67;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor-demand-profile-card strong {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor-demand-profile-card small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 740;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor-demand-profile-control {
|
||||||
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
justify-items: stretch;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor-demand-profile-control > span {
|
||||||
|
justify-self: end;
|
||||||
|
padding: 4px 9px;
|
||||||
|
color: #7a1f4d;
|
||||||
|
background: rgba(232, 62, 140, 0.1);
|
||||||
|
border: 1px solid rgba(232, 62, 140, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__control {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 40px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__value,
|
||||||
|
.nodedc-select__toggle {
|
||||||
|
height: 40px;
|
||||||
|
color: rgba(20, 23, 25, 0.86);
|
||||||
|
background: rgba(255, 255, 255, 0.86);
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.08);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92), 0 12px 28px rgba(18, 22, 25, 0.08);
|
||||||
|
outline: none;
|
||||||
|
transition: background 140ms ease, color 140ms ease, box-shadow 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 820;
|
||||||
|
line-height: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__toggle {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__value:hover,
|
||||||
|
.nodedc-select__toggle:hover,
|
||||||
|
.nodedc-select__toggle[aria-expanded="true"] {
|
||||||
|
color: rgba(10, 12, 14, 0.94);
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.96), 0 14px 34px rgba(18, 22, 25, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__toggle:focus,
|
||||||
|
.nodedc-select__toggle:focus-visible,
|
||||||
|
.nodedc-select__value:focus,
|
||||||
|
.nodedc-select__value:focus-visible,
|
||||||
|
.nodedc-select__option:focus,
|
||||||
|
.nodedc-select__option:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__chevron {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
opacity: 0.72;
|
||||||
|
border-right: 2px solid currentColor;
|
||||||
|
border-bottom: 2px solid currentColor;
|
||||||
|
transform: translateY(-2px) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-dropdown-surface.nodedc-select__menu {
|
||||||
|
z-index: 30000;
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
max-height: 232px;
|
||||||
|
padding: 7px;
|
||||||
|
overflow: auto;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.08);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 18px 60px rgba(24, 32, 29, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-dropdown-surface.nodedc-select__menu::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-dropdown-option.nodedc-select__option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: rgba(28, 31, 33, 0.74);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 820;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__option-label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__option-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__option-action {
|
||||||
|
display: grid;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
place-items: center;
|
||||||
|
color: rgba(10, 12, 14, 0.68);
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 140ms ease, color 140ms ease, transform 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__option-action:hover,
|
||||||
|
.nodedc-select__option-action:focus-visible {
|
||||||
|
color: #e83e8c;
|
||||||
|
background: rgba(232, 62, 140, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__option-action[data-disabled="true"] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.46;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-dropdown-option.nodedc-select__option:hover,
|
||||||
|
.nodedc-dropdown-option.nodedc-select__option:focus,
|
||||||
|
.nodedc-dropdown-option.nodedc-select__option[data-selected="true"] {
|
||||||
|
color: rgba(10, 12, 14, 0.95);
|
||||||
|
background: rgba(24, 32, 29, 0.065);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-dropdown-option.nodedc-select__option[data-disabled="true"] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.54;
|
||||||
|
}
|
||||||
|
|
||||||
.anchor-review-workspace-list {
|
.anchor-review-workspace-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
@ -12258,6 +12863,8 @@ textarea:focus {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyword-stage-head,
|
||||||
|
.keyword-anchor-stage-head,
|
||||||
.keyword-curation-board-head,
|
.keyword-curation-board-head,
|
||||||
.keyword-curation-footer {
|
.keyword-curation-footer {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -12271,6 +12878,10 @@ textarea:focus {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.anchor-demand-profile-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.anchor-review-row {
|
.anchor-review-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
@ -13075,9 +13686,9 @@ textarea:focus {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: rgba(10, 12, 14, 0.28);
|
background: rgba(10, 12, 14, 0.14);
|
||||||
backdrop-filter: var(--nodedc-glass-panel-blur);
|
backdrop-filter: blur(18px) saturate(1.04);
|
||||||
-webkit-backdrop-filter: var(--nodedc-glass-panel-blur);
|
-webkit-backdrop-filter: blur(18px) saturate(1.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.seo-project-name-modal {
|
.seo-project-name-modal {
|
||||||
|
|
@ -13148,6 +13759,62 @@ textarea:focus {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seo-keyword-suppress-modal {
|
||||||
|
width: min(36rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-keyword-context-modal {
|
||||||
|
width: min(38rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-keyword-suppress-preview {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.88rem;
|
||||||
|
background: rgba(255, 255, 255, 0.74);
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.07);
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 12px 32px rgba(24, 32, 29, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-keyword-suppress-preview span {
|
||||||
|
justify-self: start;
|
||||||
|
min-height: 22px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 9px;
|
||||||
|
color: rgba(8, 8, 10, 0.78);
|
||||||
|
background: rgba(8, 8, 10, 0.07);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-keyword-suppress-preview strong {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.16;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-keyword-suppress-preview small,
|
||||||
|
.seo-keyword-suppress-note {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 740;
|
||||||
|
line-height: 1.42;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-project-name-modal-actions .danger-action {
|
||||||
|
background: #e83e8c !important;
|
||||||
|
border-color: #e83e8c !important;
|
||||||
|
box-shadow: 0 14px 30px rgba(232, 62, 140, 0.22) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.seo-project-name-modal-actions {
|
.seo-project-name-modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
@ -14297,7 +14964,7 @@ body:has(.seo-launcher-shell) {
|
||||||
color: rgba(8, 8, 10, 0.72);
|
color: rgba(8, 8, 10, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
.seo-stage-5 .wordstat-quota-strip {
|
.seo-stage-5 .seo-work-panel-toolbar > .wordstat-quota-strip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
@ -14305,6 +14972,76 @@ body:has(.seo-launcher-shell) {
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-global-wordstat-quota {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-global-wordstat-quota .wordstat-quota-strip {
|
||||||
|
width: clamp(14rem, 22vw, 19rem);
|
||||||
|
min-width: 12.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-context-toolbar {
|
||||||
|
display: inline-grid;
|
||||||
|
grid-template-columns: minmax(12rem, 17rem) 2.42rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.38rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-context-profile-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-context-toolbar .nodedc-select__control {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 2.42rem;
|
||||||
|
gap: 0.38rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-context-toolbar .nodedc-select__value,
|
||||||
|
.keyword-context-toolbar .nodedc-select__toggle {
|
||||||
|
height: 2.42rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-context-toolbar > button {
|
||||||
|
display: grid;
|
||||||
|
width: 2.42rem;
|
||||||
|
height: 2.42rem;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.86);
|
||||||
|
color: rgba(10, 12, 14, 0.78);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 10px 28px rgba(24, 32, 29, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-context-toolbar > button:hover:not(:disabled),
|
||||||
|
.keyword-context-toolbar > button.saving {
|
||||||
|
background: #e83e8c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-context-toolbar > button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.68;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-context-profile-menu {
|
||||||
|
min-width: 25rem;
|
||||||
|
max-width: min(31rem, calc(100vw - 1.5rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-context-profile-menu .nodedc-select__option {
|
||||||
|
min-height: 48px;
|
||||||
|
padding-inline: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.wordstat-quota-head,
|
.wordstat-quota-head,
|
||||||
.wordstat-quota-meta {
|
.wordstat-quota-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -14419,8 +15156,30 @@ body:has(.seo-launcher-shell) {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seo-topbar-status-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 2.34rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.38rem;
|
||||||
|
padding: 0 0.86rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(232, 62, 140, 0.09);
|
||||||
|
color: #bc2f72;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 860;
|
||||||
|
letter-spacing: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-topbar-status-pill.blocked {
|
||||||
|
background: rgba(139, 47, 47, 0.07);
|
||||||
|
color: #8b2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1360px) {
|
@media (max-width: 1360px) {
|
||||||
.seo-stage-5 .seo-topbar-keyword-actions > button {
|
.seo-stage-5 .seo-topbar-keyword-actions > button,
|
||||||
|
.seo-stage-5 .seo-topbar-keyword-actions > .seo-topbar-status-pill {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
min-width: 3rem;
|
min-width: 3rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -14428,7 +15187,8 @@ body:has(.seo-launcher-shell) {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seo-stage-5 .seo-topbar-keyword-actions > button svg {
|
.seo-stage-5 .seo-topbar-keyword-actions > button svg,
|
||||||
|
.seo-stage-5 .seo-topbar-keyword-actions > .seo-topbar-status-pill svg {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -15481,6 +16241,7 @@ body:has(.seo-launcher-shell) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.seo-launcher-shell.project-open .seo-stage-rail {
|
.seo-launcher-shell.project-open .seo-stage-rail {
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
border: 1px solid var(--nodedc-glass-outline) !important;
|
border: 1px solid var(--nodedc-glass-outline) !important;
|
||||||
background: var(--nodedc-glass-panel-surface-strong) !important;
|
background: var(--nodedc-glass-panel-surface-strong) !important;
|
||||||
|
|
@ -15511,6 +16272,43 @@ body:has(.seo-launcher-shell) {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seo-launcher-shell.project-open .seo-stage-rail-list {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-stage-rail-wordstat {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.72rem 0.78rem;
|
||||||
|
border: 1px solid rgba(24, 32, 29, 0.08);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.76);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 14px 34px rgba(24, 32, 29, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-stage-rail-wordstat .wordstat-quota-strip {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-stage-rail-wordstat .wordstat-quota-head {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-stage-rail-wordstat .wordstat-quota-meta {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.24rem;
|
||||||
|
font-size: 0.58rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-stage-rail-wordstat .wordstat-quota-meta span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.seo-stage-rail-head {
|
.seo-stage-rail-head {
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
grid-template-columns: minmax(0, 1fr) 2.38rem;
|
grid-template-columns: minmax(0, 1fr) 2.38rem;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue