@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.
Files changed (153) hide show
  1. package/README.md +12 -1
  2. package/dist/admin.css +232 -17
  3. package/dist/components/SeoButton/SeoButtonInner.d.ts.map +1 -1
  4. package/dist/components/SeoButton/SeoButtonInner.js +46 -16
  5. package/dist/components/SeoButton/SeoButtonInner.js.map +1 -1
  6. package/dist/components/SeoDrawer/build-analysis-input.d.ts +2 -2
  7. package/dist/components/SeoDrawer/build-analysis-input.d.ts.map +1 -1
  8. package/dist/components/SeoDrawer/build-analysis-input.js +1 -1
  9. package/dist/components/SeoDrawer/build-analysis-input.js.map +1 -1
  10. package/dist/components/SeoDrawer/buildInput.d.ts +3 -3
  11. package/dist/components/SeoDrawer/buildInput.d.ts.map +1 -1
  12. package/dist/components/SeoDrawer/buildInput.js +3 -2
  13. package/dist/components/SeoDrawer/buildInput.js.map +1 -1
  14. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssueBanner.d.ts +7 -0
  15. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssueBanner.d.ts.map +1 -0
  16. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssueBanner.js +26 -0
  17. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssueBanner.js.map +1 -0
  18. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssuePills.d.ts +8 -0
  19. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssuePills.d.ts.map +1 -0
  20. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssuePills.js +17 -0
  21. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingIssuePills.js.map +1 -0
  22. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/HeadingTreeRow.d.ts.map +1 -1
  23. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/HeadingTreeRow.js +2 -0
  24. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/HeadingTreeRow.js.map +1 -1
  25. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/headingTreeView.d.ts +1 -0
  26. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/headingTreeView.d.ts.map +1 -1
  27. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/headingTreeView.js +14 -1
  28. package/dist/components/SeoDrawer/components/HeadingsSection/HeadingTree/headingTreeView.js.map +1 -1
  29. package/dist/components/SeoDrawer/components/HeadingsSection/headingIssueCopy.d.ts +7 -0
  30. package/dist/components/SeoDrawer/components/HeadingsSection/headingIssueCopy.d.ts.map +1 -0
  31. package/dist/components/SeoDrawer/components/HeadingsSection/headingIssueCopy.js +26 -0
  32. package/dist/components/SeoDrawer/components/HeadingsSection/headingIssueCopy.js.map +1 -0
  33. package/dist/components/SeoDrawer/components/HeadingsSection/index.d.ts.map +1 -1
  34. package/dist/components/SeoDrawer/components/HeadingsSection/index.js +17 -6
  35. package/dist/components/SeoDrawer/components/HeadingsSection/index.js.map +1 -1
  36. package/dist/components/SeoDrawer/components/SerpPreview/highlight-keyphrase.d.ts +1 -1
  37. package/dist/components/SeoDrawer/components/SerpPreview/highlight-keyphrase.d.ts.map +1 -1
  38. package/dist/components/SeoDrawer/components/SerpPreview/highlight-keyphrase.js +10 -3
  39. package/dist/components/SeoDrawer/components/SerpPreview/highlight-keyphrase.js.map +1 -1
  40. package/dist/components/SeoDrawer/components/SerpPreview/index.d.ts +2 -1
  41. package/dist/components/SeoDrawer/components/SerpPreview/index.d.ts.map +1 -1
  42. package/dist/components/SeoDrawer/components/SerpPreview/index.js +8 -2
  43. package/dist/components/SeoDrawer/components/SerpPreview/index.js.map +1 -1
  44. package/dist/components/SeoDrawer/components/SerpPreview/variants.d.ts +6 -6
  45. package/dist/components/SeoDrawer/components/TabWrapper.d.ts +6 -0
  46. package/dist/components/SeoDrawer/components/TabWrapper.d.ts.map +1 -0
  47. package/dist/components/SeoDrawer/components/TabWrapper.js +8 -0
  48. package/dist/components/SeoDrawer/components/TabWrapper.js.map +1 -0
  49. package/dist/components/SeoDrawer/index.d.ts +11 -4
  50. package/dist/components/SeoDrawer/index.d.ts.map +1 -1
  51. package/dist/components/SeoDrawer/index.js +38 -16
  52. package/dist/components/SeoDrawer/index.js.map +1 -1
  53. package/dist/components/SeoDrawer/keyphraseState.d.ts +16 -0
  54. package/dist/components/SeoDrawer/keyphraseState.d.ts.map +1 -0
  55. package/dist/components/SeoDrawer/keyphraseState.js +73 -0
  56. package/dist/components/SeoDrawer/keyphraseState.js.map +1 -0
  57. package/dist/components/SeoDrawer/keyphraseStorage.d.ts +5 -0
  58. package/dist/components/SeoDrawer/keyphraseStorage.d.ts.map +1 -0
  59. package/dist/components/SeoDrawer/keyphraseStorage.js +60 -0
  60. package/dist/components/SeoDrawer/keyphraseStorage.js.map +1 -0
  61. package/dist/components/SeoDrawer/tabs/InclusiveTab.d.ts.map +1 -1
  62. package/dist/components/SeoDrawer/tabs/InclusiveTab.js +3 -2
  63. package/dist/components/SeoDrawer/tabs/InclusiveTab.js.map +1 -1
  64. package/dist/components/SeoDrawer/tabs/KeyphraseTab.d.ts +14 -7
  65. package/dist/components/SeoDrawer/tabs/KeyphraseTab.d.ts.map +1 -1
  66. package/dist/components/SeoDrawer/tabs/KeyphraseTab.js +110 -69
  67. package/dist/components/SeoDrawer/tabs/KeyphraseTab.js.map +1 -1
  68. package/dist/components/SeoDrawer/tabs/OnPageTab.d.ts.map +1 -1
  69. package/dist/components/SeoDrawer/tabs/OnPageTab.js +3 -2
  70. package/dist/components/SeoDrawer/tabs/OnPageTab.js.map +1 -1
  71. package/dist/components/SeoDrawer/tabs/ReadabilityTab.d.ts.map +1 -1
  72. package/dist/components/SeoDrawer/tabs/ReadabilityTab.js +3 -2
  73. package/dist/components/SeoDrawer/tabs/ReadabilityTab.js.map +1 -1
  74. package/dist/components/SeoDrawer/tabs/SerpTab.d.ts +2 -1
  75. package/dist/components/SeoDrawer/tabs/SerpTab.d.ts.map +1 -1
  76. package/dist/components/SeoDrawer/tabs/SerpTab.js +14 -3
  77. package/dist/components/SeoDrawer/tabs/SerpTab.js.map +1 -1
  78. package/dist/components/SeoDrawer/tabs/VitalsTab.d.ts.map +1 -1
  79. package/dist/components/SeoDrawer/tabs/VitalsTab.js +7 -3
  80. package/dist/components/SeoDrawer/tabs/VitalsTab.js.map +1 -1
  81. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseCard.d.ts +21 -0
  82. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseCard.d.ts.map +1 -0
  83. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseCard.js +38 -0
  84. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseCard.js.map +1 -0
  85. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseDetail.d.ts +25 -0
  86. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseDetail.d.ts.map +1 -0
  87. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseDetail.js +127 -0
  88. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseDetail.js.map +1 -0
  89. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.d.ts +12 -0
  90. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.d.ts.map +1 -0
  91. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.js +54 -0
  92. package/dist/components/SeoDrawer/tabs/keyphrase/KeyphraseRail.js.map +1 -0
  93. package/dist/components/SeoDrawer/tabs/keyphrase/SynonymsField.d.ts +7 -0
  94. package/dist/components/SeoDrawer/tabs/keyphrase/SynonymsField.d.ts.map +1 -0
  95. package/dist/components/SeoDrawer/tabs/keyphrase/SynonymsField.js +93 -0
  96. package/dist/components/SeoDrawer/tabs/keyphrase/SynonymsField.js.map +1 -0
  97. package/dist/components/SeoDrawer/tabs/keyphrase/icons.d.ts +2 -0
  98. package/dist/components/SeoDrawer/tabs/keyphrase/icons.d.ts.map +1 -0
  99. package/dist/components/SeoDrawer/tabs/keyphrase/icons.js +7 -0
  100. package/dist/components/SeoDrawer/tabs/keyphrase/icons.js.map +1 -0
  101. package/dist/components/SeoDrawer/useAnalysis.d.ts +0 -1
  102. package/dist/components/SeoDrawer/useAnalysis.d.ts.map +1 -1
  103. package/dist/components/SeoDrawer/useAnalysis.js +0 -3
  104. package/dist/components/SeoDrawer/useAnalysis.js.map +1 -1
  105. package/dist/components/SeoDrawer/useKeyphrases.d.ts +18 -0
  106. package/dist/components/SeoDrawer/useKeyphrases.d.ts.map +1 -0
  107. package/dist/components/SeoDrawer/useKeyphrases.js +78 -0
  108. package/dist/components/SeoDrawer/useKeyphrases.js.map +1 -0
  109. package/dist/components/SeoDrawer/useLiveDocument.d.ts +4 -3
  110. package/dist/components/SeoDrawer/useLiveDocument.d.ts.map +1 -1
  111. package/dist/components/SeoDrawer/useLiveDocument.js +17 -9
  112. package/dist/components/SeoDrawer/useLiveDocument.js.map +1 -1
  113. package/dist/components/SeoDrawer/variants.d.ts +1 -1
  114. package/dist/components/SeoDrawer/variants.d.ts.map +1 -1
  115. package/dist/components/SeoDrawer/variants.js +2 -1
  116. package/dist/components/SeoDrawer/variants.js.map +1 -1
  117. package/dist/constants/index.d.ts +1 -0
  118. package/dist/constants/index.d.ts.map +1 -1
  119. package/dist/constants/index.js +2 -0
  120. package/dist/constants/index.js.map +1 -1
  121. package/dist/engine/buildPaper.d.ts +2 -2
  122. package/dist/engine/buildPaper.d.ts.map +1 -1
  123. package/dist/engine/buildPaper.js +4 -2
  124. package/dist/engine/buildPaper.js.map +1 -1
  125. package/dist/engine/runAnalysis/index.d.ts.map +1 -1
  126. package/dist/engine/runAnalysis/index.js +7 -0
  127. package/dist/engine/runAnalysis/index.js.map +1 -1
  128. package/dist/engine/runAnalysis/services/derive-related.d.ts +4 -0
  129. package/dist/engine/runAnalysis/services/derive-related.d.ts.map +1 -0
  130. package/dist/engine/runAnalysis/services/derive-related.js +16 -0
  131. package/dist/engine/runAnalysis/services/derive-related.js.map +1 -0
  132. package/dist/engine/runAnalysis/services/derive-vitals/heading-issues.d.ts +8 -0
  133. package/dist/engine/runAnalysis/services/derive-vitals/heading-issues.d.ts.map +1 -0
  134. package/dist/engine/runAnalysis/services/derive-vitals/heading-issues.js +37 -0
  135. package/dist/engine/runAnalysis/services/derive-vitals/heading-issues.js.map +1 -0
  136. package/dist/engine/runAnalysis/services/derive-vitals/heading-tree.d.ts.map +1 -1
  137. package/dist/engine/runAnalysis/services/derive-vitals/heading-tree.js +7 -3
  138. package/dist/engine/runAnalysis/services/derive-vitals/heading-tree.js.map +1 -1
  139. package/dist/engine/types/analysis.d.ts +23 -0
  140. package/dist/engine/types/analysis.d.ts.map +1 -1
  141. package/dist/ui/Button.d.ts +9 -0
  142. package/dist/ui/Button.d.ts.map +1 -0
  143. package/dist/ui/Button.js +34 -0
  144. package/dist/ui/Button.js.map +1 -0
  145. package/dist/ui/ScoreRing.d.ts +17 -4
  146. package/dist/ui/ScoreRing.d.ts.map +1 -1
  147. package/dist/ui/ScoreRing.js +38 -14
  148. package/dist/ui/ScoreRing.js.map +1 -1
  149. package/package.json +1 -1
  150. package/dist/components/SeoDrawer/keyphrasePending.d.ts +0 -2
  151. package/dist/components/SeoDrawer/keyphrasePending.d.ts.map +0 -1
  152. package/dist/components/SeoDrawer/keyphrasePending.js +0 -9
  153. 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 &#8220;{entry.text.trim()}&#8221; 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,2 @@
