@focus-reactive/payload-plugin-seo 1.6.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/dist/admin.css +232 -17
- package/dist/components/SeoButton/SeoButtonInner.d.ts.map +1 -1
- package/dist/components/SeoButton/SeoButtonInner.js +46 -16
- package/dist/components/SeoButton/SeoButtonInner.js.map +1 -1
- package/dist/components/SeoDrawer/build-analysis-input.d.ts +2 -2
- package/dist/components/SeoDrawer/build-analysis-input.d.ts.map +1 -1
- package/dist/components/SeoDrawer/build-analysis-input.js +1 -1
- package/dist/components/SeoDrawer/build-analysis-input.js.map +1 -1
- package/dist/components/SeoDrawer/buildInput.d.ts +3 -3
- package/dist/components/SeoDrawer/buildInput.d.ts.map +1 -1
- package/dist/components/SeoDrawer/buildInput.js +3 -2
- package/dist/components/SeoDrawer/buildInput.js.map +1 -1
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssueBanner.d.ts +7 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssueBanner.d.ts.map +1 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssueBanner.js +26 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssueBanner.js.map +1 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssuePills.d.ts +8 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssuePills.d.ts.map +1 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssuePills.js +17 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssuePills.js.map +1 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/HeadingTreeRow.d.ts.map +1 -1
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/HeadingTreeRow.js +2 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/HeadingTreeRow.js.map +1 -1
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/headingTreeView.d.ts +1 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/headingTreeView.d.ts.map +1 -1
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/headingTreeView.js +14 -1
- package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/headingTreeView.js.map +1 -1
- package/dist/components/SeoDrawer/components/HeadingsSection/headingIssueCopy.d.ts +7 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/headingIssueCopy.d.ts.map +1 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/headingIssueCopy.js +26 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/headingIssueCopy.js.map +1 -0
- package/dist/components/SeoDrawer/components/HeadingsSection/index.d.ts.map +1 -1
- package/dist/components/SeoDrawer/components/HeadingsSection/index.js +17 -6
- package/dist/components/SeoDrawer/components/HeadingsSection/index.js.map +1 -1
- package/dist/components/SeoDrawer/components/SerpPreview/highlight-keyphrase.d.ts +1 -1
- package/dist/components/SeoDrawer/components/SerpPreview/highlight-keyphrase.d.ts.map +1 -1
- package/dist/components/SeoDrawer/components/SerpPreview/highlight-keyphrase.js +10 -3
- package/dist/components/SeoDrawer/components/SerpPreview/highlight-keyphrase.js.map +1 -1
- package/dist/components/SeoDrawer/components/SerpPreview/index.d.ts +2 -1
- package/dist/components/SeoDrawer/components/SerpPreview/index.d.ts.map +1 -1
- package/dist/components/SeoDrawer/components/SerpPreview/index.js +8 -2
- package/dist/components/SeoDrawer/components/SerpPreview/index.js.map +1 -1
- package/dist/components/SeoDrawer/components/SerpPreview/variants.d.ts +6 -6
- package/dist/components/SeoDrawer/components/TabWrapper.d.ts +6 -0
- package/dist/components/SeoDrawer/components/TabWrapper.d.ts.map +1 -0
- package/dist/components/SeoDrawer/components/TabWrapper.js +8 -0
- package/dist/components/SeoDrawer/components/TabWrapper.js.map +1 -0
- package/dist/components/SeoDrawer/index.d.ts +11 -4
- package/dist/components/SeoDrawer/index.d.ts.map +1 -1
- package/dist/components/SeoDrawer/index.js +38 -16
- package/dist/components/SeoDrawer/index.js.map +1 -1
- package/dist/components/SeoDrawer/keyphraseState.d.ts +16 -0
- package/dist/components/SeoDrawer/keyphraseState.d.ts.map +1 -0
- package/dist/components/SeoDrawer/keyphraseState.js +73 -0
- package/dist/components/SeoDrawer/keyphraseState.js.map +1 -0
- package/dist/components/SeoDrawer/keyphraseStorage.d.ts +5 -0
- package/dist/components/SeoDrawer/keyphraseStorage.d.ts.map +1 -0
- package/dist/components/SeoDrawer/keyphraseStorage.js +60 -0
- package/dist/components/SeoDrawer/keyphraseStorage.js.map +1 -0
- package/dist/components/SeoDrawer/tabs/InclusiveTab.d.ts.map +1 -1
- package/dist/components/SeoDrawer/tabs/InclusiveTab.js +3 -2
- package/dist/components/SeoDrawer/tabs/InclusiveTab.js.map +1 -1
- package/dist/components/SeoDrawer/tabs/KeyphraseTab.d.ts +14 -7
- package/dist/components/SeoDrawer/tabs/KeyphraseTab.d.ts.map +1 -1
- package/dist/components/SeoDrawer/tabs/KeyphraseTab.js +110 -69
- package/dist/components/SeoDrawer/tabs/KeyphraseTab.js.map +1 -1
- package/dist/components/SeoDrawer/tabs/OnPageTab.d.ts.map +1 -1
- package/dist/components/SeoDrawer/tabs/OnPageTab.js +3 -2
- package/dist/components/SeoDrawer/tabs/OnPageTab.js.map +1 -1
- package/dist/components/SeoDrawer/tabs/ReadabilityTab.d.ts.map +1 -1
- package/dist/components/SeoDrawer/tabs/ReadabilityTab.js +3 -2
- package/dist/components/SeoDrawer/tabs/ReadabilityTab.js.map +1 -1
- package/dist/components/SeoDrawer/tabs/SerpTab.d.ts +2 -1
- package/dist/components/SeoDrawer/tabs/SerpTab.d.ts.map +1 -1
- package/dist/components/SeoDrawer/tabs/SerpTab.js +14 -3
- package/dist/components/SeoDrawer/tabs/SerpTab.js.map +1 -1
- package/dist/components/SeoDrawer/tabs/VitalsTab.d.ts.map +1 -1
- package/dist/components/SeoDrawer/tabs/VitalsTab.js +7 -3
- package/dist/components/SeoDrawer/tabs/VitalsTab.js.map +1 -1
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseCard.d.ts +21 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseCard.d.ts.map +1 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseCard.js +38 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseCard.js.map +1 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseDetail.d.ts +25 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseDetail.d.ts.map +1 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseDetail.js +127 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseDetail.js.map +1 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.d.ts +12 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.d.ts.map +1 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.js +54 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.js.map +1 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/SynonymsField.d.ts +7 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/SynonymsField.d.ts.map +1 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/SynonymsField.js +93 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/SynonymsField.js.map +1 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/icons.d.ts +2 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/icons.d.ts.map +1 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/icons.js +7 -0
- package/dist/components/SeoDrawer/tabs/keyphrase/icons.js.map +1 -0
- package/dist/components/SeoDrawer/useAnalysis.d.ts +0 -1
- package/dist/components/SeoDrawer/useAnalysis.d.ts.map +1 -1
- package/dist/components/SeoDrawer/useAnalysis.js +0 -3
- package/dist/components/SeoDrawer/useAnalysis.js.map +1 -1
- package/dist/components/SeoDrawer/useKeyphrases.d.ts +18 -0
- package/dist/components/SeoDrawer/useKeyphrases.d.ts.map +1 -0
- package/dist/components/SeoDrawer/useKeyphrases.js +78 -0
- package/dist/components/SeoDrawer/useKeyphrases.js.map +1 -0
- package/dist/components/SeoDrawer/useLiveDocument.d.ts +4 -3
- package/dist/components/SeoDrawer/useLiveDocument.d.ts.map +1 -1
- package/dist/components/SeoDrawer/useLiveDocument.js +17 -9
- package/dist/components/SeoDrawer/useLiveDocument.js.map +1 -1
- package/dist/components/SeoDrawer/variants.d.ts +1 -1
- package/dist/components/SeoDrawer/variants.d.ts.map +1 -1
- package/dist/components/SeoDrawer/variants.js +2 -1
- package/dist/components/SeoDrawer/variants.js.map +1 -1
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +2 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/engine/buildPaper.d.ts +2 -2
- package/dist/engine/buildPaper.d.ts.map +1 -1
- package/dist/engine/buildPaper.js +4 -2
- package/dist/engine/buildPaper.js.map +1 -1
- package/dist/engine/runAnalysis/index.d.ts.map +1 -1
- package/dist/engine/runAnalysis/index.js +7 -0
- package/dist/engine/runAnalysis/index.js.map +1 -1
- package/dist/engine/runAnalysis/services/derive-related.d.ts +4 -0
- package/dist/engine/runAnalysis/services/derive-related.d.ts.map +1 -0
- package/dist/engine/runAnalysis/services/derive-related.js +16 -0
- package/dist/engine/runAnalysis/services/derive-related.js.map +1 -0
- package/dist/engine/runAnalysis/services/derive-vitals/heading-issues.d.ts +8 -0
- package/dist/engine/runAnalysis/services/derive-vitals/heading-issues.d.ts.map +1 -0
- package/dist/engine/runAnalysis/services/derive-vitals/heading-issues.js +37 -0
- package/dist/engine/runAnalysis/services/derive-vitals/heading-issues.js.map +1 -0
- package/dist/engine/runAnalysis/services/derive-vitals/heading-tree.d.ts.map +1 -1
- package/dist/engine/runAnalysis/services/derive-vitals/heading-tree.js +7 -3
- package/dist/engine/runAnalysis/services/derive-vitals/heading-tree.js.map +1 -1
- package/dist/engine/types/analysis.d.ts +23 -0
- package/dist/engine/types/analysis.d.ts.map +1 -1
- package/dist/ui/Button.d.ts +9 -0
- package/dist/ui/Button.d.ts.map +1 -0
- package/dist/ui/Button.js +34 -0
- package/dist/ui/Button.js.map +1 -0
- package/dist/ui/ScoreRing.d.ts +17 -4
- package/dist/ui/ScoreRing.d.ts.map +1 -1
- package/dist/ui/ScoreRing.js +38 -14
- package/dist/ui/ScoreRing.js.map +1 -1
- package/package.json +1 -1
- package/dist/components/SeoDrawer/keyphrasePending.d.ts +0 -2
- package/dist/components/SeoDrawer/keyphrasePending.d.ts.map +0 -1
- package/dist/components/SeoDrawer/keyphrasePending.js +0 -9
- package/dist/components/SeoDrawer/keyphrasePending.js.map +0 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AlertTriangle, Loader2 } from "lucide-react";
|
|
4
|
+
import { useId, useState } from "react";
|
|
5
|
+
import { Button } from "../../../../ui/Button";
|
|
6
|
+
import { CheckRow } from "../../../../ui/CheckRow";
|
|
7
|
+
import { FilterPills } from "../../../../ui/FilterPills";
|
|
8
|
+
import { Pill } from "../../../../ui/Pill";
|
|
9
|
+
import { SectionCard } from "../../../../ui/SectionCard";
|
|
10
|
+
import { TabHeader } from "../../../../ui/TabHeader";
|
|
11
|
+
import { cn } from "../../../../utils/style";
|
|
12
|
+
import { Target, Trash2 } from "./icons";
|
|
13
|
+
import { SynonymsField } from "./SynonymsField";
|
|
14
|
+
function Analysis({ analysis }) {
|
|
15
|
+
const [filter, setFilter] = useState("all");
|
|
16
|
+
if (analysis.kind === "analyzing") {
|
|
17
|
+
return /* @__PURE__ */ jsxs(
|
|
18
|
+
"div",
|
|
19
|
+
{
|
|
20
|
+
"aria-live": "polite",
|
|
21
|
+
className: "flex items-center gap-[9px] text-neutral-500 text-[13px] py-[12px]",
|
|
22
|
+
role: "status",
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ jsx(Loader2, { "aria-hidden": "true", className: "w-[15px] h-[15px] animate-spin" }),
|
|
25
|
+
"Analyzing keyphrase\u2026"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (analysis.kind === "hint") {
|
|
31
|
+
return /* @__PURE__ */ jsx("div", { className: "text-neutral-500 text-[12px] py-[6px]", children: analysis.message });
|
|
32
|
+
}
|
|
33
|
+
const { result, dim } = analysis;
|
|
34
|
+
const passing = result.checks.filter((check) => check.status === "good").length;
|
|
35
|
+
const visible = result.checks.filter((check) => filter === "all" || check.status === filter);
|
|
36
|
+
return /* @__PURE__ */ jsxs(
|
|
37
|
+
"div",
|
|
38
|
+
{
|
|
39
|
+
className: cn(
|
|
40
|
+
"flex flex-col gap-[13px] transition-opacity",
|
|
41
|
+
dim && "opacity-40 pointer-events-none"
|
|
42
|
+
),
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ jsx(
|
|
45
|
+
TabHeader,
|
|
46
|
+
{
|
|
47
|
+
score: result.ringScore,
|
|
48
|
+
status: result.status,
|
|
49
|
+
subtitle: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
50
|
+
passing,
|
|
51
|
+
" / ",
|
|
52
|
+
result.checks.length,
|
|
53
|
+
" checks passing"
|
|
54
|
+
] }),
|
|
55
|
+
title: "Keyphrase optimization"
|
|
56
|
+
}
|
|
57
|
+
),
|
|
58
|
+
/* @__PURE__ */ jsxs(SectionCard, { title: "Checks", widget: /* @__PURE__ */ jsx(Pill, { variant: "neutral", children: result.checks.length }), children: [
|
|
59
|
+
/* @__PURE__ */ jsx(FilterPills, { checks: result.checks, onChange: setFilter, value: filter }),
|
|
60
|
+
visible.map((check) => /* @__PURE__ */ jsx(CheckRow, { check }, check.id))
|
|
61
|
+
] })
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
function KeyphraseDetail({
|
|
67
|
+
entry,
|
|
68
|
+
isFocus,
|
|
69
|
+
duplicate,
|
|
70
|
+
analysis,
|
|
71
|
+
onTextChange,
|
|
72
|
+
onAddSynonym,
|
|
73
|
+
onRemoveSynonym,
|
|
74
|
+
onSetFocus,
|
|
75
|
+
onRemove
|
|
76
|
+
}) {
|
|
77
|
+
const textInputId = useId();
|
|
78
|
+
const label = isFocus ? "Focus keyphrase" : "Related keyphrase";
|
|
79
|
+
return /* @__PURE__ */ jsxs("section", { className: "flex-1 min-w-0 px-[15px] py-[18px] flex flex-col gap-[13px] overflow-auto", children: [
|
|
80
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-[9px]", children: [
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
"label",
|
|
83
|
+
{
|
|
84
|
+
className: "block text-[10px] uppercase tracking-[0.05em] text-neutral-500 mb-[6px]",
|
|
85
|
+
htmlFor: textInputId,
|
|
86
|
+
children: label
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
/* @__PURE__ */ jsx(
|
|
90
|
+
"input",
|
|
91
|
+
{
|
|
92
|
+
autoComplete: "off",
|
|
93
|
+
className: "w-full border border-neutral-200 rounded-rs px-[12px] py-[10px] text-[13px] text-neutral-800 bg-neutral-0 outline-none focus:border-neutral-800",
|
|
94
|
+
id: textInputId,
|
|
95
|
+
onChange: (e) => onTextChange(e.target.value),
|
|
96
|
+
placeholder: isFocus ? "Type your primary keyphrase" : "Type a related keyphrase",
|
|
97
|
+
type: "text",
|
|
98
|
+
value: entry.text
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] }),
|
|
102
|
+
duplicate && /* @__PURE__ */ jsxs(
|
|
103
|
+
"div",
|
|
104
|
+
{
|
|
105
|
+
"aria-live": "polite",
|
|
106
|
+
className: "text-[11.5px] text-seo-warn bg-seo-warn-100 rounded-rs px-[10px] py-[8px] flex items-center gap-[7px]",
|
|
107
|
+
role: "status",
|
|
108
|
+
children: [
|
|
109
|
+
/* @__PURE__ */ jsx(AlertTriangle, { "aria-hidden": "true", className: "w-[15px] h-[15px] flex-none" }),
|
|
110
|
+
"\u201C",
|
|
111
|
+
entry.text.trim(),
|
|
112
|
+
"\u201D is already in your keyphrase list."
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
),
|
|
116
|
+
/* @__PURE__ */ jsx("div", { className: "mb-[9px]", children: /* @__PURE__ */ jsx(SynonymsField, { onAdd: onAddSynonym, onRemove: onRemoveSynonym, synonyms: entry.synonyms }) }),
|
|
117
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-[8px] items-center", children: [
|
|
118
|
+
!isFocus && /* @__PURE__ */ jsx(Button, { icon: /* @__PURE__ */ jsx(Target, { "aria-hidden": "true" }), onClick: onSetFocus, variant: "primary", children: "Set as focus" }),
|
|
119
|
+
/* @__PURE__ */ jsx(Button, { icon: /* @__PURE__ */ jsx(Trash2, { "aria-hidden": "true" }), onClick: onRemove, variant: "error", children: "Remove" })
|
|
120
|
+
] }),
|
|
121
|
+
/* @__PURE__ */ jsx(Analysis, { analysis })
|
|
122
|
+
] });
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
KeyphraseDetail
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=KeyphraseDetail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/components/SeoDrawer/tabs/keyphrase/KeyphraseDetail.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertTriangle, Loader2 } from \"lucide-react\";\nimport { useId, useState } from \"react\";\nimport type { CategoryResult } from \"../../../../engine/types/analysis\";\nimport { Button } from \"../../../../ui/Button\";\nimport { CheckRow } from \"../../../../ui/CheckRow\";\nimport type { Filter } from \"../../../../ui/FilterPills\";\nimport { FilterPills } from \"../../../../ui/FilterPills\";\nimport { Pill } from \"../../../../ui/Pill\";\nimport { SectionCard } from \"../../../../ui/SectionCard\";\nimport { TabHeader } from \"../../../../ui/TabHeader\";\nimport { cn } from \"../../../../utils/style\";\nimport type { KeyphraseEntry } from \"../../keyphraseState\";\nimport { Target, Trash2 } from \"./icons\";\nimport { SynonymsField } from \"./SynonymsField\";\n\nexport type DetailAnalysis =\n | { kind: \"metrics\"; result: CategoryResult; dim: boolean }\n | { kind: \"analyzing\" }\n | { kind: \"hint\"; message: string };\n\nexport interface KeyphraseDetailProps {\n entry: KeyphraseEntry;\n isFocus: boolean;\n duplicate: boolean;\n analysis: DetailAnalysis;\n onTextChange: (text: string) => void;\n onAddSynonym: (syn: string) => void;\n onRemoveSynonym: (index: number) => void;\n onSetFocus: () => void;\n onRemove: () => void;\n}\n\ninterface AnalysisProps {\n analysis: DetailAnalysis;\n}\n\nfunction Analysis({ analysis }: AnalysisProps) {\n const [filter, setFilter] = useState<Filter>(\"all\");\n\n if (analysis.kind === \"analyzing\") {\n return (\n <div\n aria-live=\"polite\"\n className=\"flex items-center gap-[9px] text-neutral-500 text-[13px] py-[12px]\"\n role=\"status\"\n >\n <Loader2 aria-hidden=\"true\" className=\"w-[15px] h-[15px] animate-spin\" />\n Analyzing keyphrase…\n </div>\n );\n }\n\n if (analysis.kind === \"hint\") {\n return <div className=\"text-neutral-500 text-[12px] py-[6px]\">{analysis.message}</div>;\n }\n\n const { result, dim } = analysis;\n const passing = result.checks.filter((check) => check.status === \"good\").length;\n const visible = result.checks.filter((check) => filter === \"all\" || check.status === filter);\n\n return (\n <div\n className={cn(\n \"flex flex-col gap-[13px] transition-opacity\",\n dim && \"opacity-40 pointer-events-none\"\n )}\n >\n <TabHeader\n score={result.ringScore}\n status={result.status}\n subtitle={\n <>\n {passing} / {result.checks.length} checks passing\n </>\n }\n title=\"Keyphrase optimization\"\n />\n\n <SectionCard title=\"Checks\" widget={<Pill variant=\"neutral\">{result.checks.length}</Pill>}>\n <FilterPills checks={result.checks} onChange={setFilter} value={filter} />\n {visible.map((check) => (\n <CheckRow check={check} key={check.id} />\n ))}\n </SectionCard>\n </div>\n );\n}\n\nexport function KeyphraseDetail({\n entry,\n isFocus,\n duplicate,\n analysis,\n onTextChange,\n onAddSynonym,\n onRemoveSynonym,\n onSetFocus,\n onRemove,\n}: KeyphraseDetailProps) {\n const textInputId = useId();\n const label = isFocus ? \"Focus keyphrase\" : \"Related keyphrase\";\n\n return (\n <section className=\"flex-1 min-w-0 px-[15px] py-[18px] flex flex-col gap-[13px] overflow-auto\">\n <div className=\"mb-[9px]\">\n <label\n className=\"block text-[10px] uppercase tracking-[0.05em] text-neutral-500 mb-[6px]\"\n htmlFor={textInputId}\n >\n {label}\n </label>\n <input\n autoComplete=\"off\"\n className=\"w-full border border-neutral-200 rounded-rs px-[12px] py-[10px] text-[13px] text-neutral-800 bg-neutral-0 outline-none focus:border-neutral-800\"\n id={textInputId}\n onChange={(e) => onTextChange(e.target.value)}\n placeholder={isFocus ? \"Type your primary keyphrase\" : \"Type a related keyphrase\"}\n type=\"text\"\n value={entry.text}\n />\n </div>\n\n {duplicate && (\n <div\n aria-live=\"polite\"\n className=\"text-[11.5px] text-seo-warn bg-seo-warn-100 rounded-rs px-[10px] py-[8px] flex items-center gap-[7px]\"\n role=\"status\"\n >\n <AlertTriangle aria-hidden=\"true\" className=\"w-[15px] h-[15px] flex-none\" />\n “{entry.text.trim()}” is already in your keyphrase list.\n </div>\n )}\n\n <div className=\"mb-[9px]\">\n <SynonymsField onAdd={onAddSynonym} onRemove={onRemoveSynonym} synonyms={entry.synonyms} />\n </div>\n\n <div className=\"flex gap-[8px] items-center\">\n {!isFocus && (\n <Button icon={<Target aria-hidden=\"true\" />} onClick={onSetFocus} variant=\"primary\">\n Set as focus\n </Button>\n )}\n <Button icon={<Trash2 aria-hidden=\"true\" />} onClick={onRemove} variant=\"error\">\n Remove\n </Button>\n </div>\n\n <Analysis analysis={analysis} />\n </section>\n );\n}\n"],"mappings":";AA2CM,SA8BI,UAzBF,KALF;AAzCN,SAAS,eAAe,eAAe;AACvC,SAAS,OAAO,gBAAgB;AAEhC,SAAS,cAAc;AACvB,SAAS,gBAAgB;AAEzB,SAAS,mBAAmB;AAC5B,SAAS,YAAY;AACrB,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB;AAC1B,SAAS,UAAU;AAEnB,SAAS,QAAQ,cAAc;AAC/B,SAAS,qBAAqB;AAuB9B,SAAS,SAAS,EAAE,SAAS,GAAkB;AAC7C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAiB,KAAK;AAElD,MAAI,SAAS,SAAS,aAAa;AACjC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAU;AAAA,QACV,MAAK;AAAA,QAEL;AAAA,8BAAC,WAAQ,eAAY,QAAO,WAAU,kCAAiC;AAAA,UAAE;AAAA;AAAA;AAAA,IAE3E;AAAA,EAEJ;AAEA,MAAI,SAAS,SAAS,QAAQ;AAC5B,WAAO,oBAAC,SAAI,WAAU,yCAAyC,mBAAS,SAAQ;AAAA,EAClF;AAEA,QAAM,EAAE,QAAQ,IAAI,IAAI;AACxB,QAAM,UAAU,OAAO,OAAO,OAAO,CAAC,UAAU,MAAM,WAAW,MAAM,EAAE;AACzE,QAAM,UAAU,OAAO,OAAO,OAAO,CAAC,UAAU,WAAW,SAAS,MAAM,WAAW,MAAM;AAE3F,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,OAAO;AAAA,MACT;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,QAAQ,OAAO;AAAA,YACf,UACE,iCACG;AAAA;AAAA,cAAQ;AAAA,cAAI,OAAO,OAAO;AAAA,cAAO;AAAA,eACpC;AAAA,YAEF,OAAM;AAAA;AAAA,QACR;AAAA,QAEA,qBAAC,eAAY,OAAM,UAAS,QAAQ,oBAAC,QAAK,SAAQ,WAAW,iBAAO,OAAO,QAAO,GAChF;AAAA,8BAAC,eAAY,QAAQ,OAAO,QAAQ,UAAU,WAAW,OAAO,QAAQ;AAAA,UACvE,QAAQ,IAAI,CAAC,UACZ,oBAAC,YAAS,SAAmB,MAAM,EAAI,CACxC;AAAA,WACH;AAAA;AAAA;AAAA,EACF;AAEJ;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,cAAc,MAAM;AAC1B,QAAM,QAAQ,UAAU,oBAAoB;AAE5C,SACE,qBAAC,aAAQ,WAAU,6EACjB;AAAA,yBAAC,SAAI,WAAU,YACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,SAAS;AAAA,UAER;AAAA;AAAA,MACH;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,cAAa;AAAA,UACb,WAAU;AAAA,UACV,IAAI;AAAA,UACJ,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,KAAK;AAAA,UAC5C,aAAa,UAAU,gCAAgC;AAAA,UACvD,MAAK;AAAA,UACL,OAAO,MAAM;AAAA;AAAA,MACf;AAAA,OACF;AAAA,IAEC,aACC;AAAA,MAAC;AAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAU;AAAA,QACV,MAAK;AAAA,QAEL;AAAA,8BAAC,iBAAc,eAAY,QAAO,WAAU,+BAA8B;AAAA,UAAE;AAAA,UACpE,MAAM,KAAK,KAAK;AAAA,UAAE;AAAA;AAAA;AAAA,IAC5B;AAAA,IAGF,oBAAC,SAAI,WAAU,YACb,8BAAC,iBAAc,OAAO,cAAc,UAAU,iBAAiB,UAAU,MAAM,UAAU,GAC3F;AAAA,IAEA,qBAAC,SAAI,WAAU,+BACZ;AAAA,OAAC,WACA,oBAAC,UAAO,MAAM,oBAAC,UAAO,eAAY,QAAO,GAAI,SAAS,YAAY,SAAQ,WAAU,0BAEpF;AAAA,MAEF,oBAAC,UAAO,MAAM,oBAAC,UAAO,eAAY,QAAO,GAAI,SAAS,UAAU,SAAQ,SAAQ,oBAEhF;AAAA,OACF;AAAA,IAEA,oBAAC,YAAS,UAAoB;AAAA,KAChC;AAEJ;","names":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { KeyphraseEntry } from "../../keyphraseState";
|
|
2
|
+
import type { CardState } from "./KeyphraseCard";
|
|
3
|
+
interface KeyphraseRailProps {
|
|
4
|
+
entries: KeyphraseEntry[];
|
|
5
|
+
selectedId: string | null;
|
|
6
|
+
stateFor: (entry: KeyphraseEntry) => CardState;
|
|
7
|
+
onSelect: (id: string) => void;
|
|
8
|
+
onAdd: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function KeyphraseRail({ entries, selectedId, stateFor, onSelect, onAdd, }: KeyphraseRailProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=KeyphraseRail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KeyphraseRail.d.ts","sourceRoot":"","sources":["../../../../../src/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,UAAU,kBAAkB;IAC1B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,SAAS,CAAC;IAC/C,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,KAAK,GACN,EAAE,kBAAkB,2CAsCpB"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Plus } from "lucide-react";
|
|
4
|
+
import { MAX_KEYPHRASES } from "../../../../constants";
|
|
5
|
+
import { firstEmptyId } from "../../keyphraseState";
|
|
6
|
+
import { KeyphraseCard } from "./KeyphraseCard";
|
|
7
|
+
function KeyphraseRail({
|
|
8
|
+
entries,
|
|
9
|
+
selectedId,
|
|
10
|
+
stateFor,
|
|
11
|
+
onSelect,
|
|
12
|
+
onAdd
|
|
13
|
+
}) {
|
|
14
|
+
const atMax = entries.length >= MAX_KEYPHRASES;
|
|
15
|
+
const hasEmpty = Boolean(firstEmptyId(entries));
|
|
16
|
+
const addDisabled = atMax || hasEmpty;
|
|
17
|
+
return /* @__PURE__ */ jsx("aside", { className: "w-[264px] flex-none border-r border-neutral-200 bg-neutral-50", children: /* @__PURE__ */ jsxs("div", { className: "sticky top-0 px-[9px] py-[18px] flex flex-col gap-[5px]", children: [
|
|
18
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-[7px] pt-[5px] pb-[3px]", children: [
|
|
19
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[10px] uppercase tracking-[0.05em] text-neutral-500 font-bold", children: [
|
|
20
|
+
"Keyphrases \xB7 ",
|
|
21
|
+
entries.length,
|
|
22
|
+
"/",
|
|
23
|
+
MAX_KEYPHRASES
|
|
24
|
+
] }),
|
|
25
|
+
/* @__PURE__ */ jsx(
|
|
26
|
+
"button",
|
|
27
|
+
{
|
|
28
|
+
type: "button",
|
|
29
|
+
"aria-label": "Add related keyphrase",
|
|
30
|
+
title: atMax ? `Up to ${MAX_KEYPHRASES} keyphrases` : "Add related keyphrase",
|
|
31
|
+
disabled: addDisabled,
|
|
32
|
+
onClick: onAdd,
|
|
33
|
+
className: "w-[26px] h-[26px] rounded-rs border border-neutral-200 grid place-items-center text-neutral-600 bg-neutral-0 hover:border-neutral-800 hover:text-neutral-800 disabled:opacity-35 disabled:cursor-not-allowed [&_svg]:size-[15px]",
|
|
34
|
+
children: /* @__PURE__ */ jsx(Plus, { "aria-hidden": "true" })
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
] }),
|
|
38
|
+
entries.map((entry, i) => /* @__PURE__ */ jsx(
|
|
39
|
+
KeyphraseCard,
|
|
40
|
+
{
|
|
41
|
+
entry,
|
|
42
|
+
isFocus: i === 0,
|
|
43
|
+
selected: entry.id === selectedId,
|
|
44
|
+
state: stateFor(entry),
|
|
45
|
+
onSelect
|
|
46
|
+
},
|
|
47
|
+
entry.id
|
|
48
|
+
))
|
|
49
|
+
] }) });
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
KeyphraseRail
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=KeyphraseRail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.tsx"],"sourcesContent":["\"use client\";\n\nimport { Plus } from \"lucide-react\";\nimport { MAX_KEYPHRASES } from \"../../../../constants\";\nimport type { KeyphraseEntry } from \"../../keyphraseState\";\nimport { firstEmptyId } from \"../../keyphraseState\";\nimport { KeyphraseCard } from \"./KeyphraseCard\";\nimport type { CardState } from \"./KeyphraseCard\";\n\ninterface KeyphraseRailProps {\n entries: KeyphraseEntry[];\n selectedId: string | null;\n stateFor: (entry: KeyphraseEntry) => CardState;\n onSelect: (id: string) => void;\n onAdd: () => void;\n}\n\nexport function KeyphraseRail({\n entries,\n selectedId,\n stateFor,\n onSelect,\n onAdd,\n}: KeyphraseRailProps) {\n const atMax = entries.length >= MAX_KEYPHRASES;\n const hasEmpty = Boolean(firstEmptyId(entries));\n const addDisabled = atMax || hasEmpty;\n\n return (\n <aside className=\"w-[264px] flex-none border-r border-neutral-200 bg-neutral-50\">\n <div className=\"sticky top-0 px-[9px] py-[18px] flex flex-col gap-[5px]\">\n <div className=\"flex items-center justify-between px-[7px] pt-[5px] pb-[3px]\">\n <span className=\"text-[10px] uppercase tracking-[0.05em] text-neutral-500 font-bold\">\n Keyphrases · {entries.length}/{MAX_KEYPHRASES}\n </span>\n\n <button\n type=\"button\"\n aria-label=\"Add related keyphrase\"\n title={atMax ? `Up to ${MAX_KEYPHRASES} keyphrases` : \"Add related keyphrase\"}\n disabled={addDisabled}\n onClick={onAdd}\n className=\"w-[26px] h-[26px] rounded-rs border border-neutral-200 grid place-items-center text-neutral-600 bg-neutral-0 hover:border-neutral-800 hover:text-neutral-800 disabled:opacity-35 disabled:cursor-not-allowed [&_svg]:size-[15px]\"\n >\n <Plus aria-hidden=\"true\" />\n </button>\n </div>\n\n {entries.map((entry, i) => (\n <KeyphraseCard\n key={entry.id}\n entry={entry}\n isFocus={i === 0}\n selected={entry.id === selectedId}\n state={stateFor(entry)}\n onSelect={onSelect}\n />\n ))}\n </div>\n </aside>\n );\n}\n"],"mappings":";AAgCU,SAYE,KAZF;AA9BV,SAAS,YAAY;AACrB,SAAS,sBAAsB;AAE/B,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAWvB,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,QAAQ,QAAQ,UAAU;AAChC,QAAM,WAAW,QAAQ,aAAa,OAAO,CAAC;AAC9C,QAAM,cAAc,SAAS;AAE7B,SACE,oBAAC,WAAM,WAAU,iEACf,+BAAC,SAAI,WAAU,2DACb;AAAA,yBAAC,SAAI,WAAU,gEACb;AAAA,2BAAC,UAAK,WAAU,sEAAqE;AAAA;AAAA,QACrE,QAAQ;AAAA,QAAO;AAAA,QAAE;AAAA,SACjC;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAW;AAAA,UACX,OAAO,QAAQ,SAAS,cAAc,gBAAgB;AAAA,UACtD,UAAU;AAAA,UACV,SAAS;AAAA,UACT,WAAU;AAAA,UAEV,8BAAC,QAAK,eAAY,QAAO;AAAA;AAAA,MAC3B;AAAA,OACF;AAAA,IAEC,QAAQ,IAAI,CAAC,OAAO,MACnB;AAAA,MAAC;AAAA;AAAA,QAEC;AAAA,QACA,SAAS,MAAM;AAAA,QACf,UAAU,MAAM,OAAO;AAAA,QACvB,OAAO,SAAS,KAAK;AAAA,QACrB;AAAA;AAAA,MALK,MAAM;AAAA,IAMb,CACD;AAAA,KACH,GACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface SynonymsFieldProps {
|
|
2
|
+
synonyms: string[];
|
|
3
|
+
onAdd: (syn: string) => void;
|
|
4
|
+
onRemove: (index: number) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function SynonymsField({ synonyms, onAdd, onRemove }: SynonymsFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=SynonymsField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SynonymsField.d.ts","sourceRoot":"","sources":["../../../../../src/components/SeoDrawer/tabs/keyphrase/SynonymsField.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,kBAAkB,2CA8E9E"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Plus, X } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { cn } from "../../../../utils/style";
|
|
6
|
+
function SynonymsField({ synonyms, onAdd, onRemove }) {
|
|
7
|
+
const [draft, setDraft] = useState("");
|
|
8
|
+
const [focused, setFocused] = useState(false);
|
|
9
|
+
const canAdd = draft.trim().length > 0;
|
|
10
|
+
const commit = () => {
|
|
11
|
+
if (!canAdd)
|
|
12
|
+
return;
|
|
13
|
+
onAdd(draft.trim());
|
|
14
|
+
setDraft("");
|
|
15
|
+
};
|
|
16
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
17
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[10px] uppercase tracking-[0.05em] text-neutral-500 mb-[6px]", children: "Synonyms" }),
|
|
18
|
+
/* @__PURE__ */ jsxs(
|
|
19
|
+
"div",
|
|
20
|
+
{
|
|
21
|
+
className: cn(
|
|
22
|
+
"border rounded-rs bg-neutral-0 overflow-hidden",
|
|
23
|
+
focused ? "border-neutral-800" : "border-neutral-200"
|
|
24
|
+
),
|
|
25
|
+
children: [
|
|
26
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-[7px] p-[9px] items-center", children: [
|
|
27
|
+
synonyms.map((synonym, index) => /* @__PURE__ */ jsxs(
|
|
28
|
+
"span",
|
|
29
|
+
{
|
|
30
|
+
className: "bg-neutral-1000 text-neutral-0 rounded-[15px] pl-[12px] pr-[6px] py-[4px] text-[11.5px] inline-flex items-center gap-[7px]",
|
|
31
|
+
children: [
|
|
32
|
+
synonym,
|
|
33
|
+
/* @__PURE__ */ jsx(
|
|
34
|
+
"button",
|
|
35
|
+
{
|
|
36
|
+
type: "button",
|
|
37
|
+
"aria-label": `Remove synonym ${synonym} (${index + 1})`,
|
|
38
|
+
className: "grid place-items-center p-0.5 text-[#a9adb4] rounded-full bg-transparent hover:text-neutral-0 hover:bg-neutral-900! [&_svg]:size-[1em] cursor-pointer",
|
|
39
|
+
onClick: () => onRemove(index),
|
|
40
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
`${synonym}-${index}`
|
|
46
|
+
)),
|
|
47
|
+
/* @__PURE__ */ jsxs(
|
|
48
|
+
"button",
|
|
49
|
+
{
|
|
50
|
+
type: "button",
|
|
51
|
+
disabled: !canAdd,
|
|
52
|
+
onClick: commit,
|
|
53
|
+
className: "inline-flex items-center gap-[5px] rounded-[15px] px-[11px] py-[5px] text-[11.5px] font-semibold bg-neutral-1000 text-neutral-0 disabled:bg-neutral-100 disabled:text-neutral-300 [&_svg]:size-[13px] cursor-pointer",
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ jsx(Plus, { "aria-hidden": "true" }),
|
|
56
|
+
" Add synonym"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
] }),
|
|
61
|
+
/* @__PURE__ */ jsx("div", { className: "border-t border-neutral-150" }),
|
|
62
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[8px] px-[11px] py-[8px]", children: [
|
|
63
|
+
/* @__PURE__ */ jsx(
|
|
64
|
+
"input",
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
value: draft,
|
|
68
|
+
onChange: (e) => setDraft(e.target.value),
|
|
69
|
+
onFocus: () => setFocused(true),
|
|
70
|
+
onBlur: () => setFocused(false),
|
|
71
|
+
onKeyDown: (e) => {
|
|
72
|
+
if (e.key === "Enter") {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
commit();
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
placeholder: "Add a synonym\u2026",
|
|
78
|
+
"aria-label": "Add a synonym",
|
|
79
|
+
className: "flex-1 border-0 outline-none text-[12.5px] text-neutral-800 bg-transparent"
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
/* @__PURE__ */ jsx("span", { className: "text-[9px] text-neutral-500 border border-neutral-200 rounded-[4px] px-[5px] py-[1px] font-semibold", children: "Enter" })
|
|
83
|
+
] })
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
),
|
|
87
|
+
/* @__PURE__ */ jsx("div", { className: "text-[11px] text-neutral-500 mt-[6px] leading-[1.45]", children: "Synonyms count as keyphrase matches in this keyphrase\u2019s checks. One chip per synonym." })
|
|
88
|
+
] });
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
SynonymsField
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=SynonymsField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/components/SeoDrawer/tabs/keyphrase/SynonymsField.tsx"],"sourcesContent":["\"use client\";\n\nimport { Plus, X } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"../../../../utils/style\";\n\nexport interface SynonymsFieldProps {\n synonyms: string[];\n onAdd: (syn: string) => void;\n onRemove: (index: number) => void;\n}\n\nexport function SynonymsField({ synonyms, onAdd, onRemove }: SynonymsFieldProps) {\n const [draft, setDraft] = useState(\"\");\n const [focused, setFocused] = useState(false);\n const canAdd = draft.trim().length > 0;\n\n const commit = () => {\n if (!canAdd) return;\n onAdd(draft.trim());\n setDraft(\"\");\n };\n\n return (\n <div>\n <span className=\"block text-[10px] uppercase tracking-[0.05em] text-neutral-500 mb-[6px]\">\n Synonyms\n </span>\n\n <div\n className={cn(\n \"border rounded-rs bg-neutral-0 overflow-hidden\",\n focused ? \"border-neutral-800\" : \"border-neutral-200\"\n )}\n >\n <div className=\"flex flex-wrap gap-[7px] p-[9px] items-center\">\n {synonyms.map((synonym, index) => (\n <span\n key={`${synonym}-${index}`}\n className=\"bg-neutral-1000 text-neutral-0 rounded-[15px] pl-[12px] pr-[6px] py-[4px] text-[11.5px] inline-flex items-center gap-[7px]\"\n >\n {synonym}\n\n <button\n type=\"button\"\n aria-label={`Remove synonym ${synonym} (${index + 1})`}\n className=\"grid place-items-center p-0.5 text-[#a9adb4] rounded-full bg-transparent hover:text-neutral-0 hover:bg-neutral-900! [&_svg]:size-[1em] cursor-pointer\"\n onClick={() => onRemove(index)}\n >\n <X aria-hidden=\"true\" />\n </button>\n </span>\n ))}\n <button\n type=\"button\"\n disabled={!canAdd}\n onClick={commit}\n className=\"inline-flex items-center gap-[5px] rounded-[15px] px-[11px] py-[5px] text-[11.5px] font-semibold bg-neutral-1000 text-neutral-0 disabled:bg-neutral-100 disabled:text-neutral-300 [&_svg]:size-[13px] cursor-pointer\"\n >\n <Plus aria-hidden=\"true\" /> Add synonym\n </button>\n </div>\n <div className=\"border-t border-neutral-150\" />\n <div className=\"flex items-center gap-[8px] px-[11px] py-[8px]\">\n <input\n type=\"text\"\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onFocus={() => setFocused(true)}\n onBlur={() => setFocused(false)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n commit();\n }\n }}\n placeholder=\"Add a synonym…\"\n aria-label=\"Add a synonym\"\n className=\"flex-1 border-0 outline-none text-[12.5px] text-neutral-800 bg-transparent\"\n />\n <span className=\"text-[9px] text-neutral-500 border border-neutral-200 rounded-[4px] px-[5px] py-[1px] font-semibold\">\n Enter\n </span>\n </div>\n </div>\n <div className=\"text-[11px] text-neutral-500 mt-[6px] leading-[1.45]\">\n Synonyms count as keyphrase matches in this keyphrase’s checks. One chip per synonym.\n </div>\n </div>\n );\n}\n"],"mappings":";AAyBM,cAYM,YAZN;AAvBN,SAAS,MAAM,SAAS;AACxB,SAAS,gBAAgB;AACzB,SAAS,UAAU;AAQZ,SAAS,cAAc,EAAE,UAAU,OAAO,SAAS,GAAuB;AAC/E,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,SAAS,MAAM,KAAK,EAAE,SAAS;AAErC,QAAM,SAAS,MAAM;AACnB,QAAI,CAAC;AAAQ;AACb,UAAM,MAAM,KAAK,CAAC;AAClB,aAAS,EAAE;AAAA,EACb;AAEA,SACE,qBAAC,SACC;AAAA,wBAAC,UAAK,WAAU,2EAA0E,sBAE1F;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,UAAU,uBAAuB;AAAA,QACnC;AAAA,QAEA;AAAA,+BAAC,SAAI,WAAU,iDACZ;AAAA,qBAAS,IAAI,CAAC,SAAS,UACtB;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAU;AAAA,gBAET;AAAA;AAAA,kBAED;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,cAAY,kBAAkB,OAAO,KAAK,QAAQ,CAAC;AAAA,sBACnD,WAAU;AAAA,sBACV,SAAS,MAAM,SAAS,KAAK;AAAA,sBAE7B,8BAAC,KAAE,eAAY,QAAO;AAAA;AAAA,kBACxB;AAAA;AAAA;AAAA,cAZK,GAAG,OAAO,IAAI,KAAK;AAAA,YAa1B,CACD;AAAA,YACD;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,UAAU,CAAC;AAAA,gBACX,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV;AAAA,sCAAC,QAAK,eAAY,QAAO;AAAA,kBAAE;AAAA;AAAA;AAAA,YAC7B;AAAA,aACF;AAAA,UACA,oBAAC,SAAI,WAAU,+BAA8B;AAAA,UAC7C,qBAAC,SAAI,WAAU,kDACb;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,OAAO;AAAA,gBACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,gBACxC,SAAS,MAAM,WAAW,IAAI;AAAA,gBAC9B,QAAQ,MAAM,WAAW,KAAK;AAAA,gBAC9B,WAAW,CAAC,MAAM;AAChB,sBAAI,EAAE,QAAQ,SAAS;AACrB,sBAAE,eAAe;AACjB,2BAAO;AAAA,kBACT;AAAA,gBACF;AAAA,gBACA,aAAY;AAAA,gBACZ,cAAW;AAAA,gBACX,WAAU;AAAA;AAAA,YACZ;AAAA,YACA,oBAAC,UAAK,WAAU,uGAAsG,mBAEtH;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,SAAI,WAAU,wDAAuD,wGAEtE;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../../../../src/components/SeoDrawer/tabs/keyphrase/icons.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/components/SeoDrawer/tabs/keyphrase/icons.tsx"],"sourcesContent":["\"use client\";\n\nexport { Target, Trash2 } from \"lucide-react\";\n"],"mappings":";AAEA,SAAS,QAAQ,cAAc;","names":[]}
|
|
@@ -10,7 +10,6 @@ export interface UseAnalysisArgs {
|
|
|
10
10
|
export interface UseAnalysisResult {
|
|
11
11
|
result: AnalysisResult | null;
|
|
12
12
|
analyzing: boolean;
|
|
13
|
-
analyzedKeyphrase: string | null;
|
|
14
13
|
analyzeNow: () => void;
|
|
15
14
|
}
|
|
16
15
|
export declare function useAnalysis({ getInput, signature, supportedLocales, enabled, }: UseAnalysisArgs): UseAnalysisResult;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAnalysis.d.ts","sourceRoot":"","sources":["../../../src/components/SeoDrawer/useAnalysis.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAIjF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,
|
|
1
|
+
{"version":3,"file":"useAnalysis.d.ts","sourceRoot":"","sources":["../../../src/components/SeoDrawer/useAnalysis.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAIjF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,OAAc,GACf,EAAE,eAAe,GAAG,iBAAiB,CAoDrC"}
|
|
@@ -11,7 +11,6 @@ function useAnalysis({
|
|
|
11
11
|
}) {
|
|
12
12
|
const [result, setResult] = useState(null);
|
|
13
13
|
const [analyzing, setAnalyzing] = useState(false);
|
|
14
|
-
const [analyzedKeyphrase, setAnalyzedKeyphrase] = useState(null);
|
|
15
14
|
const getInputRef = useRef(getInput);
|
|
16
15
|
getInputRef.current = getInput;
|
|
17
16
|
const signatureRef = useRef(signature);
|
|
@@ -30,7 +29,6 @@ function useAnalysis({
|
|
|
30
29
|
if (runId !== runSeq.current)
|
|
31
30
|
return;
|
|
32
31
|
setResult(runAnalysis(input));
|
|
33
|
-
setAnalyzedKeyphrase(input.keyphrase);
|
|
34
32
|
} finally {
|
|
35
33
|
if (runId === runSeq.current)
|
|
36
34
|
setAnalyzing(false);
|
|
@@ -52,7 +50,6 @@ function useAnalysis({
|
|
|
52
50
|
return {
|
|
53
51
|
result,
|
|
54
52
|
analyzing,
|
|
55
|
-
analyzedKeyphrase,
|
|
56
53
|
analyzeNow
|
|
57
54
|
};
|
|
58
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/SeoDrawer/useAnalysis.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { runAnalysis } from \"../../engine/runAnalysis\";\nimport type { AnalysisInput, AnalysisResult } from \"../../engine/types/analysis\";\nimport { decideAutoAction } from \"./analysisDecision\";\nimport { ensureLanguagePack } from \"./languagePacks\";\n\nexport interface UseAnalysisArgs {\n getInput: (opts?: { live?: boolean }) => Promise<AnalysisInput>;\n signature: string;\n supportedLocales: string[];\n enabled?: boolean;\n}\n\nexport interface UseAnalysisResult {\n result: AnalysisResult | null;\n analyzing: boolean;\n
|
|
1
|
+
{"version":3,"sources":["../../../src/components/SeoDrawer/useAnalysis.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { runAnalysis } from \"../../engine/runAnalysis\";\nimport type { AnalysisInput, AnalysisResult } from \"../../engine/types/analysis\";\nimport { decideAutoAction } from \"./analysisDecision\";\nimport { ensureLanguagePack } from \"./languagePacks\";\n\nexport interface UseAnalysisArgs {\n getInput: (opts?: { live?: boolean }) => Promise<AnalysisInput>;\n signature: string;\n supportedLocales: string[];\n enabled?: boolean;\n}\n\nexport interface UseAnalysisResult {\n result: AnalysisResult | null;\n analyzing: boolean;\n analyzeNow: () => void;\n}\n\nexport function useAnalysis({\n getInput,\n signature,\n supportedLocales,\n enabled = true,\n}: UseAnalysisArgs): UseAnalysisResult {\n const [result, setResult] = useState<AnalysisResult | null>(null);\n const [analyzing, setAnalyzing] = useState(false);\n\n const getInputRef = useRef(getInput);\n getInputRef.current = getInput;\n const signatureRef = useRef(signature);\n signatureRef.current = signature;\n const localesRef = useRef(supportedLocales);\n localesRef.current = supportedLocales;\n\n const lastSignature = useRef<string | null>(null);\n const runSeq = useRef(0);\n\n const run = useCallback(async (live: boolean) => {\n const runId = ++runSeq.current;\n\n lastSignature.current = signatureRef.current;\n setAnalyzing(true);\n\n try {\n const input = await getInputRef.current({ live });\n await ensureLanguagePack(input.locale, localesRef.current);\n if (runId !== runSeq.current) return;\n\n setResult(runAnalysis(input));\n } finally {\n if (runId === runSeq.current) setAnalyzing(false);\n }\n }, []);\n\n const analyzeNow = useCallback(() => {\n void run(true);\n }, [run]);\n\n useEffect(() => {\n const action = decideAutoAction({\n enabled,\n signature,\n lastSignature: lastSignature.current,\n });\n\n if (action === \"skip\") return;\n\n void run(false);\n }, [signature, run, enabled]);\n\n return {\n result,\n analyzing,\n analyzeNow,\n };\n}\n"],"mappings":";AAEA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AACzD,SAAS,mBAAmB;AAE5B,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AAe5B,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GAAuC;AACrC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAgC,IAAI;AAChE,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAEhD,QAAM,cAAc,OAAO,QAAQ;AACnC,cAAY,UAAU;AACtB,QAAM,eAAe,OAAO,SAAS;AACrC,eAAa,UAAU;AACvB,QAAM,aAAa,OAAO,gBAAgB;AAC1C,aAAW,UAAU;AAErB,QAAM,gBAAgB,OAAsB,IAAI;AAChD,QAAM,SAAS,OAAO,CAAC;AAEvB,QAAM,MAAM,YAAY,OAAO,SAAkB;AAC/C,UAAM,QAAQ,EAAE,OAAO;AAEvB,kBAAc,UAAU,aAAa;AACrC,iBAAa,IAAI;AAEjB,QAAI;AACF,YAAM,QAAQ,MAAM,YAAY,QAAQ,EAAE,KAAK,CAAC;AAChD,YAAM,mBAAmB,MAAM,QAAQ,WAAW,OAAO;AACzD,UAAI,UAAU,OAAO;AAAS;AAE9B,gBAAU,YAAY,KAAK,CAAC;AAAA,IAC9B,UAAE;AACA,UAAI,UAAU,OAAO;AAAS,qBAAa,KAAK;AAAA,IAClD;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,aAAa,YAAY,MAAM;AACnC,SAAK,IAAI,IAAI;AAAA,EACf,GAAG,CAAC,GAAG,CAAC;AAER,YAAU,MAAM;AACd,UAAM,SAAS,iBAAiB;AAAA,MAC9B;AAAA,MACA;AAAA,MACA,eAAe,cAAc;AAAA,IAC/B,CAAC;AAED,QAAI,WAAW;AAAQ;AAEvB,SAAK,IAAI,KAAK;AAAA,EAChB,GAAG,CAAC,WAAW,KAAK,OAAO,CAAC;AAE5B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { KeyphraseEntry } from "./keyphraseState";
|
|
2
|
+
export interface UseKeyphrasesArgs {
|
|
3
|
+
collectionSlug: string;
|
|
4
|
+
docId: string;
|
|
5
|
+
localeCode: string;
|
|
6
|
+
}
|
|
7
|
+
export interface UseKeyphrasesApi {
|
|
8
|
+
keyphrases: KeyphraseEntry[];
|
|
9
|
+
addRelated: () => string | undefined;
|
|
10
|
+
updateText: (id: string, text: string) => void;
|
|
11
|
+
addSynonym: (id: string, syn: string) => void;
|
|
12
|
+
removeSynonym: (id: string, index: number) => void;
|
|
13
|
+
remove: (id: string) => void;
|
|
14
|
+
setFocus: (id: string) => void;
|
|
15
|
+
isDuplicate: (id: string, text: string) => boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function useKeyphrases({ collectionSlug, docId, localeCode, }: UseKeyphrasesArgs): UseKeyphrasesApi;
|
|
18
|
+
//# sourceMappingURL=useKeyphrases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useKeyphrases.d.ts","sourceRoot":"","sources":["../../../src/components/SeoDrawer/useKeyphrases.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,UAAU,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACrC,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACpD;AAED,wBAAgB,aAAa,CAAC,EAC5B,cAAc,EACd,KAAK,EACL,UAAU,GACX,EAAE,iBAAiB,GAAG,gBAAgB,CAyEtC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import * as ops from "./keyphraseState";
|
|
4
|
+
import { loadKeyphrases, saveKeyphrases, storageKey } from "./keyphraseStorage";
|
|
5
|
+
function useKeyphrases({
|
|
6
|
+
collectionSlug,
|
|
7
|
+
docId,
|
|
8
|
+
localeCode
|
|
9
|
+
}) {
|
|
10
|
+
const key = storageKey(collectionSlug, docId, localeCode);
|
|
11
|
+
const [list, setList] = useState(() => loadKeyphrases(key));
|
|
12
|
+
const keyRef = useRef(key);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (keyRef.current !== key) {
|
|
15
|
+
keyRef.current = key;
|
|
16
|
+
setList(loadKeyphrases(key));
|
|
17
|
+
}
|
|
18
|
+
}, [key]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
saveKeyphrases(key, list);
|
|
21
|
+
}, [key, list]);
|
|
22
|
+
const listRef = useRef(list);
|
|
23
|
+
listRef.current = list;
|
|
24
|
+
useEffect(
|
|
25
|
+
() => () => {
|
|
26
|
+
const pruned = ops.pruneEmpties(listRef.current);
|
|
27
|
+
if (pruned.length !== listRef.current.length)
|
|
28
|
+
saveKeyphrases(keyRef.current, pruned);
|
|
29
|
+
},
|
|
30
|
+
[]
|
|
31
|
+
);
|
|
32
|
+
const addRelated = useCallback(() => {
|
|
33
|
+
const current = listRef.current;
|
|
34
|
+
const next = ops.addRelated(current);
|
|
35
|
+
if (next === current)
|
|
36
|
+
return ops.firstEmptyId(current);
|
|
37
|
+
setList(next);
|
|
38
|
+
return next.at(-1)?.id;
|
|
39
|
+
}, []);
|
|
40
|
+
const updateText = useCallback(
|
|
41
|
+
(id, text) => setList((l) => ops.updateText(l, id, text)),
|
|
42
|
+
[]
|
|
43
|
+
);
|
|
44
|
+
const addSynonym = useCallback(
|
|
45
|
+
(id, syn) => setList((l) => ops.addSynonym(l, id, syn)),
|
|
46
|
+
[]
|
|
47
|
+
);
|
|
48
|
+
const removeSynonym = useCallback(
|
|
49
|
+
(id, index) => setList((l) => ops.removeSynonym(l, id, index)),
|
|
50
|
+
[]
|
|
51
|
+
);
|
|
52
|
+
const remove = useCallback(
|
|
53
|
+
(id) => setList((l) => {
|
|
54
|
+
const next = ops.remove(l, id);
|
|
55
|
+
return next.length > 0 ? next : [ops.createEntry()];
|
|
56
|
+
}),
|
|
57
|
+
[]
|
|
58
|
+
);
|
|
59
|
+
const setFocus = useCallback((id) => setList((l) => ops.setFocus(l, id)), []);
|
|
60
|
+
const isDuplicate = useCallback(
|
|
61
|
+
(id, text) => ops.isDuplicate(listRef.current, id, text),
|
|
62
|
+
[]
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
keyphrases: list,
|
|
66
|
+
addRelated,
|
|
67
|
+
updateText,
|
|
68
|
+
addSynonym,
|
|
69
|
+
removeSynonym,
|
|
70
|
+
remove,
|
|
71
|
+
setFocus,
|
|
72
|
+
isDuplicate
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
useKeyphrases
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=useKeyphrases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/SeoDrawer/useKeyphrases.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport * as ops from \"./keyphraseState\";\nimport type { KeyphraseEntry } from \"./keyphraseState\";\nimport { loadKeyphrases, saveKeyphrases, storageKey } from \"./keyphraseStorage\";\n\nexport interface UseKeyphrasesArgs {\n collectionSlug: string;\n docId: string;\n localeCode: string;\n}\n\nexport interface UseKeyphrasesApi {\n keyphrases: KeyphraseEntry[];\n addRelated: () => string | undefined;\n updateText: (id: string, text: string) => void;\n addSynonym: (id: string, syn: string) => void;\n removeSynonym: (id: string, index: number) => void;\n remove: (id: string) => void;\n setFocus: (id: string) => void;\n isDuplicate: (id: string, text: string) => boolean;\n}\n\nexport function useKeyphrases({\n collectionSlug,\n docId,\n localeCode,\n}: UseKeyphrasesArgs): UseKeyphrasesApi {\n const key = storageKey(collectionSlug, docId, localeCode);\n const [list, setList] = useState<KeyphraseEntry[]>(() => loadKeyphrases(key));\n\n const keyRef = useRef(key);\n\n useEffect(() => {\n if (keyRef.current !== key) {\n keyRef.current = key;\n setList(loadKeyphrases(key));\n }\n }, [key]);\n\n useEffect(() => {\n saveKeyphrases(key, list);\n }, [key, list]);\n\n const listRef = useRef(list);\n listRef.current = list;\n useEffect(\n () => () => {\n const pruned = ops.pruneEmpties(listRef.current);\n if (pruned.length !== listRef.current.length) saveKeyphrases(keyRef.current, pruned);\n },\n []\n );\n\n const addRelated = useCallback((): string | undefined => {\n const current = listRef.current;\n const next = ops.addRelated(current);\n\n if (next === current) return ops.firstEmptyId(current);\n setList(next);\n\n return next.at(-1)?.id;\n }, []);\n const updateText = useCallback(\n (id: string, text: string) => setList((l) => ops.updateText(l, id, text)),\n []\n );\n const addSynonym = useCallback(\n (id: string, syn: string) => setList((l) => ops.addSynonym(l, id, syn)),\n []\n );\n const removeSynonym = useCallback(\n (id: string, index: number) => setList((l) => ops.removeSynonym(l, id, index)),\n []\n );\n const remove = useCallback(\n (id: string) =>\n setList((l) => {\n const next = ops.remove(l, id);\n\n return next.length > 0 ? next : [ops.createEntry()];\n }),\n []\n );\n const setFocus = useCallback((id: string) => setList((l) => ops.setFocus(l, id)), []);\n const isDuplicate = useCallback(\n (id: string, text: string) => ops.isDuplicate(listRef.current, id, text),\n []\n );\n\n return {\n keyphrases: list,\n addRelated,\n updateText,\n addSynonym,\n removeSynonym,\n remove,\n setFocus,\n isDuplicate,\n };\n}\n"],"mappings":";AAEA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AACzD,YAAY,SAAS;AAErB,SAAS,gBAAgB,gBAAgB,kBAAkB;AAmBpD,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AACF,GAAwC;AACtC,QAAM,MAAM,WAAW,gBAAgB,OAAO,UAAU;AACxD,QAAM,CAAC,MAAM,OAAO,IAAI,SAA2B,MAAM,eAAe,GAAG,CAAC;AAE5E,QAAM,SAAS,OAAO,GAAG;AAEzB,YAAU,MAAM;AACd,QAAI,OAAO,YAAY,KAAK;AAC1B,aAAO,UAAU;AACjB,cAAQ,eAAe,GAAG,CAAC;AAAA,IAC7B;AAAA,EACF,GAAG,CAAC,GAAG,CAAC;AAER,YAAU,MAAM;AACd,mBAAe,KAAK,IAAI;AAAA,EAC1B,GAAG,CAAC,KAAK,IAAI,CAAC;AAEd,QAAM,UAAU,OAAO,IAAI;AAC3B,UAAQ,UAAU;AAClB;AAAA,IACE,MAAM,MAAM;AACV,YAAM,SAAS,IAAI,aAAa,QAAQ,OAAO;AAC/C,UAAI,OAAO,WAAW,QAAQ,QAAQ;AAAQ,uBAAe,OAAO,SAAS,MAAM;AAAA,IACrF;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,aAAa,YAAY,MAA0B;AACvD,UAAM,UAAU,QAAQ;AACxB,UAAM,OAAO,IAAI,WAAW,OAAO;AAEnC,QAAI,SAAS;AAAS,aAAO,IAAI,aAAa,OAAO;AACrD,YAAQ,IAAI;AAEZ,WAAO,KAAK,GAAG,EAAE,GAAG;AAAA,EACtB,GAAG,CAAC,CAAC;AACL,QAAM,aAAa;AAAA,IACjB,CAAC,IAAY,SAAiB,QAAQ,CAAC,MAAM,IAAI,WAAW,GAAG,IAAI,IAAI,CAAC;AAAA,IACxE,CAAC;AAAA,EACH;AACA,QAAM,aAAa;AAAA,IACjB,CAAC,IAAY,QAAgB,QAAQ,CAAC,MAAM,IAAI,WAAW,GAAG,IAAI,GAAG,CAAC;AAAA,IACtE,CAAC;AAAA,EACH;AACA,QAAM,gBAAgB;AAAA,IACpB,CAAC,IAAY,UAAkB,QAAQ,CAAC,MAAM,IAAI,cAAc,GAAG,IAAI,KAAK,CAAC;AAAA,IAC7E,CAAC;AAAA,EACH;AACA,QAAM,SAAS;AAAA,IACb,CAAC,OACC,QAAQ,CAAC,MAAM;AACb,YAAM,OAAO,IAAI,OAAO,GAAG,EAAE;AAE7B,aAAO,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,YAAY,CAAC;AAAA,IACpD,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACA,QAAM,WAAW,YAAY,CAAC,OAAe,QAAQ,CAAC,MAAM,IAAI,SAAS,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;AACpF,QAAM,cAAc;AAAA,IAClB,CAAC,IAAY,SAAiB,IAAI,YAAY,QAAQ,SAAS,IAAI,IAAI;AAAA,IACvE,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AnalysisInput } from "../../engine/types/analysis";
|
|
1
|
+
import type { AnalysisInput, KeyphraseInput } from "../../engine/types/analysis";
|
|
2
2
|
import type { SeoFieldPaths } from "../../types/config";
|
|
3
|
+
export declare function keyphraseSignature(keyphrases: KeyphraseInput[]): string;
|
|
3
4
|
export interface LiveDocArgs {
|
|
4
5
|
collectionSlug: string;
|
|
5
6
|
fields: SeoFieldPaths;
|
|
@@ -7,7 +8,7 @@ export interface LiveDocArgs {
|
|
|
7
8
|
name: string;
|
|
8
9
|
baseUrl: string;
|
|
9
10
|
};
|
|
10
|
-
|
|
11
|
+
keyphrases: KeyphraseInput[];
|
|
11
12
|
enabled?: boolean;
|
|
12
13
|
extractContentPath: string;
|
|
13
14
|
}
|
|
@@ -17,5 +18,5 @@ export interface UseLiveDocumentResult {
|
|
|
17
18
|
live?: boolean;
|
|
18
19
|
}) => Promise<AnalysisInput>;
|
|
19
20
|
}
|
|
20
|
-
export declare function useLiveDocument({ collectionSlug: _collectionSlug, fields, site,
|
|
21
|
+
export declare function useLiveDocument({ collectionSlug: _collectionSlug, fields, site, keyphrases, enabled, extractContentPath, }: LiveDocArgs): UseLiveDocumentResult;
|
|
21
22
|
//# sourceMappingURL=useLiveDocument.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLiveDocument.d.ts","sourceRoot":"","sources":["../../../src/components/SeoDrawer/useLiveDocument.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"useLiveDocument.d.ts","sourceRoot":"","sources":["../../../src/components/SeoDrawer/useLiveDocument.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AACjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,cAAc,EAAE,GAAG,MAAM,CAEvE;AAED,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;CACjE;AAED,wBAAgB,eAAe,CAAC,EAC9B,cAAc,EAAE,eAAe,EAC/B,MAAM,EACN,IAAI,EACJ,UAAU,EACV,OAAc,EACd,kBAAkB,GACnB,EAAE,WAAW,GAAG,qBAAqB,CA6ErC"}
|