@agent-cms/agentcms 0.0.0-stage → 0.1.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 (84) hide show
  1. package/LICENSE +143 -0
  2. package/README.md +1119 -2
  3. package/changelog.json +20 -0
  4. package/dist/admin/admin-files.json +48 -0
  5. package/dist/admin/assets/CodeEditor-DcFzGUt8.js +24 -0
  6. package/dist/admin/assets/flexweg-logo-CdtuqAFA.png +0 -0
  7. package/dist/admin/assets/index-CLZ1gQZl.js +5460 -0
  8. package/dist/admin/assets/index-DMcGogR2.css +1 -0
  9. package/dist/admin/assets/sql-wasm-C1U8OeUW.wasm +0 -0
  10. package/dist/admin/assets/sql-wasm-CYl3LXH_.js +2 -0
  11. package/dist/admin/assets/sql-wasm-V5jTDcrT.js +1 -0
  12. package/dist/admin/config.js +11 -0
  13. package/dist/admin/external.default.json +72 -0
  14. package/dist/admin/index.html +116 -0
  15. package/dist/admin/plugins/core-seo/bundle.js +55 -0
  16. package/dist/admin/plugins/core-seo/manifest.json +7 -0
  17. package/dist/admin/plugins/flexweg-archives/bundle.js +1414 -0
  18. package/dist/admin/plugins/flexweg-archives/manifest.json +7 -0
  19. package/dist/admin/plugins/flexweg-multilang/bundle.js +1187 -0
  20. package/dist/admin/plugins/flexweg-multilang/manifest.json +7 -0
  21. package/dist/admin/plugins/flexweg-rss/bundle.js +1354 -0
  22. package/dist/admin/plugins/flexweg-rss/manifest.json +7 -0
  23. package/dist/admin/plugins/flexweg-search/bundle.js +1243 -0
  24. package/dist/admin/plugins/flexweg-search/manifest.json +7 -0
  25. package/dist/admin/plugins/flexweg-sitemaps/bundle.js +1293 -0
  26. package/dist/admin/plugins/flexweg-sitemaps/manifest.json +7 -0
  27. package/dist/admin/runtime/cms-runtime.js +164 -0
  28. package/dist/admin/runtime/react-dom-client.js +13 -0
  29. package/dist/admin/runtime/react-dom.js +16 -0
  30. package/dist/admin/runtime/react-i18next.js +21 -0
  31. package/dist/admin/runtime/react-jsx-runtime.js +17 -0
  32. package/dist/admin/runtime/react.js +55 -0
  33. package/dist/admin/themes/corporate/bundle.js +14465 -0
  34. package/dist/admin/themes/corporate/manifest.json +7 -0
  35. package/dist/admin/themes/corporate/theme.css +2 -0
  36. package/dist/admin/themes/magazine/bundle.js +10229 -0
  37. package/dist/admin/themes/magazine/manifest.json +7 -0
  38. package/dist/admin/themes/magazine/theme.css +2 -0
  39. package/dist/admin/themes/marketplace-core/bundle.js +11213 -0
  40. package/dist/admin/themes/marketplace-core/manifest.json +7 -0
  41. package/dist/admin/themes/marketplace-core/theme.css +1613 -0
  42. package/dist/admin/themes/portfolio/bundle.js +9195 -0
  43. package/dist/admin/themes/portfolio/manifest.json +7 -0
  44. package/dist/admin/themes/portfolio/theme.css +2 -0
  45. package/dist/admin/themes/storefront/bundle.js +13172 -0
  46. package/dist/admin/themes/storefront/manifest.json +7 -0
  47. package/dist/admin/themes/storefront/theme.css +2 -0
  48. package/dist/agent/agentcms.mjs +1711 -0
  49. package/dist/agent/chunks/hosting-D_uj-Mt1.mjs +117 -0
  50. package/dist/agent/chunks/index-CrvEoyi0.mjs +3608 -0
  51. package/dist/agent/chunks/install-DkCCcKAO.mjs +577 -0
  52. package/dist/agent/chunks/plan-EYKegfhp.mjs +245 -0
  53. package/dist/agent/chunks/registry-lkfb_E9O.mjs +53517 -0
  54. package/dist/agent/chunks/siteAdd-BqcYt8nu.mjs +118 -0
  55. package/dist/agent/chunks/sql-wasm-GjJDiefO.mjs +10 -0
  56. package/dist/agent/chunks/sqljs-DjSCStJ0.mjs +20 -0
  57. package/dist/agent/vendor/sql-wasm.cjs +188 -0
  58. package/dist/theme-assets/corporate-menu.js +266 -0
  59. package/dist/theme-assets/corporate-posts.js +571 -0
  60. package/dist/theme-assets/corporate.css +2 -0
  61. package/dist/theme-assets/default-menu.js +266 -0
  62. package/dist/theme-assets/default-posts.js +378 -0
  63. package/dist/theme-assets/default.css +1 -0
  64. package/dist/theme-assets/magazine-menu.js +227 -0
  65. package/dist/theme-assets/magazine-posts.js +361 -0
  66. package/dist/theme-assets/magazine.css +2 -0
  67. package/dist/theme-assets/portfolio-filters.js +104 -0
  68. package/dist/theme-assets/portfolio-menu.js +240 -0
  69. package/dist/theme-assets/portfolio-posts.js +358 -0
  70. package/dist/theme-assets/portfolio.css +2 -0
  71. package/dist/theme-assets/storefront-catalog.js +504 -0
  72. package/dist/theme-assets/storefront-menu.js +239 -0
  73. package/dist/theme-assets/storefront-posts.js +404 -0
  74. package/dist/theme-assets/storefront.css +2 -0
  75. package/dist/version.json +9 -0
  76. package/package.json +101 -4
  77. package/plugin/.claude-plugin/plugin.json +16 -0
  78. package/plugin/skills/agentcms-install/SKILL.md +110 -0
  79. package/plugin/skills/agentcms-install/references/cloudflare.md +15 -0
  80. package/plugin/skills/agentcms-install/references/flexweg.md +11 -0
  81. package/plugin/skills/agentcms-install/references/github.md +67 -0
  82. package/plugin/skills/agentcms-install/references/gitlab.md +15 -0
  83. package/plugin/skills/agentcms-install/references/s3.md +81 -0
  84. package/plugin/skills/agentcms-manage/SKILL.md +92 -0