1
+ export { Target, Trash2 } from "lucide-react";
2
+ //# sourceMappingURL=icons.d.ts.map
@@ -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,7 @@
1
+ "use client";
2
+ import { Target, Trash2 } from "lucide-react";
3
+ export {
4
+ Target,
5
+ Trash2
6
+ };
7
+ //# sourceMappingURL=icons.js.map
@@ -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,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,OAAc,GACf,EAAE,eAAe,GAAG,iBAAiB,CAuDrC"}
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 analyzedKeyphrase: string | null;\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 const [analyzedKeyphrase, setAnalyzedKeyphrase] = useState<string | null>(null);\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 setAnalyzedKeyphrase(input.keyphrase);\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 analyzedKeyphrase,\n analyzeNow,\n };\n}\n"],"mappings":";AAEA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AACzD,SAAS,mBAAmB;AAE5B,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AAgB5B,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;AAChD,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAwB,IAAI;AAE9E,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;AAC5B,2BAAqB,MAAM,SAAS;AAAA,IACtC,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,IACA;AAAA,EACF;AACF;","names":[]}
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
- keyphrase: string;
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, keyphrase, enabled, extractContentPath, }: LiveDocArgs): UseLiveDocumentResult;
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;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,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,SAAS,EAAE,MAAM,CAAC;IAClB,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,SAAS,EACT,OAAc,EACd,kBAAkB,GACnB,EAAE,WAAW,GAAG,qBAAqB,CAwErC"}
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"}