@@ -0,0 +1,1243 @@
1
+ import { sha256Hex as E, uploadFile as H, buildPostUrl as Z, markdownToPlainText as W, pickPublicLocale as $, i18n as C, useCmsData as V, formatDateTime as X, toast as T, fetchAllPosts as z, updatePluginConfig as B } from "@flexweg/cms-runtime";
2
+ import { jsxs as d, jsx as s } from "react/jsx-runtime";
3
+ import { forwardRef as O, createElement as A, useState as I, useEffect as Y } from "react";
4
+ import { useTranslation as ee } from "react-i18next";
5
+ const Q = {
6
+ title: "Search",
7
+ description: "Builds a public search-index.json over your posts (and optionally pages) and ships a runtime that opens a search modal anywhere a [data-cms-search] trigger appears in your theme.",
8
+ regenerationTarget: {
9
+ label: "Search index",
10
+ description: "Force-rebuild /search-index.json + /search.js runtime"
11
+ },
12
+ tabs: {
13
+ index: "Index",
14
+ behavior: "Behavior"
15
+ },
16
+ index: {
17
+ title: "What to index",
18
+ indexExcerpt: "Excerpts",
19
+ indexExcerptHelp: "When the post has no excerpt, we fall back to the first 200 characters of the body.",
20
+ indexCategory: "Category names",
21
+ indexTags: "Tag names",
22
+ includePages: "Include static pages",
23
+ excludedTerms: "Excluded categories",
24
+ excludedTermsHelp: "Posts whose primary category is selected here are skipped at indexing time.",
25
+ none: "None"
26
+ },
27
+ behavior: {
28
+ title: "Runtime behavior",
29
+ minQueryLength: "Minimum query length",
30
+ minQueryLengthHelp: "Below this number of characters, the modal shows nothing.",
31
+ maxResults: "Maximum results",
32
+ maxResultsHelp: "Hard cap on the number of results displayed at once."
33
+ },
34
+ status: {
35
+ notIndexed: "Not indexed yet",
36
+ lastIndexed: "Last indexed: {{date}}",
37
+ items: "{{count}} item",
38
+ items_plural: "{{count}} items"
39
+ },
40
+ actions: {
41
+ save: "Save",
42
+ saving: "Saving…",
43
+ saved: "Saved.",
44
+ forceRegenerate: "Force regenerate",
45
+ regenerating: "Regenerating…",
46
+ regenerated: "Search index regenerated.",
47
+ failed: "Save failed."
48
+ },
49
+ baseUrlMissing: "Site URL is empty in Settings → General. Search files cannot be uploaded until that's set.",
50
+ // Strings baked into search-index.json's `meta` block. The runtime JS
51
+ // reads them verbatim — no i18n library on the public side.
52
+ runtime: {
53
+ placeholder: "Search…",
54
+ close: "Close",
55
+ noResults: "No results",
56
+ dialogLabel: "Search"
57
+ }
58
+ }, ne = {
59
+ title: "Recherche",
60
+ description: "Génère un fichier search-index.json public à partir de vos articles (et optionnellement pages) et installe un runtime qui ouvre une modale de recherche partout où un déclencheur [data-cms-search] est présent dans votre thème.",
61
+ regenerationTarget: {
62
+ label: "Index de recherche",
63
+ description: "Reconstruit /search-index.json + le runtime /search.js"
64
+ },
65
+ tabs: {
66
+ index: "Index",
67
+ behavior: "Comportement"
68
+ },
69
+ index: {
70
+ title: "Ce qui est indexé",
71
+ indexExcerpt: "Extraits",
72
+ indexExcerptHelp: "Si l'article n'a pas d'extrait, on retombe sur les 200 premiers caractères du corps.",
73
+ indexCategory: "Noms des catégories",
74
+ indexTags: "Noms des tags",
75
+ includePages: "Inclure les pages statiques",
76
+ excludedTerms: "Catégories exclues",
77
+ excludedTermsHelp: "Les articles dont la catégorie principale est cochée ici sont ignorés à l'indexation.",
78
+ none: "Aucune"
79
+ },
80
+ behavior: {
81
+ title: "Comportement runtime",
82
+ minQueryLength: "Longueur minimale de requête",
83
+ minQueryLengthHelp: "En dessous de ce nombre de caractères, la modale n'affiche rien.",
84
+ maxResults: "Nombre maximum de résultats",
85
+ maxResultsHelp: "Plafond du nombre de résultats affichés simultanément."
86
+ },
87
+ status: {
88
+ notIndexed: "Pas encore indexé",
89
+ lastIndexed: "Dernière indexation : {{date}}",
90
+ items: "{{count}} élément",
91
+ items_plural: "{{count}} éléments"
92
+ },
93
+ actions: {
94
+ save: "Enregistrer",
95
+ saving: "Enregistrement…",
96
+ saved: "Enregistré.",
97
+ forceRegenerate: "Régénérer manuellement",
98
+ regenerating: "Régénération…",
99
+ regenerated: "Index de recherche régénéré.",
100
+ failed: "Échec de l'enregistrement."
101
+ },
102
+ baseUrlMissing: "L'URL du site est vide dans Réglages → Général. Les fichiers de recherche ne peuvent pas être uploadés tant qu'elle n'est pas renseignée.",
103
+ runtime: {
104
+ placeholder: "Rechercher…",
105
+ close: "Fermer",
106
+ noResults: "Aucun résultat",
107
+ dialogLabel: "Recherche"
108
+ }
109
+ }, te = {
110
+ title: "Suche",
111
+ description: "Erstellt eine öffentliche search-index.json über Ihre Beiträge (und optional Seiten) und liefert eine Laufzeit, die ein Suchfenster überall dort öffnet, wo ein [data-cms-search]-Trigger im Theme vorhanden ist.",
112
+ regenerationTarget: {
113
+ label: "Suchindex",
114
+ description: "/search-index.json + /search.js erzwungen neu erzeugen"
115
+ },
116
+ tabs: {
117
+ index: "Index",
118
+ behavior: "Verhalten"
119
+ },
120
+ index: {
121
+ title: "Was indexiert wird",
122
+ indexExcerpt: "Auszüge",
123
+ indexExcerptHelp: "Wenn der Beitrag keinen Auszug hat, verwenden wir die ersten 200 Zeichen des Inhalts.",
124
+ indexCategory: "Kategorienamen",
125
+ indexTags: "Tag-Namen",
126
+ includePages: "Statische Seiten einbeziehen",
127
+ excludedTerms: "Ausgeschlossene Kategorien",
128
+ excludedTermsHelp: "Beiträge, deren Hauptkategorie hier ausgewählt ist, werden bei der Indexierung übersprungen.",
129
+ none: "Keine"
130
+ },
131
+ behavior: {
132
+ title: "Laufzeitverhalten",
133
+ minQueryLength: "Mindestlänge der Anfrage",
134
+ minQueryLengthHelp: "Unterhalb dieser Zeichenanzahl zeigt das Suchfenster nichts an.",
135
+ maxResults: "Maximale Ergebnisse",
136
+ maxResultsHelp: "Hartes Limit für die Anzahl der gleichzeitig angezeigten Ergebnisse."
137
+ },
138
+ status: {
139
+ notIndexed: "Noch nicht indexiert",
140
+ lastIndexed: "Zuletzt indexiert: {{date}}",
141
+ items: "{{count}} Element",
142
+ items_plural: "{{count}} Elemente"
143
+ },
144
+ actions: {
145
+ save: "Speichern",
146
+ saving: "Speichern…",
147
+ saved: "Gespeichert.",
148
+ forceRegenerate: "Manuell neu generieren",
149
+ regenerating: "Neu generieren…",
150
+ regenerated: "Suchindex neu generiert.",
151
+ failed: "Speichern fehlgeschlagen."
152
+ },
153
+ baseUrlMissing: "Die Website-URL ist in Einstellungen → Allgemein leer. Die Suchdateien können nicht hochgeladen werden, bis das eingetragen ist.",
154
+ runtime: {
155
+ placeholder: "Suchen…",
156
+ close: "Schließen",
157
+ noResults: "Keine Ergebnisse",
158
+ dialogLabel: "Suche"
159
+ }
160
+ }, ae = {
161
+ title: "Búsqueda",
162
+ description: "Genera un archivo público search-index.json sobre tus artículos (y opcionalmente páginas) y entrega un runtime que abre una modal de búsqueda en cualquier disparador [data-cms-search] presente en tu tema.",
163
+ regenerationTarget: {
164
+ label: "Índice de búsqueda",
165
+ description: "Reconstruye /search-index.json + el runtime /search.js"
166
+ },
167
+ tabs: {
168
+ index: "Índice",
169
+ behavior: "Comportamiento"
170
+ },
171
+ index: {
172
+ title: "Qué se indexa",
173
+ indexExcerpt: "Extractos",
174
+ indexExcerptHelp: "Si el artículo no tiene extracto, usamos los primeros 200 caracteres del cuerpo.",
175
+ indexCategory: "Nombres de categorías",
176
+ indexTags: "Nombres de etiquetas",
177
+ includePages: "Incluir páginas estáticas",
178
+ excludedTerms: "Categorías excluidas",
179
+ excludedTermsHelp: "Los artículos cuya categoría principal esté seleccionada aquí se omiten en la indexación.",
180
+ none: "Ninguna"
181
+ },
182
+ behavior: {
183
+ title: "Comportamiento del runtime",
184
+ minQueryLength: "Longitud mínima de consulta",
185
+ minQueryLengthHelp: "Por debajo de esta cantidad de caracteres, la modal no muestra nada.",
186
+ maxResults: "Resultados máximos",
187
+ maxResultsHelp: "Límite estricto del número de resultados mostrados a la vez."
188
+ },
189
+ status: {
190
+ notIndexed: "Aún no indexado",
191
+ lastIndexed: "Última indexación: {{date}}",
192
+ items: "{{count}} elemento",
193
+ items_plural: "{{count}} elementos"
194
+ },
195
+ actions: {
196
+ save: "Guardar",
197
+ saving: "Guardando…",
198
+ saved: "Guardado.",
199
+ forceRegenerate: "Regenerar manualmente",
200
+ regenerating: "Regenerando…",
201
+ regenerated: "Índice de búsqueda regenerado.",
202
+ failed: "Error al guardar."
203
+ },
204
+ baseUrlMissing: "La URL del sitio está vacía en Ajustes → General. Los archivos de búsqueda no se pueden subir hasta que se rellene.",
205
+ runtime: {
206
+ placeholder: "Buscar…",
207
+ close: "Cerrar",
208
+ noResults: "Sin resultados",
209
+ dialogLabel: "Búsqueda"
210
+ }
211
+ }, se = {
212
+ title: "Zoeken",
213
+ description: "Bouwt een openbare search-index.json over je berichten (en optioneel pagina's) en levert een runtime die een zoekvenster opent overal waar een [data-cms-search]-trigger in je thema staat.",
214
+ regenerationTarget: {
215
+ label: "Zoekindex",
216
+ description: "Bouwt /search-index.json + de /search.js runtime opnieuw"
217
+ },
218
+ tabs: {
219
+ index: "Index",
220
+ behavior: "Gedrag"
221
+ },
222
+ index: {
223
+ title: "Wat geïndexeerd wordt",
224
+ indexExcerpt: "Samenvattingen",
225
+ indexExcerptHelp: "Als het bericht geen samenvatting heeft, vallen we terug op de eerste 200 tekens van de tekst.",
226
+ indexCategory: "Categorienamen",
227
+ indexTags: "Tag-namen",
228
+ includePages: "Statische pagina's meenemen",
229
+ excludedTerms: "Uitgesloten categorieën",
230
+ excludedTermsHelp: "Berichten waarvan de hoofdcategorie hier is geselecteerd, worden overgeslagen bij het indexeren.",
231
+ none: "Geen"
232
+ },
233
+ behavior: {
234
+ title: "Runtime-gedrag",
235
+ minQueryLength: "Minimale lengte zoekterm",
236
+ minQueryLengthHelp: "Onder dit aantal tekens toont het zoekvenster niets.",
237
+ maxResults: "Maximaal aantal resultaten",
238
+ maxResultsHelp: "Harde limiet op het aantal gelijktijdig getoonde resultaten."
239
+ },
240
+ status: {
241
+ notIndexed: "Nog niet geïndexeerd",
242
+ lastIndexed: "Laatst geïndexeerd: {{date}}",
243
+ items: "{{count}} item",
244
+ items_plural: "{{count}} items"
245
+ },
246
+ actions: {
247
+ save: "Opslaan",
248
+ saving: "Opslaan…",
249
+ saved: "Opgeslagen.",
250
+ forceRegenerate: "Handmatig regenereren",
251
+ regenerating: "Regenereren…",
252
+ regenerated: "Zoekindex geregenereerd.",
253
+ failed: "Opslaan mislukt."
254
+ },
255
+ baseUrlMissing: "De site-URL is leeg in Instellingen → Algemeen. Zoekbestanden kunnen pas worden geüpload als die ingevuld is.",
256
+ runtime: {
257
+ placeholder: "Zoeken…",
258
+ close: "Sluiten",
259
+ noResults: "Geen resultaten",
260
+ dialogLabel: "Zoeken"
261
+ }
262
+ }, re = {
263
+ title: "Pesquisa",
264
+ description: "Gera um arquivo público search-index.json sobre seus posts (e opcionalmente páginas) e entrega um runtime que abre um modal de pesquisa em qualquer gatilho [data-cms-search] do seu tema.",
265
+ regenerationTarget: {
266
+ label: "Índice de pesquisa",
267
+ description: "Reconstrói /search-index.json + o runtime /search.js"
268
+ },
269
+ tabs: {
270
+ index: "Índice",
271
+ behavior: "Comportamento"
272
+ },
273
+ index: {
274
+ title: "O que é indexado",
275
+ indexExcerpt: "Resumos",
276
+ indexExcerptHelp: "Se o post não tem resumo, usamos os primeiros 200 caracteres do conteúdo.",
277
+ indexCategory: "Nomes das categorias",
278
+ indexTags: "Nomes das tags",
279
+ includePages: "Incluir páginas estáticas",
280
+ excludedTerms: "Categorias excluídas",
281
+ excludedTermsHelp: "Posts cuja categoria principal esteja selecionada aqui são ignorados na indexação.",
282
+ none: "Nenhuma"
283
+ },
284
+ behavior: {
285
+ title: "Comportamento de runtime",
286
+ minQueryLength: "Comprimento mínimo da consulta",
287
+ minQueryLengthHelp: "Abaixo deste número de caracteres, o modal não mostra nada.",
288
+ maxResults: "Resultados máximos",
289
+ maxResultsHelp: "Limite rígido do número de resultados exibidos por vez."
290
+ },
291
+ status: {
292
+ notIndexed: "Ainda não indexado",
293
+ lastIndexed: "Última indexação: {{date}}",
294
+ items: "{{count}} item",
295
+ items_plural: "{{count}} itens"
296
+ },
297
+ actions: {
298
+ save: "Salvar",
299
+ saving: "Salvando…",
300
+ saved: "Salvo.",
301
+ forceRegenerate: "Regenerar manualmente",
302
+ regenerating: "Regenerando…",
303
+ regenerated: "Índice de pesquisa regenerado.",
304
+ failed: "Falha ao salvar."
305
+ },
306
+ baseUrlMissing: "A URL do site está vazia em Configurações → Geral. Os arquivos de pesquisa não podem ser enviados até que ela esteja preenchida.",
307
+ runtime: {
308
+ placeholder: "Pesquisar…",
309
+ close: "Fechar",
310
+ noResults: "Sem resultados",
311
+ dialogLabel: "Pesquisa"
312
+ }
313
+ }, ie = {
314
+ title: "검색",
315
+ description: "게시물(선택적으로 페이지 포함)에 대한 공개 search-index.json을 빌드하고 테마의 [data-cms-search] 트리거가 있는 곳에서 검색 모달을 여는 런타임을 제공합니다.",
316
+ regenerationTarget: {
317
+ label: "검색 인덱스",
318
+ description: "/search-index.json + /search.js 런타임 강제 재빌드"
319
+ },
320
+ tabs: {
321
+ index: "인덱스",
322
+ behavior: "동작"
323
+ },
324
+ index: {
325
+ title: "인덱싱 대상",
326
+ indexExcerpt: "요약",
327
+ indexExcerptHelp: "게시물에 요약이 없으면 본문의 첫 200자를 사용합니다.",
328
+ indexCategory: "카테고리 이름",
329
+ indexTags: "태그 이름",
330
+ includePages: "정적 페이지 포함",
331
+ excludedTerms: "제외할 카테고리",
332
+ excludedTermsHelp: "기본 카테고리가 여기에서 선택된 게시물은 인덱싱 시 건너뜁니다.",
333
+ none: "없음"
334
+ },
335
+ behavior: {
336
+ title: "런타임 동작",
337
+ minQueryLength: "최소 쿼리 길이",
338
+ minQueryLengthHelp: "이 글자 수 미만이면 모달이 아무것도 표시하지 않습니다.",
339
+ maxResults: "최대 결과 수",
340
+ maxResultsHelp: "한 번에 표시되는 결과 수의 절대 상한."
341
+ },
342
+ status: {
343
+ notIndexed: "아직 인덱싱되지 않음",
344
+ lastIndexed: "마지막 인덱싱: {{date}}",
345
+ items: "{{count}}개 항목",
346
+ items_plural: "{{count}}개 항목"
347
+ },
348
+ actions: {
349
+ save: "저장",
350
+ saving: "저장 중…",
351
+ saved: "저장됨.",
352
+ forceRegenerate: "수동으로 재생성",
353
+ regenerating: "재생성 중…",
354
+ regenerated: "검색 인덱스가 재생성되었습니다.",
355
+ failed: "저장 실패."
356
+ },
357
+ baseUrlMissing: "설정 → 일반에서 사이트 URL이 비어 있습니다. URL이 설정될 때까지 검색 파일을 업로드할 수 없습니다.",
358
+ runtime: {
359
+ placeholder: "검색…",
360
+ close: "닫기",
361
+ noResults: "결과 없음",
362
+ dialogLabel: "검색"
363
+ }
364
+ }, S = {
365
+ indexExcerpt: !0,
366
+ indexCategory: !0,
367
+ indexTags: !1,
368
+ includePages: !1,
369
+ excludedTermIds: [],
370
+ minQueryLength: 2,
371
+ maxResults: 20
372
+ }, R = "flexweg-search", D = "search-index.json", q = "search.js", N = `/*
373
+ * flexweg-search runtime — public-site code.
374
+ *
375
+ * Auto-attaches to every \`[data-cms-search]\` element at DOMContentLoaded.
376
+ * On click: lazy-fetches \`/search-index.json\` (cached), opens a modal
377
+ * with an input + results list. Multi-token substring search across the
378
+ * fields the indexer chose to include (title always, optionally
379
+ * excerpt / category / tags). Title hits are weighted higher.
380
+ *
381
+ * Runtime UI strings (placeholder, "no results", aria labels) and
382
+ * thresholds (minQueryLength, maxResults) live in the index file's
383
+ * \`meta\` block — the indexer bakes them in at build time so the
384
+ * runtime doesn't need an i18n library.
385
+ *
386
+ * Default styles are injected on first open via a single <style> tag.
387
+ * Themes can override anything by targeting \`.cms-search-*\` classes
388
+ * with higher specificity (or wrapping rules in \`:where()\`).
389
+ */
390
+
391
+ (function () {
392
+ if (typeof document === "undefined") return;
393
+
394
+ // ─── Base path ────────────────────────────────────────────────────
395
+ // Sites served from a sub-folder (e.g. GitHub Pages project sites)
396
+ // carry <meta name="x-cms-base" content="/repo/"> in <head>; no meta
397
+ // means the site lives at the domain root. URLs built at runtime from
398
+ // JSON data go through withBase() so they land under that folder.
399
+ var BASE = (function () {
400
+ var meta = document.querySelector('meta[name="x-cms-base"]');
401
+ var b = (meta && meta.getAttribute("content")) || "/";
402
+ if (b.charAt(0) !== "/") b = "/" + b;
403
+ if (b.charAt(b.length - 1) !== "/") b += "/";
404
+ return b;
405
+ })();
406
+
407
+ function withBase(url) {
408
+ if (BASE === "/" || !url) return url;
409
+ // Absolute URLs (http:, mailto:, data:, …), protocol-relative and
410
+ // fragment-only links pass through.
411
+ if (/^([a-z][a-z0-9+.-]*:|\\/\\/|#)/i.test(url)) return url;
412
+ if (url.indexOf(BASE) === 0 || url === BASE.slice(0, -1)) return url;
413
+ return BASE + (url.charAt(0) === "/" ? url.slice(1) : url);
414
+ }
415
+
416
+ var INDEX_URL = withBase("/search-index.json");
417
+ var STYLES = [
418
+ ".cms-search-modal{position:fixed;inset:0;z-index:9999;display:flex;align-items:flex-start;justify-content:center;padding:8vh 16px 16px;opacity:0;transition:opacity .15s ease}",
419
+ ".cms-search-modal.open{opacity:1}",
420
+ ".cms-search-backdrop{position:absolute;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px)}",
421
+ ".cms-search-panel{position:relative;width:100%;max-width:640px;background:#fff;color:#0f172a;border-radius:8px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);display:flex;flex-direction:column;max-height:80vh;overflow:hidden;font-family:inherit}",
422
+ ".cms-search-header{display:flex;gap:8px;padding:12px 14px;border-bottom:1px solid #e2e8f0;align-items:center}",
423
+ ".cms-search-header input{flex:1;border:0;outline:0;font-size:18px;background:transparent;color:inherit;padding:8px 4px}",
424
+ ".cms-search-header button{border:0;background:transparent;color:#64748b;font-size:24px;line-height:1;width:32px;height:32px;border-radius:4px;cursor:pointer}",
425
+ ".cms-search-header button:hover{background:#f1f5f9;color:#0f172a}",
426
+ ".cms-search-results{flex:1;overflow-y:auto;padding:4px 0}",
427
+ ".cms-search-result{display:block;padding:12px 14px;border-bottom:1px solid #f1f5f9;text-decoration:none;color:inherit}",
428
+ ".cms-search-result:hover,.cms-search-result.is-active{background:#f8fafc}",
429
+ ".cms-search-result-title{display:block;font-weight:600;font-size:15px;line-height:1.4;margin-bottom:2px}",
430
+ ".cms-search-result-excerpt{display:block;font-size:13px;line-height:1.5;color:#475569}",
431
+ ".cms-search-result-category{display:inline-block;margin-top:6px;font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:#64748b}",
432
+ ".cms-search-result mark{background:#fef08a;color:inherit;padding:0 1px;border-radius:2px}",
433
+ ".cms-search-empty{padding:24px;text-align:center;color:#64748b;font-size:14px}",
434
+ ].join("");
435
+
436
+ var stylesInjected = false;
437
+ function injectStyles() {
438
+ if (stylesInjected) return;
439
+ stylesInjected = true;
440
+ var s = document.createElement("style");
441
+ s.setAttribute("data-cms-search-styles", "");
442
+ s.appendChild(document.createTextNode(STYLES));
443
+ document.head.appendChild(s);
444
+ }
445
+
446
+ // The fetched index is cached after the first open so reopening is
447
+ // instantaneous. A null \`data\` field means we already attempted and
448
+ // failed (network / parse error); we render an empty modal in that
449
+ // case rather than hammering the server on every reopen.
450
+ var indexCache = null;
451
+ async function loadIndex() {
452
+ if (indexCache) return indexCache;
453
+ try {
454
+ var res = await fetch(INDEX_URL, { credentials: "same-origin" });
455
+ if (!res.ok) throw new Error("HTTP " + res.status);
456
+ var data = await res.json();
457
+ indexCache = data && Array.isArray(data.items) ? data : { meta: {}, items: [] };
458
+ } catch (e) {
459
+ console.warn("[flexweg-search] failed to load index:", e);
460
+ indexCache = { meta: {}, items: [] };
461
+ }
462
+ return indexCache;
463
+ }
464
+
465
+ function tokenize(q) {
466
+ return q.toLowerCase().trim().split(/\\s+/).filter(Boolean);
467
+ }
468
+
469
+ function buildHaystack(item) {
470
+ var parts = [item.title];
471
+ if (item.excerpt) parts.push(item.excerpt);
472
+ if (item.category) parts.push(item.category);
473
+ if (item.tags && item.tags.length) parts.push(item.tags.join(" "));
474
+ return parts.join(" ").toLowerCase();
475
+ }
476
+
477
+ function score(item, tokens) {
478
+ var titleLower = item.title.toLowerCase();
479
+ var s = 0;
480
+ for (var i = 0; i < tokens.length; i++) {
481
+ var t = tokens[i];
482
+ if (titleLower.indexOf(t) !== -1) s += 10;
483
+ if (titleLower.indexOf(t) === 0) s += 5;
484
+ }
485
+ return s;
486
+ }
487
+
488
+ function search(items, tokens, max) {
489
+ var matched = [];
490
+ for (var i = 0; i < items.length; i++) {
491
+ var hay = buildHaystack(items[i]);
492
+ var ok = true;
493
+ for (var j = 0; j < tokens.length; j++) {
494
+ if (hay.indexOf(tokens[j]) === -1) { ok = false; break; }
495
+ }
496
+ if (ok) matched.push(items[i]);
497
+ }
498
+ matched.sort(function (a, b) { return score(b, tokens) - score(a, tokens); });
499
+ return matched.slice(0, max);
500
+ }
501
+
502
+ function escapeHtml(s) {
503
+ return String(s).replace(/[&<>"]/g, function (c) {
504
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
505
+ });
506
+ }
507
+
508
+ function escapeRegex(s) {
509
+ return s.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&");
510
+ }
511
+
512
+ function highlight(text, tokens) {
513
+ var out = escapeHtml(text);
514
+ for (var i = 0; i < tokens.length; i++) {
515
+ var re = new RegExp("(" + escapeRegex(tokens[i]) + ")", "gi");
516
+ out = out.replace(re, "<mark>$1</mark>");
517
+ }
518
+ return out;
519
+ }
520
+
521
+ function renderResults(container, items, tokens) {
522
+ if (!items.length) {
523
+ container.innerHTML = "";
524
+ return 0;
525
+ }
526
+ var html = "";
527
+ for (var i = 0; i < items.length; i++) {
528
+ var it = items[i];
529
+ var titleHtml = highlight(it.title, tokens);
530
+ var excerptHtml = it.excerpt ? '<span class="cms-search-result-excerpt">' + highlight(it.excerpt, tokens) + "</span>" : "";
531
+ var categoryHtml = it.category ? '<span class="cms-search-result-category">' + escapeHtml(it.category) + "</span>" : "";
532
+ html += '<a class="cms-search-result" href="' + escapeHtml(withBase("/" + it.url)) + '" data-cms-search-result>'
533
+ + '<span class="cms-search-result-title">' + titleHtml + "</span>"
534
+ + excerptHtml
535
+ + categoryHtml
536
+ + "</a>";
537
+ }
538
+ container.innerHTML = html;
539
+ return items.length;
540
+ }
541
+
542
+ var modal = null;
543
+ function close() {
544
+ if (!modal) return;
545
+ var m = modal;
546
+ modal = null;
547
+ m.classList.remove("open");
548
+ m.addEventListener("transitionend", function () {
549
+ if (m.parentNode) m.parentNode.removeChild(m);
550
+ }, { once: true });
551
+ document.removeEventListener("keydown", onKeydown);
552
+ }
553
+
554
+ function onKeydown(e) {
555
+ if (!modal) return;
556
+ if (e.key === "Escape") {
557
+ e.preventDefault();
558
+ close();
559
+ }
560
+ }
561
+
562
+ async function open() {
563
+ if (modal) return;
564
+ injectStyles();
565
+ var data = await loadIndex();
566
+ var meta = data.meta || {};
567
+ var minQuery = typeof meta.minQueryLength === "number" ? meta.minQueryLength : 2;
568
+ var maxResults = typeof meta.maxResults === "number" ? meta.maxResults : 20;
569
+ var placeholder = meta.placeholder || "Search…";
570
+ var closeLabel = meta.close || "Close";
571
+ var noResults = meta.noResults || "No results";
572
+ var dialogLabel = meta.dialogLabel || "Search";
573
+
574
+ var m = document.createElement("div");
575
+ m.className = "cms-search-modal";
576
+ m.setAttribute("role", "dialog");
577
+ m.setAttribute("aria-modal", "true");
578
+ m.setAttribute("aria-label", dialogLabel);
579
+
580
+ m.innerHTML = ''
581
+ + '<div class="cms-search-backdrop" data-cms-search-close></div>'
582
+ + '<div class="cms-search-panel">'
583
+ + '<div class="cms-search-header">'
584
+ + '<input type="search" autocomplete="off" placeholder="' + escapeHtml(placeholder) + '" aria-label="' + escapeHtml(placeholder) + '" />'
585
+ + '<button type="button" data-cms-search-close aria-label="' + escapeHtml(closeLabel) + '">×</button>'
586
+ + '</div>'
587
+ + '<div class="cms-search-results" role="listbox"></div>'
588
+ + '<div class="cms-search-empty" hidden>' + escapeHtml(noResults) + '</div>'
589
+ + '</div>';
590
+
591
+ document.body.appendChild(m);
592
+ modal = m;
593
+
594
+ var closers = m.querySelectorAll("[data-cms-search-close]");
595
+ for (var i = 0; i < closers.length; i++) closers[i].addEventListener("click", close);
596
+
597
+ var input = m.querySelector("input");
598
+ var results = m.querySelector(".cms-search-results");
599
+ var empty = m.querySelector(".cms-search-empty");
600
+
601
+ input.addEventListener("input", function () {
602
+ var q = input.value;
603
+ if (q.length < minQuery) {
604
+ results.innerHTML = "";
605
+ empty.hidden = true;
606
+ return;
607
+ }
608
+ var tokens = tokenize(q);
609
+ if (!tokens.length) {
610
+ results.innerHTML = "";
611
+ empty.hidden = true;
612
+ return;
613
+ }
614
+ var hits = search(data.items, tokens, maxResults);
615
+ var n = renderResults(results, hits, tokens);
616
+ empty.hidden = n > 0;
617
+ });
618
+
619
+ document.addEventListener("keydown", onKeydown);
620
+
621
+ // Trigger CSS transition on the next frame so opacity:0 → 1 plays.
622
+ requestAnimationFrame(function () {
623
+ m.classList.add("open");
624
+ input.focus();
625
+ });
626
+ }
627
+
628
+ function attachTriggers() {
629
+ var triggers = document.querySelectorAll("[data-cms-search]");
630
+ for (var i = 0; i < triggers.length; i++) {
631
+ triggers[i].addEventListener("click", function (e) {
632
+ e.preventDefault();
633
+ open();
634
+ });
635
+ }
636
+ }
637
+
638
+ if (document.readyState === "loading") {
639
+ document.addEventListener("DOMContentLoaded", attachTriggers);
640
+ } else {
641
+ attachTriggers();
642
+ }
643
+ })();
644
+ `;
645
+ function oe(t) {
646
+ const n = $(t), e = R, r = Q.runtime;
647
+ return {
648
+ placeholder: C.getResource(n, e, "runtime.placeholder") ?? r.placeholder,
649
+ close: C.getResource(n, e, "runtime.close") ?? r.close,
650
+ noResults: C.getResource(n, e, "runtime.noResults") ?? r.noResults,
651
+ dialogLabel: C.getResource(n, e, "runtime.dialogLabel") ?? r.dialogLabel
652
+ };
653
+ }
654
+ function F(t) {
655
+ const { posts: n, pages: e, terms: r, settings: m, config: u } = t, k = new Set(u.excludedTermIds), b = [];
656
+ for (const i of n)
657
+ i.status === "online" && (i.primaryTermId && k.has(i.primaryTermId) || b.push(i));
658
+ if (u.includePages)
659
+ for (const i of e)
660
+ i.status === "online" && b.push(i);
661
+ b.sort((i, c) => {
662
+ var g, w, p, a, o, L, x, P;
663
+ const f = ((w = (g = c.publishedAt) == null ? void 0 : g.toMillis) == null ? void 0 : w.call(g)) ?? ((a = (p = c.updatedAt) == null ? void 0 : p.toMillis) == null ? void 0 : a.call(p)) ?? 0, h = ((L = (o = i.publishedAt) == null ? void 0 : o.toMillis) == null ? void 0 : L.call(o)) ?? ((P = (x = i.updatedAt) == null ? void 0 : x.toMillis) == null ? void 0 : P.call(x)) ?? 0;
664
+ return f - h;
665
+ });
666
+ const l = /* @__PURE__ */ new Map();
667
+ for (const i of r) l.set(i.id, i);
668
+ const v = b.map((i) => {
669
+ var g, w, p, a;
670
+ const c = i.primaryTermId ? l.get(i.primaryTermId) : void 0, f = {
671
+ id: i.id,
672
+ title: i.title,
673
+ url: Z({ post: i, primaryTerm: (c == null ? void 0 : c.type) === "category" ? c : void 0 })
674
+ };
675
+ if (u.indexExcerpt) {
676
+ const o = i.excerpt && i.excerpt.trim() || W(i.contentMarkdown ?? "", 200);
677
+ o && (f.excerpt = o);
678
+ }
679
+ if (u.indexCategory && (c == null ? void 0 : c.type) === "category" && (f.category = c.name), u.indexTags && i.termIds && i.termIds.length) {
680
+ const o = [];
681
+ for (const L of i.termIds) {
682
+ const x = l.get(L);
683
+ x && x.type === "tag" && o.push(x.name);
684
+ }
685
+ o.length && (f.tags = o);
686
+ }
687
+ const h = ((w = (g = i.publishedAt) == null ? void 0 : g.toMillis) == null ? void 0 : w.call(g)) ?? ((a = (p = i.updatedAt) == null ? void 0 : p.toMillis) == null ? void 0 : a.call(p));
688
+ return typeof h == "number" && (f.publishedAt = h), f;
689
+ });
690
+ return { meta: {
691
+ minQueryLength: u.minQueryLength,
692
+ maxResults: u.maxResults,
693
+ ...oe(m.language),
694
+ generatedAt: Date.now()
695
+ }, items: v };
696
+ }
697
+ function G(t) {
698
+ const n = {
699
+ meta: { ...t.meta, generatedAt: 0 },
700
+ items: t.items
701
+ };
702
+ return E(JSON.stringify(n));
703
+ }
704
+ async function le(t) {
705
+ const n = F({
706
+ posts: t.posts,
707
+ pages: t.pages,
708
+ terms: t.terms,
709
+ settings: t.settings,
710
+ config: t.config
711
+ }), e = await G(n), r = { ...S, ...t.config };
712
+ return r.lastIndexHash === e ? { nextConfig: r, uploaded: !1 } : (await H({
713
+ path: D,
714
+ content: JSON.stringify(n),
715
+ encoding: "utf-8"
716
+ }), {
717
+ nextConfig: { ...r, lastIndexHash: e, lastIndexedAt: Date.now() },
718
+ uploaded: !0
719
+ });
720
+ }
721
+ async function ce(t) {
722
+ const n = await E(N), e = { ...S, ...t };
723
+ return e.lastRuntimeHash === n ? { nextConfig: e, uploaded: !1 } : (await H({
724
+ path: q,
725
+ content: N,
726
+ encoding: "utf-8"
727
+ }), { nextConfig: { ...e, lastRuntimeHash: n }, uploaded: !0 });
728
+ }
729
+ async function _(t) {
730
+ const n = F({
731
+ posts: t.posts,
732
+ pages: t.pages,
733
+ terms: t.terms,
734
+ settings: t.settings,
735
+ config: t.config
736
+ }), e = await G(n), r = await E(N);
737
+ return await H({
738
+ path: D,
739
+ content: JSON.stringify(n),
740
+ encoding: "utf-8"
741
+ }), await H({
742
+ path: q,
743
+ content: N,
744
+ encoding: "utf-8"
745
+ }), {
746
+ nextConfig: {
747
+ ...S,
748
+ ...t.config,
749
+ lastIndexHash: e,
750
+ lastRuntimeHash: r,
751
+ lastIndexedAt: Date.now()
752
+ },
753
+ uploaded: !0
754
+ };
755
+ }
756
+ /**
757
+ * @license lucide-react v0.460.0 - ISC
758
+ *
759
+ * This source code is licensed under the ISC license.
760
+ * See the LICENSE file in the root directory of this source tree.
761
+ */
762
+ const de = (t) => t.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), K = (...t) => t.filter((n, e, r) => !!n && n.trim() !== "" && r.indexOf(n) === e).join(" ").trim();
763
+ /**
764
+ * @license lucide-react v0.460.0 - ISC
765
+ *
766
+ * This source code is licensed under the ISC license.
767
+ * See the LICENSE file in the root directory of this source tree.
768
+ */
769
+ var ue = {
770
+ xmlns: "http://www.w3.org/2000/svg",
771
+ width: 24,
772
+ height: 24,
773
+ viewBox: "0 0 24 24",
774
+ fill: "none",
775
+ stroke: "currentColor",
776
+ strokeWidth: 2,
777
+ strokeLinecap: "round",
778
+ strokeLinejoin: "round"
779
+ };
780
+ /**
781
+ * @license lucide-react v0.460.0 - ISC
782
+ *
783
+ * This source code is licensed under the ISC license.
784
+ * See the LICENSE file in the root directory of this source tree.
785
+ */
786
+ const me = O(
787
+ ({
788
+ color: t = "currentColor",
789
+ size: n = 24,
790
+ strokeWidth: e = 2,
791
+ absoluteStrokeWidth: r,
792
+ className: m = "",
793
+ children: u,
794
+ iconNode: k,
795
+ ...b
796
+ }, l) => A(
797
+ "svg",
798
+ {
799
+ ref: l,
800
+ ...ue,
801
+ width: n,
802
+ height: n,
803
+ stroke: t,
804
+ strokeWidth: r ? Number(e) * 24 / Number(n) : e,
805
+ className: K("lucide", m),
806
+ ...b
807
+ },
808
+ [
809
+ ...k.map(([v, y]) => A(v, y)),
810
+ ...Array.isArray(u) ? u : [u]
811
+ ]
812
+ )
813
+ );
814
+ /**
815
+ * @license lucide-react v0.460.0 - ISC
816
+ *
817
+ * This source code is licensed under the ISC license.
818
+ * See the LICENSE file in the root directory of this source tree.
819
+ */
820
+ const M = (t, n) => {
821
+ const e = O(
822
+ ({ className: r, ...m }, u) => A(me, {
823
+ ref: u,
824
+ iconNode: n,
825
+ className: K(`lucide-${de(t)}`, r),
826
+ ...m
827
+ })
828
+ );
829
+ return e.displayName = `${t}`, e;
830
+ };
831
+ /**
832
+ * @license lucide-react v0.460.0 - ISC
833
+ *
834
+ * This source code is licensed under the ISC license.
835
+ * See the LICENSE file in the root directory of this source tree.
836
+ */
837
+ const U = M("LoaderCircle", [
838
+ ["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]
839
+ ]);
840
+ /**
841
+ * @license lucide-react v0.460.0 - ISC
842
+ *
843
+ * This source code is licensed under the ISC license.
844
+ * See the LICENSE file in the root directory of this source tree.
845
+ */
846
+ const he = M("RefreshCw", [
847
+ ["path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8", key: "v9h5vc" }],
848
+ ["path", { d: "M21 3v5h-5", key: "1q7to0" }],
849
+ ["path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16", key: "3uifl3" }],
850
+ ["path", { d: "M8 16H3v5", key: "1cv678" }]
851
+ ]);
852
+ /**
853
+ * @license lucide-react v0.460.0 - ISC
854
+ *
855
+ * This source code is licensed under the ISC license.
856
+ * See the LICENSE file in the root directory of this source tree.
857
+ */
858
+ const ge = M("Save", [
859
+ [
860
+ "path",
861
+ {
862
+ d: "M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z",
863
+ key: "1c8476"
864
+ }
865
+ ],
866
+ ["path", { d: "M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7", key: "1ydtos" }],
867
+ ["path", { d: "M7 3v4a1 1 0 0 0 1 1h7", key: "t51u73" }]
868
+ ]);
869
+ function pe({
870
+ config: t,
871
+ save: n
872
+ }) {
873
+ const { t: e, i18n: r } = ee("flexweg-search"), { settings: m, terms: u, categories: k } = V(), b = r.resolvedLanguage ?? r.language, [l, v] = I(t);
874
+ Y(() => v(t), [t]);
875
+ const [y, i] = I(!1), [c, f] = I(!1);
876
+ function h(a) {
877
+ v((o) => ({ ...o, ...a }));
878
+ }
879
+ function g(a) {
880
+ const o = new Set(l.excludedTermIds);
881
+ o.has(a) ? o.delete(a) : o.add(a), h({ excludedTermIds: [...o] });
882
+ }
883
+ async function w() {
884
+ i(!0);
885
+ try {
886
+ await n(l), T.success(e("actions.saved"));
887
+ } catch (a) {
888
+ console.error("[flexweg-search] save failed:", a), T.error(e("actions.failed"));
889
+ } finally {
890
+ i(!1);
891
+ }
892
+ }
893
+ async function p() {
894
+ if (!m.baseUrl) {
895
+ T.error(e("baseUrlMissing"));
896
+ return;
897
+ }
898
+ f(!0);
899
+ try {
900
+ const [a, o] = await Promise.all([
901
+ z({ type: "post" }),
902
+ z({ type: "page" })
903
+ ]), x = await _({
904
+ posts: a,
905
+ pages: o,
906
+ terms: u,
907
+ settings: m,
908
+ config: l
909
+ });
910
+ await n(x.nextConfig), v(x.nextConfig), T.success(e("actions.regenerated"));
911
+ } catch (a) {
912
+ console.error("[flexweg-search] regeneration failed:", a), T.error(e("actions.failed"));
913
+ } finally {
914
+ f(!1);
915
+ }
916
+ }
917
+ return /* @__PURE__ */ d("div", { className: "space-y-6", children: [
918
+ /* @__PURE__ */ s("p", { className: "text-sm text-surface-600 dark:text-surface-300", children: e("description") }),
919
+ /* @__PURE__ */ d("section", { className: "card p-4 space-y-4", children: [
920
+ /* @__PURE__ */ s("h2", { className: "font-semibold", children: e("index.title") }),
921
+ /* @__PURE__ */ d("label", { className: "flex items-start gap-2", children: [
922
+ /* @__PURE__ */ s(
923
+ "input",
924
+ {
925
+ type: "checkbox",
926
+ className: "mt-1",
927
+ checked: l.indexExcerpt,
928
+ onChange: (a) => h({ indexExcerpt: a.target.checked })
929
+ }
930
+ ),
931
+ /* @__PURE__ */ d("span", { children: [
932
+ /* @__PURE__ */ s("span", { className: "text-sm font-medium block", children: e("index.indexExcerpt") }),
933
+ /* @__PURE__ */ s("span", { className: "text-xs text-surface-500 dark:text-surface-400", children: e("index.indexExcerptHelp") })
934
+ ] })
935
+ ] }),
936
+ /* @__PURE__ */ d("label", { className: "flex items-center gap-2", children: [
937
+ /* @__PURE__ */ s(
938
+ "input",
939
+ {
940
+ type: "checkbox",
941
+ checked: l.indexCategory,
942
+ onChange: (a) => h({ indexCategory: a.target.checked })
943
+ }
944
+ ),
945
+ /* @__PURE__ */ s("span", { className: "text-sm font-medium", children: e("index.indexCategory") })
946
+ ] }),
947
+ /* @__PURE__ */ d("label", { className: "flex items-center gap-2", children: [
948
+ /* @__PURE__ */ s(
949
+ "input",
950
+ {
951
+ type: "checkbox",
952
+ checked: l.indexTags,
953
+ onChange: (a) => h({ indexTags: a.target.checked })
954
+ }
955
+ ),
956
+ /* @__PURE__ */ s("span", { className: "text-sm font-medium", children: e("index.indexTags") })
957
+ ] }),
958
+ /* @__PURE__ */ d("label", { className: "flex items-center gap-2 pt-2 border-t border-surface-200 dark:border-surface-700", children: [
959
+ /* @__PURE__ */ s(
960
+ "input",
961
+ {
962
+ type: "checkbox",
963
+ checked: l.includePages,
964
+ onChange: (a) => h({ includePages: a.target.checked })
965
+ }
966
+ ),
967
+ /* @__PURE__ */ s("span", { className: "text-sm font-medium", children: e("index.includePages") })
968
+ ] }),
969
+ /* @__PURE__ */ d("div", { children: [
970
+ /* @__PURE__ */ s("p", { className: "label", children: e("index.excludedTerms") }),
971
+ /* @__PURE__ */ s("p", { className: "text-xs text-surface-500 mb-2 dark:text-surface-400", children: e("index.excludedTermsHelp") }),
972
+ k.length === 0 ? /* @__PURE__ */ s("p", { className: "text-xs italic text-surface-500 dark:text-surface-400", children: e("index.none") }) : /* @__PURE__ */ s("ul", { className: "space-y-1", children: k.map((a) => /* @__PURE__ */ s("li", { children: /* @__PURE__ */ d("label", { className: "flex items-center gap-2", children: [
973
+ /* @__PURE__ */ s(
974
+ "input",
975
+ {
976
+ type: "checkbox",
977
+ checked: l.excludedTermIds.includes(a.id),
978
+ onChange: () => g(a.id)
979
+ }
980
+ ),
981
+ /* @__PURE__ */ s("span", { className: "text-sm", children: a.name }),
982
+ /* @__PURE__ */ d("span", { className: "text-xs text-surface-400", children: [
983
+ "/",
984
+ a.slug
985
+ ] })
986
+ ] }) }, a.id)) })
987
+ ] })
988
+ ] }),
989
+ /* @__PURE__ */ d("section", { className: "card p-4 space-y-4", children: [
990
+ /* @__PURE__ */ s("h2", { className: "font-semibold", children: e("behavior.title") }),
991
+ /* @__PURE__ */ d("div", { children: [
992
+ /* @__PURE__ */ s("label", { className: "label", children: e("behavior.minQueryLength") }),
993
+ /* @__PURE__ */ s(
994
+ "input",
995
+ {
996
+ type: "number",
997
+ className: "input max-w-xs",
998
+ min: 1,
999
+ max: 10,
1000
+ value: l.minQueryLength,
1001
+ onChange: (a) => h({
1002
+ minQueryLength: Math.max(
1003
+ 1,
1004
+ Math.min(10, Number.parseInt(a.target.value, 10) || 2)
1005
+ )
1006
+ })
1007
+ }
1008
+ ),
1009
+ /* @__PURE__ */ s("p", { className: "text-xs text-surface-500 mt-1 dark:text-surface-400", children: e("behavior.minQueryLengthHelp") })
1010
+ ] }),
1011
+ /* @__PURE__ */ d("div", { children: [
1012
+ /* @__PURE__ */ s("label", { className: "label", children: e("behavior.maxResults") }),
1013
+ /* @__PURE__ */ s(
1014
+ "input",
1015
+ {
1016
+ type: "number",
1017
+ className: "input max-w-xs",
1018
+ min: 1,
1019
+ max: 100,
1020
+ value: l.maxResults,
1021
+ onChange: (a) => h({
1022
+ maxResults: Math.max(
1023
+ 1,
1024
+ Math.min(100, Number.parseInt(a.target.value, 10) || 20)
1025
+ )
1026
+ })
1027
+ }
1028
+ ),
1029
+ /* @__PURE__ */ s("p", { className: "text-xs text-surface-500 mt-1 dark:text-surface-400", children: e("behavior.maxResultsHelp") })
1030
+ ] })
1031
+ ] }),
1032
+ /* @__PURE__ */ d("section", { className: "card p-4 flex flex-wrap items-center gap-3", children: [
1033
+ /* @__PURE__ */ s("p", { className: "text-xs text-surface-500 dark:text-surface-400 flex-1 min-w-0", children: l.lastIndexedAt ? e("status.lastIndexed", { date: X(l.lastIndexedAt, b) }) : e("status.notIndexed") }),
1034
+ /* @__PURE__ */ s(
1035
+ "button",
1036
+ {
1037
+ type: "button",
1038
+ className: "btn btn-secondary",
1039
+ onClick: p,
1040
+ disabled: c || y,
1041
+ children: /* @__PURE__ */ d("span", { className: "inline-flex items-center justify-center gap-1.5", children: [
1042
+ /* @__PURE__ */ s(U, { className: "w-4 h-4 animate-spin " + (c ? "" : "hidden") }),
1043
+ /* @__PURE__ */ s(he, { className: "w-4 h-4 " + (c ? "hidden" : "") }),
1044
+ /* @__PURE__ */ s("span", { children: e(c ? "actions.regenerating" : "actions.forceRegenerate") })
1045
+ ] })
1046
+ }
1047
+ ),
1048
+ /* @__PURE__ */ d(
1049
+ "button",
1050
+ {
1051
+ type: "button",
1052
+ className: "btn btn-primary",
1053
+ onClick: w,
1054
+ disabled: y || c,
1055
+ children: [
1056
+ /* @__PURE__ */ s(U, { className: y ? "w-4 h-4 animate-spin" : "hidden" }),
1057
+ /* @__PURE__ */ s(ge, { className: y ? "hidden" : "w-4 h-4" }),
1058
+ /* @__PURE__ */ s("span", { children: e(y ? "actions.saving" : "actions.save") })
1059
+ ]
1060
+ }
1061
+ )
1062
+ ] })
1063
+ ] });
1064
+ }
1065
+ const xe = `# Flexweg Search
1066
+
1067
+ A theme-agnostic search plugin for Flexweg CMS. Builds a static
1068
+ \`/search-index.json\` over your published content and ships a tiny
1069
+ runtime (\`/search.js\`) that opens a search modal anywhere a
1070
+ \`[data-cms-search]\` trigger is present in your theme.
1071
+
1072
+ No backend, no dependencies, no third-party search service — pure
1073
+ client-side substring matching against a static JSON.
1074
+
1075
+ ## How it integrates with a theme
1076
+
1077
+ Themes expose a search trigger as plain HTML:
1078
+
1079
+ \`\`\`html
1080
+ <button type="button" data-cms-search aria-label="Search">…</button>
1081
+ \`\`\`
1082
+
1083
+ The plugin's runtime attaches click handlers to every such element on
1084
+ \`DOMContentLoaded\` and opens its modal when the user clicks. Themes
1085
+ don't need to know anything about the plugin's API.
1086
+
1087
+ If the plugin is disabled, the trigger stays inert (the runtime never
1088
+ loads). The theme keeps a working button shape — just no behavior.
1089
+
1090
+ ## What gets indexed
1091
+
1092
+ - **Title** — always.
1093
+ - **Excerpt** — opt-in. When the post has no explicit excerpt, falls
1094
+ back to a 200-character plain-text rendering of the body.
1095
+ - **Category name** — opt-in.
1096
+ - **Tag names** — opt-in.
1097
+
1098
+ The body is **never** indexed in full — too heavy for client-side
1099
+ fetch, and the title + excerpt path covers most search-by-title use
1100
+ cases. If you need true full-text search, plug in an external service.
1101
+
1102
+ ## Performance
1103
+
1104
+ - Index file is a single JSON, fetched **once** on the first modal
1105
+ open per page load and cached in memory.
1106
+ - Token search runs in linear time (substring \`indexOf\`); fine up to
1107
+ a few thousand items.
1108
+ - Title hits are weighted higher than excerpt / category / tag hits, so
1109
+ matching titles rise to the top.
1110
+
1111
+ ## Lifecycle
1112
+
1113
+ The index is regenerated on every publish action that mutates the
1114
+ corpus:
1115
+
1116
+ - \`publish.complete\` — a post just went online (or was edited).
1117
+ - \`post.unpublished\` — a post moved back to draft.
1118
+ - \`post.deleted\` — a post was removed entirely.
1119
+
1120
+ \`/search.js\` itself is uploaded once on first run, then again only
1121
+ when its bundle hash changes (i.e. the admin was redeployed with a new
1122
+ runtime). Subsequent publishes only rewrite the JSON.
1123
+
1124
+ A **Force regenerate** button in the settings page rebuilds and
1125
+ re-uploads both files unconditionally — useful after switching
1126
+ \`settings.language\` (the runtime UI strings come from there) or when
1127
+ you suspect the public site is out of sync.
1128
+
1129
+ ## Settings
1130
+
1131
+ - **Index** — toggles for excerpt / category / tags / static-pages
1132
+ inclusion + a checklist of categories to exclude.
1133
+ - **Behavior** — minimum query length (default 2) and maximum results
1134
+ (default 20). These are baked into the index file's \`meta\` block, so
1135
+ a change requires a regeneration to take effect.
1136
+
1137
+ ## Customizing the modal styles
1138
+
1139
+ The runtime injects its own minimal \`<style>\` block on first open
1140
+ (\`.cms-search-modal\`, \`.cms-search-panel\`, \`.cms-search-result\`, …).
1141
+ Themes override anything by writing rules with higher specificity, or
1142
+ by wrapping their own rules in \`:where()\`.
1143
+
1144
+ ## Files written to your site
1145
+
1146
+ - \`/search-index.json\` — the index. Re-uploaded on every corpus change.
1147
+ - \`/search.js\` — the runtime. Re-uploaded only when its hash changes.
1148
+
1149
+ Both live at the site root. The plugin auto-injects
1150
+ \`<script src="/search.js" defer><\/script>\` on every page through the
1151
+ \`page.body.end\` hook, so themes never have to add the script tag
1152
+ themselves.
1153
+
1154
+ ## Limitations / known trade-offs
1155
+
1156
+ - **First-publish race window** — the page being published is uploaded
1157
+ before \`/search.js\`. A visitor opening the page during that brief
1158
+ window (typically <1s) gets a 404 on the script. Subsequent loads
1159
+ work fine.
1160
+ - **No fuzzy match** — token-by-token substring only. Typos = no
1161
+ results. Acceptable for a CMS search; consider a hosted service if
1162
+ you need typo tolerance.
1163
+ - **Single index per site** — no per-language splits. The runtime UI
1164
+ strings respect \`settings.language\`, but the index itself is one
1165
+ flat list; multilingual content sites should use one language per
1166
+ site for now.
1167
+ `;
1168
+ function J(t) {
1169
+ const n = (t.pluginConfigs ?? {})[R];
1170
+ return { ...S, ...n ?? {} };
1171
+ }
1172
+ async function j(t, n) {
1173
+ if (!n.settings.baseUrl) {
1174
+ console.warn("[flexweg-search] skipping regeneration — baseUrl not configured");
1175
+ return;
1176
+ }
1177
+ let e = J(n.settings);
1178
+ try {
1179
+ const r = await ce(e);
1180
+ e = r.nextConfig;
1181
+ const m = await le({
1182
+ posts: n.posts,
1183
+ pages: n.pages,
1184
+ terms: n.terms,
1185
+ settings: n.settings,
1186
+ config: e
1187
+ });
1188
+ e = m.nextConfig, (r.uploaded || m.uploaded) && await B(R, e);
1189
+ } catch (r) {
1190
+ console.error("[flexweg-search] regeneration failed:", r);
1191
+ }
1192
+ }
1193
+ const we = {
1194
+ id: R,
1195
+ name: "Flexweg Search",
1196
+ version: "1.0.0",
1197
+ author: "Flexweg",
1198
+ description: "Generates /search-index.json + a search runtime that opens a modal anywhere a [data-cms-search] trigger is present in your theme.",
1199
+ readme: xe,
1200
+ i18n: { en: Q, fr: ne, de: te, es: ae, nl: se, pt: re, ko: ie },
1201
+ settings: {
1202
+ navLabelKey: "title",
1203
+ defaultConfig: S,
1204
+ component: pe
1205
+ },
1206
+ register(t) {
1207
+ t.addAction("publish.complete", async (n, e) => {
1208
+ await j(n, e);
1209
+ }), t.addAction("post.unpublished", async (n, e) => {
1210
+ await j(n, e);
1211
+ }), t.addAction("post.deleted", async (n, e) => {
1212
+ await j(n, e);
1213
+ }), t.addFilter("page.body.end", (n, ...e) => {
1214
+ e[0];
1215
+ const r = `<script src="/${q}" defer><\/script>`;
1216
+ return [n, r].filter(Boolean).join("");
1217
+ }), t.registerRegenerationTarget({
1218
+ id: R,
1219
+ labelKey: "regenerationTarget.label",
1220
+ descriptionKey: "regenerationTarget.description",
1221
+ priority: 220,
1222
+ run: async (n, e) => {
1223
+ if (!n.settings.baseUrl) {
1224
+ e({ level: "warn", message: "[flexweg-search] skipped — site URL not set." });
1225
+ return;
1226
+ }
1227
+ const r = J(n.settings);
1228
+ e({ level: "info", message: "Regenerating search runtime + index…" });
1229
+ const m = await _({
1230
+ posts: n.posts,
1231
+ pages: n.pages,
1232
+ terms: n.terms,
1233
+ settings: n.settings,
1234
+ config: r
1235
+ });
1236
+ JSON.stringify(m.nextConfig) !== JSON.stringify(r) && await B(R, m.nextConfig), e({ level: "success", message: "Search: runtime + index uploaded." });
1237
+ }
1238
+ });
1239
+ }
1240
+ };
1241
+ export {
1242
+ we as manifest
1243
+ };