accessify-widget 0.2.20 → 0.2.21

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.
@@ -4760,8 +4760,8 @@ const deJson = {
4760
4760
  "feature.tts.desc": "Text per Klick vorlesen lassen",
4761
4761
  "feature.textSimplify": "Text vereinfachen",
4762
4762
  "feature.textSimplify.desc": "Text in Einfache Sprache umwandeln",
4763
- "feature.altText": "Alt-Text erzeugen",
4764
- "feature.altText.desc": "Bildbeschreibungen automatisch erstellen",
4763
+ "feature.altText": "Bildbeschreibung",
4764
+ "feature.altText.desc": "Bilder per Klick beschreiben lassen",
4765
4765
  "feature.autoScan": "WCAG-Prüfung",
4766
4766
  "feature.autoScan.desc": "Seite auf Barrierefreiheit prüfen",
4767
4767
  "feature.saturation": "Sättigung",
@@ -4849,8 +4849,8 @@ const enJson = {
4849
4849
  "feature.tts.desc": "Click any text to hear it spoken",
4850
4850
  "feature.textSimplify": "Simplify Text",
4851
4851
  "feature.textSimplify.desc": "Simplify text for easier understanding",
4852
- "feature.altText": "Alt-Text Generator",
4853
- "feature.altText.desc": "Auto-generate image descriptions",
4852
+ "feature.altText": "Image Description",
4853
+ "feature.altText.desc": "Click images to get AI descriptions",
4854
4854
  "feature.autoScan": "WCAG Scan",
4855
4855
  "feature.autoScan.desc": "Scan page for accessibility issues",
4856
4856
  "feature.saturation": "Saturation",
@@ -4997,7 +4997,7 @@ const locales = {
4997
4997
  "feature.pageStructure": "页面结构",
4998
4998
  "feature.tts": "朗读",
4999
4999
  "feature.textSimplify": "简化文本",
5000
- "feature.altText": "生成替代文本",
5000
+ "feature.altText": "图片描述",
5001
5001
  "feature.autoScan": "WCAG 扫描"
5002
5002
  }),
5003
5003
  hi: withEnglish({
@@ -5040,7 +5040,7 @@ const locales = {
5040
5040
  "feature.pageStructure": "पेज संरचना",
5041
5041
  "feature.tts": "जोर से पढ़ें",
5042
5042
  "feature.textSimplify": "टेक्स्ट सरल करें",
5043
- "feature.altText": "ऑल्ट-टेक्स्ट बनाएँ",
5043
+ "feature.altText": "चित्र विवरण",
5044
5044
  "feature.autoScan": "WCAG स्कैन"
5045
5045
  }),
5046
5046
  es: withEnglish({
@@ -5083,7 +5083,7 @@ const locales = {
5083
5083
  "feature.pageStructure": "Estructura de pagina",
5084
5084
  "feature.tts": "Lectura en voz alta",
5085
5085
  "feature.textSimplify": "Simplificar texto",
5086
- "feature.altText": "Generar texto alternativo",
5086
+ "feature.altText": "Descripcion de imagen",
5087
5087
  "feature.autoScan": "Escaneo WCAG"
5088
5088
  }),
5089
5089
  fr: withEnglish({
@@ -5126,7 +5126,7 @@ const locales = {
5126
5126
  "feature.pageStructure": "Structure de page",
5127
5127
  "feature.tts": "Lecture audio",
5128
5128
  "feature.textSimplify": "Simplifier le texte",
5129
- "feature.altText": "Generer un texte alternatif",
5129
+ "feature.altText": "Description d'image",
5130
5130
  "feature.autoScan": "Analyse WCAG"
5131
5131
  }),
5132
5132
  ar: withEnglish({
@@ -5169,7 +5169,7 @@ const locales = {
5169
5169
  "feature.pageStructure": "بنية الصفحة",
5170
5170
  "feature.tts": "القراءة بصوت عالٍ",
5171
5171
  "feature.textSimplify": "تبسيط النص",
5172
- "feature.altText": "إنشاء نص بديل",
5172
+ "feature.altText": "وصف الصورة",
5173
5173
  "feature.autoScan": "فحص WCAG"
5174
5174
  }),
5175
5175
  bn: withEnglish({
@@ -5211,7 +5211,7 @@ const locales = {
5211
5211
  "feature.pageStructure": "পেজ স্ট্রাকচার",
5212
5212
  "feature.tts": "জোরে পড়া",
5213
5213
  "feature.textSimplify": "টেক্সট সহজ করুন",
5214
- "feature.altText": "অল্ট-টেক্সট তৈরি",
5214
+ "feature.altText": "ছবির বিবরণ",
5215
5215
  "feature.autoScan": "WCAG স্ক্যান"
5216
5216
  }),
5217
5217
  pt: withEnglish({
@@ -5254,7 +5254,7 @@ const locales = {
5254
5254
  "feature.pageStructure": "Estrutura da pagina",
5255
5255
  "feature.tts": "Leitura em voz alta",
5256
5256
  "feature.textSimplify": "Simplificar texto",
5257
- "feature.altText": "Gerar texto alternativo",
5257
+ "feature.altText": "Descricao da imagem",
5258
5258
  "feature.autoScan": "Varredura WCAG"
5259
5259
  }),
5260
5260
  ru: withEnglish({
@@ -5297,7 +5297,7 @@ const locales = {
5297
5297
  "feature.pageStructure": "Структура страницы",
5298
5298
  "feature.tts": "Озвучивание",
5299
5299
  "feature.textSimplify": "Упростить текст",
5300
- "feature.altText": "Создать alt-текст",
5300
+ "feature.altText": "Описание изображения",
5301
5301
  "feature.autoScan": "Проверка WCAG"
5302
5302
  }),
5303
5303
  ur: withEnglish({
@@ -5339,7 +5339,7 @@ const locales = {
5339
5339
  "feature.pageStructure": "صفحہ ساخت",
5340
5340
  "feature.tts": "بلند آواز میں پڑھیں",
5341
5341
  "feature.textSimplify": "متن آسان کریں",
5342
- "feature.altText": "آلٹ ٹیکسٹ بنائیں",
5342
+ "feature.altText": "تصویر کی تفصیل",
5343
5343
  "feature.autoScan": "WCAG اسکین"
5344
5344
  }),
5345
5345
  id: withEnglish({
@@ -5381,7 +5381,7 @@ const locales = {
5381
5381
  "feature.pageStructure": "Struktur halaman",
5382
5382
  "feature.tts": "Bacakan",
5383
5383
  "feature.textSimplify": "Sederhanakan teks",
5384
- "feature.altText": "Buat alt-text",
5384
+ "feature.altText": "Deskripsi gambar",
5385
5385
  "feature.autoScan": "Pindai WCAG"
5386
5386
  }),
5387
5387
  ja: withEnglish({
@@ -5424,7 +5424,7 @@ const locales = {
5424
5424
  "feature.pageStructure": "ページ構造",
5425
5425
  "feature.tts": "読み上げ",
5426
5426
  "feature.textSimplify": "文章を簡単にする",
5427
- "feature.altText": "代替テキスト生成",
5427
+ "feature.altText": "画像の説明",
5428
5428
  "feature.autoScan": "WCAG スキャン"
5429
5429
  }),
5430
5430
  pcm: withEnglish({
@@ -5466,7 +5466,7 @@ const locales = {
5466
5466
  "feature.pageStructure": "Page structure",
5467
5467
  "feature.tts": "Read aloud",
5468
5468
  "feature.textSimplify": "Simplify text",
5469
- "feature.altText": "Generate alt-text",
5469
+ "feature.altText": "Image Description",
5470
5470
  "feature.autoScan": "WCAG scan"
5471
5471
  }),
5472
5472
  mr: withEnglish({
@@ -5508,7 +5508,7 @@ const locales = {
5508
5508
  "feature.pageStructure": "पृष्ठरचना",
5509
5509
  "feature.tts": "मोठ्याने वाचा",
5510
5510
  "feature.textSimplify": "मजकूर सोपा करा",
5511
- "feature.altText": "alt-text तयार करा",
5511
+ "feature.altText": "चित्र वर्णन",
5512
5512
  "feature.autoScan": "WCAG स्कॅन"
5513
5513
  }),
5514
5514
  te: withEnglish({
@@ -5550,7 +5550,7 @@ const locales = {
5550
5550
  "feature.pageStructure": "పేజీ నిర్మాణం",
5551
5551
  "feature.tts": "గట్టిగా చదవండి",
5552
5552
  "feature.textSimplify": "టెక్స్ట్ సులభతరం చేయి",
5553
- "feature.altText": "alt-text రూపొందించు",
5553
+ "feature.altText": "చిత్ర వివరణ",
5554
5554
  "feature.autoScan": "WCAG స్కాన్"
5555
5555
  }),
5556
5556
  tr: withEnglish({
@@ -5593,7 +5593,7 @@ const locales = {
5593
5593
  "feature.pageStructure": "Sayfa yapisi",
5594
5594
  "feature.tts": "Sesli oku",
5595
5595
  "feature.textSimplify": "Metni sadeletir",
5596
- "feature.altText": "Alt metin olustur",
5596
+ "feature.altText": "Resim aciklamasi",
5597
5597
  "feature.autoScan": "WCAG taramasi"
5598
5598
  }),
5599
5599
  ta: withEnglish({
@@ -5635,7 +5635,7 @@ const locales = {
5635
5635
  "feature.pageStructure": "பக்க அமைப்பு",
5636
5636
  "feature.tts": "சத்தமாக வாசி",
5637
5637
  "feature.textSimplify": "உரையை எளிமைப்படுத்து",
5638
- "feature.altText": "alt-text உருவாக்கு",
5638
+ "feature.altText": "பட விளக்கம்",
5639
5639
  "feature.autoScan": "WCAG ஸ்கேன்"
5640
5640
  }),
5641
5641
  vi: withEnglish({
@@ -5677,7 +5677,7 @@ const locales = {
5677
5677
  "feature.pageStructure": "Cau truc trang",
5678
5678
  "feature.tts": "Doc thanh tieng",
5679
5679
  "feature.textSimplify": "Don gian hoa van ban",
5680
- "feature.altText": "Tao alt-text",
5680
+ "feature.altText": "Mo ta hinh anh",
5681
5681
  "feature.autoScan": "Quet WCAG"
5682
5682
  }),
5683
5683
  ko: withEnglish({
@@ -5720,7 +5720,7 @@ const locales = {
5720
5720
  "feature.pageStructure": "페이지 구조",
5721
5721
  "feature.tts": "소리 내어 읽기",
5722
5722
  "feature.textSimplify": "텍스트 단순화",
5723
- "feature.altText": "대체 텍스트 생성",
5723
+ "feature.altText": "이미지 설명",
5724
5724
  "feature.autoScan": "WCAG 검사"
5725
5725
  })
5726
5726
  };
@@ -6078,6 +6078,20 @@ function createProxyService(siteKey, proxyUrl) {
6078
6078
  }
6079
6079
  const data = await res.json();
6080
6080
  return data.result || "";
6081
+ },
6082
+ async describeImage(imageUrl, lang) {
6083
+ const res = await fetch(`${base}/v1/ai/describe-image`, {
6084
+ method: "POST",
6085
+ headers,
6086
+ body: JSON.stringify({ imageUrl, lang })
6087
+ });
6088
+ if (!res.ok) {
6089
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
6090
+ if (res.status === 429) throw new RateLimitError(err.error || "Rate limit reached");
6091
+ throw new Error(err.error || `AI proxy error: ${res.status}`);
6092
+ }
6093
+ const data = await res.json();
6094
+ return data.result || "";
6081
6095
  }
6082
6096
  };
6083
6097
  }
@@ -6156,6 +6170,33 @@ function createDirectService(config2) {
6156
6170
  }
6157
6171
  const data = await response.json();
6158
6172
  return extractContent(data);
6173
+ },
6174
+ async describeImage(imageUrl, lang) {
6175
+ const langInstruction = lang && lang.startsWith("de") ? " Beschreibe auf Deutsch." : "";
6176
+ const response = await fetch(`${OPENROUTER_BASE}/chat/completions`, {
6177
+ method: "POST",
6178
+ headers,
6179
+ body: JSON.stringify({
6180
+ model: config2.visionModel || DEFAULTS.visionModel,
6181
+ messages: [{
6182
+ role: "user",
6183
+ content: [
6184
+ {
6185
+ type: "text",
6186
+ text: `Describe what is shown in this image. 2-3 sentences, easy to understand.${langInstruction} Return ONLY the description.`
6187
+ },
6188
+ { type: "image_url", image_url: { url: imageUrl } }
6189
+ ]
6190
+ }],
6191
+ max_tokens: 500
6192
+ })
6193
+ });
6194
+ if (!response.ok) {
6195
+ if (response.status === 429) throw new RateLimitError("Rate limit reached. Please try again later.");
6196
+ throw new Error(`AI vision error: ${response.status}`);
6197
+ }
6198
+ const data = await response.json();
6199
+ return extractContent(data);
6159
6200
  }
6160
6201
  };
6161
6202
  }
@@ -6310,14 +6351,14 @@ function FeatureGrid($$anchor, $$props) {
6310
6351
  const FEATURE_LOADERS = {
6311
6352
  contrast: () => import("./contrast-CqsICAkU.js"),
6312
6353
  "text-size": () => import("./text-size-C6OFhCGi.js"),
6313
- "keyboard-nav": () => import("./keyboard-nav-BtZsDcZQ.js"),
6354
+ "keyboard-nav": () => import("./keyboard-nav-CZma_qRC.js"),
6314
6355
  "link-highlight": () => import("./link-highlight-DBGm067Y.js"),
6315
6356
  "reading-guide": () => import("./reading-guide-VT8NciIL.js"),
6316
6357
  "reading-mask": () => import("./reading-mask-BABChuCz.js"),
6317
6358
  "animation-stop": () => import("./animation-stop-C0MwseK0.js"),
6318
6359
  "hide-images": () => import("./hide-images-B_LeCBcd.js"),
6319
6360
  "big-cursor": () => import("./big-cursor-B2UKu9dQ.js"),
6320
- "page-structure": () => import("./page-structure-DTKpsB5y.js"),
6361
+ "page-structure": () => import("./page-structure-DTIVV243.js"),
6321
6362
  tts: () => import("./tts-CjszLRnb.js"),
6322
6363
  "text-simplify": () => import("./text-simplify-Cvhpio7g.js"),
6323
6364
  "alt-text": () => Promise.resolve().then(() => altText)
@@ -7587,7 +7628,6 @@ function createWidgetStyles(config2) {
7587
7628
  }
7588
7629
  const IDB_NAME = "accessify-alt-text-cache";
7589
7630
  const IDB_STORE = "alt-texts";
7590
- const IDB_REPORT_STORE = "alt-text-reports";
7591
7631
  const IDB_VERSION = 2;
7592
7632
  function hashSrc(src) {
7593
7633
  let hash = 5381;
@@ -7598,6 +7638,20 @@ function hashSrc(src) {
7598
7638
  }
7599
7639
  return Math.abs(hash).toString(36);
7600
7640
  }
7641
+ function normalizeImageUrl(url) {
7642
+ try {
7643
+ const u = new URL(url);
7644
+ u.searchParams.delete("width");
7645
+ u.searchParams.delete("height");
7646
+ u.searchParams.delete("scale-down-to");
7647
+ return u.href;
7648
+ } catch {
7649
+ return url;
7650
+ }
7651
+ }
7652
+ function getImageSrc(img) {
7653
+ return img.currentSrc || img.src;
7654
+ }
7601
7655
  function openCache() {
7602
7656
  return new Promise((resolve) => {
7603
7657
  try {
@@ -7605,7 +7659,7 @@ function openCache() {
7605
7659
  req.onupgradeneeded = () => {
7606
7660
  const db = req.result;
7607
7661
  if (!db.objectStoreNames.contains(IDB_STORE)) db.createObjectStore(IDB_STORE, { keyPath: "key" });
7608
- if (!db.objectStoreNames.contains(IDB_REPORT_STORE)) db.createObjectStore(IDB_REPORT_STORE, { keyPath: "key" });
7662
+ if (!db.objectStoreNames.contains("alt-text-reports")) db.createObjectStore("alt-text-reports", { keyPath: "key" });
7609
7663
  };
7610
7664
  req.onsuccess = () => resolve(req.result);
7611
7665
  req.onerror = () => resolve(null);
@@ -7623,8 +7677,7 @@ async function getAllCachedAltTexts() {
7623
7677
  const tx = db.transaction(IDB_STORE, "readonly");
7624
7678
  const req = tx.objectStore(IDB_STORE).getAll();
7625
7679
  req.onsuccess = () => {
7626
- const results = req.result;
7627
- for (const r of results) {
7680
+ for (const r of req.result) {
7628
7681
  map.set(r.src, r.altText);
7629
7682
  }
7630
7683
  resolve(map);
@@ -7641,9 +7694,23 @@ async function getCachedAltText(src) {
7641
7694
  return new Promise((resolve) => {
7642
7695
  try {
7643
7696
  const tx = db.transaction(IDB_STORE, "readonly");
7644
- const req = tx.objectStore(IDB_STORE).get(hashSrc(src));
7645
- req.onsuccess = () => resolve(req.result?.altText || null);
7646
- req.onerror = () => resolve(null);
7697
+ const store = tx.objectStore(IDB_STORE);
7698
+ const req1 = store.get(hashSrc(src));
7699
+ req1.onsuccess = () => {
7700
+ if (req1.result?.altText) {
7701
+ resolve(req1.result.altText);
7702
+ return;
7703
+ }
7704
+ const norm = normalizeImageUrl(src);
7705
+ if (norm !== src) {
7706
+ const req2 = store.get(hashSrc(norm));
7707
+ req2.onsuccess = () => resolve(req2.result?.altText || null);
7708
+ req2.onerror = () => resolve(null);
7709
+ } else {
7710
+ resolve(null);
7711
+ }
7712
+ };
7713
+ req1.onerror = () => resolve(null);
7647
7714
  } catch {
7648
7715
  resolve(null);
7649
7716
  }
@@ -7663,41 +7730,6 @@ async function setCachedAltText(src, altText2, langCode) {
7663
7730
  }
7664
7731
  });
7665
7732
  }
7666
- async function saveReport(src, altText2, langCode) {
7667
- const db = await openCache();
7668
- if (!db) return;
7669
- return new Promise((resolve) => {
7670
- try {
7671
- const tx = db.transaction(IDB_REPORT_STORE, "readwrite");
7672
- tx.objectStore(IDB_REPORT_STORE).put({
7673
- key: hashSrc(src) + "_" + Date.now(),
7674
- src,
7675
- pageUrl: window.location.href,
7676
- altText: altText2,
7677
- lang: langCode,
7678
- reportedAt: Date.now()
7679
- });
7680
- tx.oncomplete = () => resolve();
7681
- tx.onerror = () => resolve();
7682
- } catch {
7683
- resolve();
7684
- }
7685
- });
7686
- }
7687
- async function removeCachedAltText(src) {
7688
- const db = await openCache();
7689
- if (!db) return;
7690
- return new Promise((resolve) => {
7691
- try {
7692
- const tx = db.transaction(IDB_STORE, "readwrite");
7693
- tx.objectStore(IDB_STORE).delete(hashSrc(src));
7694
- tx.oncomplete = () => resolve();
7695
- tx.onerror = () => resolve();
7696
- } catch {
7697
- resolve();
7698
- }
7699
- });
7700
- }
7701
7733
  const DEFAULT_API_BASE = "https://accessify-api.accessify.workers.dev";
7702
7734
  async function fetchServerAltTexts(siteKey, proxyUrl, lang) {
7703
7735
  const map = /* @__PURE__ */ new Map();
@@ -7712,9 +7744,7 @@ async function fetchServerAltTexts(siteKey, proxyUrl, lang) {
7712
7744
  const data = await res.json();
7713
7745
  if (data.blocks) {
7714
7746
  for (const block2 of data.blocks) {
7715
- if (block2.selector && block2.result) {
7716
- map.set(block2.selector, block2.result);
7717
- }
7747
+ if (block2.selector && block2.result) map.set(block2.selector, block2.result);
7718
7748
  }
7719
7749
  }
7720
7750
  } catch {
@@ -7751,62 +7781,33 @@ async function autoApplyCachedAltTexts(widgetConfig) {
7751
7781
  }
7752
7782
  }
7753
7783
  const localCache = await getAllCachedAltTexts();
7754
- const localOnly = [];
7755
7784
  for (const [url, alt] of localCache) {
7756
- if (!cache.has(url)) {
7757
- cache.set(url, alt);
7758
- localOnly.push({ imageUrl: url, altText: alt });
7759
- }
7760
- }
7761
- if (siteKey && localOnly.length > 0) {
7762
- try {
7763
- const base = proxyUrl || DEFAULT_API_BASE;
7764
- const pageUrl = window.location.origin + window.location.pathname;
7765
- fetch(`${base}/v1/cache/batch-persist-alt-text`, {
7766
- method: "POST",
7767
- headers: { "Content-Type": "application/json" },
7768
- body: JSON.stringify({
7769
- siteKey,
7770
- pageUrl,
7771
- entries: localOnly.map((e) => ({ ...e, lang: pageLang }))
7772
- })
7773
- }).catch(() => {
7774
- });
7775
- } catch {
7776
- }
7785
+ if (!cache.has(url)) cache.set(url, alt);
7777
7786
  }
7778
7787
  if (cache.size === 0) return 0;
7779
- let applied = 0;
7780
- const images = document.querySelectorAll("img");
7781
- images.forEach((img) => {
7782
- if (img.closest("#accessify-root") || img.closest("accessify-widget")) return;
7788
+ function applyToImg(img) {
7789
+ if (img.closest("#accessify-root") || img.closest("accessify-widget")) return false;
7783
7790
  const alt = img.getAttribute("alt");
7784
- if (alt !== null && alt.trim() !== "") return;
7785
- const src = img.currentSrc || img.src;
7786
- const cached = cache.get(src) || cache.get(img.src);
7791
+ if (alt !== null && alt.trim() !== "") return false;
7792
+ const src = getImageSrc(img);
7793
+ const cached = cache.get(src) || cache.get(normalizeImageUrl(src)) || cache.get(img.src);
7787
7794
  if (cached) {
7788
7795
  img.setAttribute("alt", cached);
7789
7796
  img.setAttribute("title", cached);
7790
7797
  img.setAttribute("data-accessify-alt", "auto");
7791
- applied++;
7798
+ return true;
7792
7799
  }
7800
+ return false;
7801
+ }
7802
+ let applied = 0;
7803
+ document.querySelectorAll("img").forEach((img) => {
7804
+ if (applyToImg(img)) applied++;
7793
7805
  });
7794
7806
  const observer2 = new MutationObserver((mutations) => {
7795
7807
  for (const m of mutations) {
7796
7808
  for (const node of m.addedNodes) {
7797
7809
  const imgs = node instanceof HTMLImageElement ? [node] : node instanceof HTMLElement ? Array.from(node.querySelectorAll("img")) : [];
7798
- for (const img of imgs) {
7799
- if (img.closest("#accessify-root") || img.closest("accessify-widget")) continue;
7800
- const a = img.getAttribute("alt");
7801
- if (a !== null && a.trim() !== "") continue;
7802
- const src = img.currentSrc || img.src;
7803
- const c = cache.get(src) || cache.get(img.src);
7804
- if (c) {
7805
- img.setAttribute("alt", c);
7806
- img.setAttribute("title", c);
7807
- img.setAttribute("data-accessify-alt", "auto");
7808
- }
7809
- }
7810
+ for (const img of imgs) applyToImg(img);
7810
7811
  }
7811
7812
  }
7812
7813
  });
@@ -7820,13 +7821,13 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
7820
7821
  let enabled = false;
7821
7822
  let styleEl = null;
7822
7823
  let badgeEl = null;
7824
+ let tooltipEl = null;
7825
+ let domObserver = null;
7826
+ let autoGenerating = false;
7823
7827
  let missingAltImages = [];
7824
- let existingAltImages = [];
7825
7828
  const processedImages = /* @__PURE__ */ new Map();
7826
- let autoGenerating = false;
7827
- let tooltipEl = null;
7828
- const infoButtons = /* @__PURE__ */ new Map();
7829
- let reportPanelEl = null;
7829
+ let describeClickHandler = null;
7830
+ let describeEscHandler = null;
7830
7831
  function lang() {
7831
7832
  return getCurrentWidgetLang() || initialLang;
7832
7833
  }
@@ -7835,15 +7836,12 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
7835
7836
  }
7836
7837
  const STYLE_ID = "accessify-alt-text-styles";
7837
7838
  const HIGHLIGHT_CLASS = "accessify-alt-missing";
7838
- const HAS_ALT_CLASS = "accessify-alt-existing";
7839
- const BADGE_CLASS = "accessify-alt-badge";
7840
7839
  function getStyles() {
7841
7840
  return `
7842
7841
  .${HIGHLIGHT_CLASS} {
7843
7842
  outline: 3px dashed #e63946 !important;
7844
7843
  outline-offset: 3px !important;
7845
7844
  position: relative !important;
7846
- transition: outline-color 0.2s ease !important;
7847
7845
  }
7848
7846
  .${HIGHLIGHT_CLASS}.accessify-alt-generating::after {
7849
7847
  content: '\\2026';
@@ -7869,99 +7867,65 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
7869
7867
  content: none;
7870
7868
  }
7871
7869
 
7872
- .${HAS_ALT_CLASS} {
7873
- outline: 3px solid #2a9d8f !important;
7874
- outline-offset: 3px !important;
7875
- position: relative !important;
7870
+ img[data-a11y-describable="true"] {
7871
+ cursor: help !important;
7876
7872
  }
7877
7873
 
7878
- .accessify-alt-tooltip {
7874
+ .accessify-describe-tooltip {
7879
7875
  position: fixed;
7880
7876
  z-index: 2147483647;
7881
- max-width: 320px;
7882
- padding: 8px 12px;
7877
+ max-width: 340px;
7878
+ padding: 14px 18px;
7883
7879
  background: #1a1a2e;
7884
7880
  color: #e0e0e0;
7885
- border-radius: 8px;
7886
- box-shadow: 0 4px 20px rgba(0,0,0,0.4);
7881
+ border-radius: 10px;
7882
+ box-shadow: 0 6px 24px rgba(0,0,0,0.45);
7887
7883
  font-family: system-ui, -apple-system, sans-serif;
7888
- font-size: 13px;
7889
- line-height: 1.5;
7890
- pointer-events: none;
7884
+ font-size: 14px;
7885
+ line-height: 1.6;
7891
7886
  word-break: break-word;
7892
- opacity: 1;
7887
+ animation: accessify-fade-in 0.15s ease;
7893
7888
  }
7894
-
7895
- .accessify-alt-info-btn {
7889
+ .accessify-describe-tooltip .accessify-describe-close {
7896
7890
  position: absolute;
7897
7891
  top: 6px;
7898
- right: 6px;
7899
- z-index: 10;
7900
- width: 22px;
7901
- height: 22px;
7902
- padding: 0;
7892
+ right: 8px;
7893
+ background: none;
7903
7894
  border: none;
7904
- border-radius: 50%;
7905
- background: rgba(0,0,0,0.6);
7906
- color: #ccc;
7907
- font-family: system-ui, -apple-system, sans-serif;
7908
- font-size: 13px;
7909
- font-weight: 600;
7910
- line-height: 22px;
7911
- text-align: center;
7895
+ color: #888;
7896
+ font-size: 16px;
7912
7897
  cursor: pointer;
7913
- opacity: 0;
7914
- transition: opacity 0.15s ease;
7915
- backdrop-filter: blur(4px);
7916
- }
7917
- .${HIGHLIGHT_CLASS}.accessify-alt-done:hover + .accessify-alt-info-btn,
7918
- .accessify-alt-info-btn:hover {
7919
- opacity: 1;
7898
+ padding: 2px 6px;
7899
+ line-height: 1;
7920
7900
  }
7921
- .accessify-alt-info-btn:hover {
7922
- background: rgba(0,0,0,0.8);
7901
+ .accessify-describe-tooltip .accessify-describe-close:hover {
7923
7902
  color: #fff;
7924
7903
  }
7925
-
7926
- .accessify-alt-report-panel {
7927
- position: fixed;
7928
- z-index: 2147483646;
7929
- width: 240px;
7930
- padding: 12px;
7931
- background: #1a1a2e;
7932
- color: #f0f0f0;
7933
- border-radius: 10px;
7934
- box-shadow: 0 8px 32px rgba(0,0,0,0.4);
7935
- font-family: system-ui, -apple-system, sans-serif;
7936
- font-size: 12px;
7937
- line-height: 1.5;
7938
- }
7939
- .accessify-alt-report-panel p {
7940
- margin: 0 0 8px 0;
7904
+ .accessify-describe-loading {
7905
+ display: flex;
7906
+ align-items: center;
7907
+ gap: 8px;
7941
7908
  color: #aaa;
7909
+ font-style: italic;
7910
+ }
7911
+ .accessify-describe-loading::before {
7912
+ content: '';
7913
+ width: 14px;
7914
+ height: 14px;
7915
+ border: 2px solid #555;
7916
+ border-top-color: #aaa;
7917
+ border-radius: 50%;
7918
+ animation: accessify-spin 0.8s linear infinite;
7942
7919
  }
7943
- .accessify-alt-report-btn {
7944
- display: inline-flex;
7945
- align-items: center;
7946
- gap: 4px;
7947
- padding: 5px 10px;
7948
- border: 1px solid rgba(220,53,69,0.3);
7949
- border-radius: 6px;
7950
- background: rgba(220,53,69,0.08);
7951
- color: #f0a0a8;
7952
- font-size: 11px;
7953
- cursor: pointer;
7954
- transition: background 0.15s ease;
7920
+ @keyframes accessify-spin {
7921
+ to { transform: rotate(360deg); }
7955
7922
  }
7956
- .accessify-alt-report-btn:hover { background: rgba(220,53,69,0.2); }
7957
- .accessify-alt-report-btn.reported {
7958
- border-color: rgba(42,157,143,0.3);
7959
- background: rgba(42,157,143,0.1);
7960
- color: #2a9d8f;
7961
- cursor: default;
7923
+ @keyframes accessify-fade-in {
7924
+ from { opacity: 0; transform: translateY(4px); }
7925
+ to { opacity: 1; transform: translateY(0); }
7962
7926
  }
7963
7927
 
7964
- .${BADGE_CLASS} {
7928
+ .accessify-alt-badge {
7965
7929
  position: fixed;
7966
7930
  top: 12px;
7967
7931
  right: 12px;
@@ -7978,7 +7942,7 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
7978
7942
  font-size: 13px;
7979
7943
  user-select: none;
7980
7944
  }
7981
- .${BADGE_CLASS}-count {
7945
+ .accessify-alt-badge-count {
7982
7946
  display: inline-flex;
7983
7947
  align-items: center;
7984
7948
  justify-content: center;
@@ -8005,100 +7969,128 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8005
7969
  styleEl = null;
8006
7970
  document.getElementById(STYLE_ID)?.remove();
8007
7971
  }
8008
- function showTooltip(img) {
8009
- const text = img.getAttribute("alt") || processedImages.get(img)?.generatedAlt;
8010
- if (!text) return;
8011
- hideTooltip();
7972
+ function showDescribeTooltip(text, rect, loading = false) {
7973
+ hideDescribeTooltip();
8012
7974
  const tip = document.createElement("div");
8013
- tip.className = "accessify-alt-tooltip";
8014
- tip.textContent = text;
7975
+ tip.className = "accessify-describe-tooltip";
7976
+ const closeBtn = document.createElement("button");
7977
+ closeBtn.className = "accessify-describe-close";
7978
+ closeBtn.textContent = "×";
7979
+ closeBtn.setAttribute("aria-label", "Close");
7980
+ closeBtn.addEventListener("click", (e) => {
7981
+ e.stopPropagation();
7982
+ hideDescribeTooltip();
7983
+ });
7984
+ tip.appendChild(closeBtn);
7985
+ if (loading) {
7986
+ const loadingEl = document.createElement("div");
7987
+ loadingEl.className = "accessify-describe-loading";
7988
+ loadingEl.textContent = isDE() ? "Bild wird analysiert…" : "Analyzing image…";
7989
+ tip.appendChild(loadingEl);
7990
+ } else {
7991
+ const textEl = document.createElement("div");
7992
+ textEl.textContent = text;
7993
+ tip.appendChild(textEl);
7994
+ }
8015
7995
  document.body.appendChild(tip);
8016
7996
  tooltipEl = tip;
8017
- const rect = img.getBoundingClientRect();
8018
7997
  const tipRect = tip.getBoundingClientRect();
8019
- let top = rect.top - tipRect.height - 8;
8020
- if (top < 4) top = rect.bottom + 8;
7998
+ let top = rect.bottom + 8;
7999
+ if (top + tipRect.height > window.innerHeight - 8) {
8000
+ top = rect.top - tipRect.height - 8;
8001
+ }
8002
+ if (top < 4) top = 4;
8021
8003
  let left = rect.left + (rect.width - tipRect.width) / 2;
8022
- left = Math.max(4, Math.min(left, window.innerWidth - tipRect.width - 4));
8004
+ left = Math.max(8, Math.min(left, window.innerWidth - tipRect.width - 8));
8023
8005
  tip.style.top = `${top}px`;
8024
8006
  tip.style.left = `${left}px`;
8025
8007
  }
8026
- function hideTooltip() {
8008
+ function hideDescribeTooltip() {
8027
8009
  tooltipEl?.remove();
8028
8010
  tooltipEl = null;
8029
8011
  }
8030
- function onMouseEnter(e) {
8031
- showTooltip(e.currentTarget);
8032
- }
8033
- function onMouseLeave() {
8034
- hideTooltip();
8035
- }
8036
- function addInfoButton(img) {
8037
- if (infoButtons.has(img)) return;
8038
- const parent = img.parentElement;
8039
- if (parent) {
8040
- const pos = getComputedStyle(parent).position;
8041
- if (pos === "static") parent.style.position = "relative";
8012
+ async function handleImageClick(img) {
8013
+ const rect = img.getBoundingClientRect();
8014
+ const cached = img.dataset.accessifyDescription;
8015
+ if (cached) {
8016
+ showDescribeTooltip(cached, rect);
8017
+ return;
8042
8018
  }
8043
- const btn = document.createElement("button");
8044
- btn.className = "accessify-alt-info-btn";
8045
- btn.textContent = "i";
8046
- btn.setAttribute("aria-label", isDE() ? "Alt-Text melden" : "Report alt text");
8047
- btn.addEventListener("click", (e) => {
8048
- e.preventDefault();
8049
- e.stopPropagation();
8050
- showReportPanel(img, btn);
8051
- });
8052
- img.insertAdjacentElement("afterend", btn);
8053
- infoButtons.set(img, btn);
8054
- }
8055
- function removeInfoButtons() {
8056
- infoButtons.forEach((btn) => btn.remove());
8057
- infoButtons.clear();
8058
- }
8059
- function showReportPanel(img, anchor) {
8060
- closeReportPanel();
8061
- const altText2 = img.getAttribute("alt") || "";
8062
- const panel = document.createElement("div");
8063
- panel.className = "accessify-alt-report-panel";
8064
- const btnRect = anchor.getBoundingClientRect();
8065
- panel.style.top = `${btnRect.bottom + 6}px`;
8066
- panel.style.left = `${Math.min(btnRect.left, window.innerWidth - 260)}px`;
8067
- const desc = document.createElement("p");
8068
- desc.textContent = isDE() ? "Alt-Text falsch? Melden Sie den Fehler." : "Alt text wrong? Report the error.";
8069
- panel.appendChild(desc);
8070
- const reportBtn = document.createElement("button");
8071
- reportBtn.className = "accessify-alt-report-btn";
8072
- reportBtn.textContent = isDE() ? "⚠ Fehler melden" : "⚠ Report error";
8073
- reportBtn.addEventListener("click", async () => {
8074
- await saveReport(img.src, altText2, lang());
8075
- await removeCachedAltText(img.src);
8076
- reportBtn.textContent = isDE() ? " Gemeldet" : " Reported";
8077
- reportBtn.classList.add("reported");
8019
+ const existingAlt = img.getAttribute("alt");
8020
+ showDescribeTooltip("", rect, true);
8021
+ try {
8022
+ if (!aiService) {
8023
+ showDescribeTooltip(
8024
+ existingAlt || (isDE() ? "Kein API-Key konfiguriert" : "No API key configured"),
8025
+ rect
8026
+ );
8027
+ return;
8028
+ }
8029
+ const src = getImageSrc(img);
8030
+ const description = await aiService.describeImage(src, lang());
8031
+ if (!enabled) return;
8032
+ img.dataset.accessifyDescription = description;
8033
+ showDescribeTooltip(description, rect);
8034
+ const currentAlt = img.getAttribute("alt");
8035
+ if (!currentAlt || !currentAlt.trim()) {
8036
+ try {
8037
+ const ctx = gatherImageContext(img);
8038
+ const altText2 = await aiService.generateAltText(src, ctx, lang());
8039
+ if (altText2) {
8040
+ img.setAttribute("alt", altText2);
8041
+ img.setAttribute("title", altText2);
8042
+ setCachedAltText(src, altText2, lang()).catch(() => {
8043
+ });
8044
+ if (siteKey) persistAltTextToServer(siteKey, proxyUrl, src, altText2, lang());
8045
+ }
8046
+ } catch {
8047
+ }
8048
+ }
8049
+ } catch (err) {
8050
+ console.warn("[Accessify] Image description failed:", err);
8051
+ showDescribeTooltip(
8052
+ existingAlt || (isDE() ? "Beschreibung konnte nicht geladen werden" : "Could not load description"),
8053
+ rect
8054
+ );
8055
+ }
8056
+ }
8057
+ function enableClickToDescribe() {
8058
+ document.querySelectorAll("img").forEach((img) => {
8059
+ if (img.closest("#accessify-root") || img.closest("accessify-widget")) return;
8060
+ if (img.complete && img.naturalWidth > 0 && (img.naturalWidth < 20 || img.naturalHeight < 20)) return;
8061
+ img.dataset.a11yDescribable = "true";
8078
8062
  });
8079
- panel.appendChild(reportBtn);
8080
- document.body.appendChild(panel);
8081
- reportPanelEl = panel;
8082
- setTimeout(() => {
8083
- const handleOutside = (e) => {
8084
- if (!panel.contains(e.target) && e.target !== anchor) {
8085
- closeReportPanel();
8086
- document.removeEventListener("click", handleOutside);
8063
+ describeClickHandler = (e) => {
8064
+ const img = e.target.closest("img[data-a11y-describable]");
8065
+ if (!img) {
8066
+ if (tooltipEl && !e.target.closest(".accessify-describe-tooltip")) {
8067
+ hideDescribeTooltip();
8087
8068
  }
8088
- };
8089
- document.addEventListener("click", handleOutside);
8090
- }, 50);
8091
- const handleEsc = (e) => {
8092
- if (e.key === "Escape") {
8093
- closeReportPanel();
8094
- document.removeEventListener("keydown", handleEsc);
8069
+ return;
8095
8070
  }
8071
+ e.preventDefault();
8072
+ e.stopPropagation();
8073
+ handleImageClick(img);
8096
8074
  };
8097
- document.addEventListener("keydown", handleEsc);
8075
+ document.addEventListener("click", describeClickHandler, true);
8076
+ describeEscHandler = (e) => {
8077
+ if (e.key === "Escape") hideDescribeTooltip();
8078
+ };
8079
+ document.addEventListener("keydown", describeEscHandler);
8098
8080
  }
8099
- function closeReportPanel() {
8100
- reportPanelEl?.remove();
8101
- reportPanelEl = null;
8081
+ function disableClickToDescribe() {
8082
+ if (describeClickHandler) {
8083
+ document.removeEventListener("click", describeClickHandler, true);
8084
+ describeClickHandler = null;
8085
+ }
8086
+ if (describeEscHandler) {
8087
+ document.removeEventListener("keydown", describeEscHandler);
8088
+ describeEscHandler = null;
8089
+ }
8090
+ hideDescribeTooltip();
8091
+ document.querySelectorAll("img[data-a11y-describable]").forEach((img) => {
8092
+ delete img.dataset.a11yDescribable;
8093
+ });
8102
8094
  }
8103
8095
  function isGenericAlt(alt) {
8104
8096
  if (!alt.trim()) return true;
@@ -8107,67 +8099,54 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8107
8099
  if (/\.(jpg|jpeg|png|gif|webp|svg|avif)$/i.test(alt)) return true;
8108
8100
  return false;
8109
8101
  }
8102
+ function isDecorativeImage(img) {
8103
+ const role = img.getAttribute("role");
8104
+ if (role === "presentation" || role === "none") return true;
8105
+ if (img.complete && img.naturalWidth > 0 && (img.naturalWidth < 50 || img.naturalHeight < 50)) return true;
8106
+ return false;
8107
+ }
8108
+ function needsAltText(img) {
8109
+ if (img.closest("#accessify-root") || img.closest("accessify-widget")) return false;
8110
+ if (img.complete && img.naturalWidth > 0 && (img.naturalWidth < 20 || img.naturalHeight < 20)) return false;
8111
+ const alt = img.getAttribute("alt");
8112
+ if (alt === null) return true;
8113
+ if (isGenericAlt(alt)) return true;
8114
+ if (alt === "" && !isDecorativeImage(img)) return true;
8115
+ return false;
8116
+ }
8110
8117
  function scanForMissingAlt() {
8111
8118
  const missing = [];
8112
8119
  document.querySelectorAll("img").forEach((img) => {
8113
- if (img.closest("#accessify-root") || img.closest("accessify-widget")) return;
8114
- if (img.complete && img.naturalWidth > 0 && (img.naturalWidth < 20 || img.naturalHeight < 20)) return;
8115
- const alt = img.getAttribute("alt");
8116
- if (alt === "") {
8117
- const role = img.getAttribute("role");
8118
- if (role === "presentation" || role === "none") return;
8119
- if (img.complete && img.naturalWidth > 0 && (img.naturalWidth < 50 || img.naturalHeight < 50)) return;
8120
- missing.push(img);
8121
- return;
8122
- }
8123
- if (alt === null || isGenericAlt(alt)) missing.push(img);
8120
+ if (needsAltText(img)) missing.push(img);
8124
8121
  });
8125
8122
  document.querySelectorAll("picture").forEach((picture) => {
8126
8123
  if (picture.closest("#accessify-root") || picture.closest("accessify-widget")) return;
8127
8124
  const img = picture.querySelector("img");
8128
- if (img && !missing.includes(img)) {
8129
- const alt = img.getAttribute("alt");
8130
- if (alt === null || isGenericAlt(alt)) {
8131
- missing.push(img);
8132
- }
8133
- }
8134
- });
8135
- document.querySelectorAll("*").forEach((el) => {
8136
- if (el.closest("#accessify-root") || el.closest("accessify-widget")) return;
8137
- if (el.tagName === "SCRIPT" || el.tagName === "STYLE") return;
8138
- const bg = window.getComputedStyle(el).backgroundImage;
8139
- if (!bg || bg === "none" || !bg.startsWith("url(")) return;
8140
- const src = bg.match(/url\(["']?(.+?)["']?\)/)?.[1];
8141
- if (!src || src.includes("data:") || src.includes("gradient")) return;
8142
- if (el.textContent?.trim() || el.getAttribute("aria-label")) return;
8143
- if (!el.dataset.accessifyBgSrc) {
8144
- el.dataset.accessifyBgSrc = src;
8125
+ if (img && !missing.includes(img) && needsAltText(img)) {
8126
+ missing.push(img);
8145
8127
  }
8146
8128
  });
8147
8129
  return missing;
8148
8130
  }
8149
- function scanWithExistingAlt(excludeMissing) {
8150
- const existing = [];
8151
- const missingSet = new Set(excludeMissing);
8152
- document.querySelectorAll("img").forEach((img) => {
8153
- if (img.closest("#accessify-root") || img.closest("accessify-widget")) return;
8154
- if (missingSet.has(img)) return;
8155
- if (img.complete && img.naturalWidth > 0 && (img.naturalWidth < 20 || img.naturalHeight < 20)) return;
8156
- const alt = img.getAttribute("alt");
8157
- if (alt && alt.trim() && !isGenericAlt(alt)) {
8158
- existing.push(img);
8159
- }
8160
- });
8161
- return existing;
8162
- }
8163
- function scanAutoApplied() {
8164
- const images = document.querySelectorAll('img[data-accessify-alt="auto"]');
8165
- const applied = [];
8166
- images.forEach((img) => {
8167
- if (img.closest("#accessify-root")) return;
8168
- if (!processedImages.has(img)) applied.push(img);
8169
- });
8170
- return applied;
8131
+ function gatherImageContext(img) {
8132
+ const parts = [];
8133
+ try {
8134
+ const url = new URL(getImageSrc(img), window.location.href);
8135
+ const filename = url.pathname.split("/").pop();
8136
+ if (filename) parts.push(`Filename: ${filename}`);
8137
+ } catch {
8138
+ }
8139
+ const figure = img.closest("figure");
8140
+ if (figure) {
8141
+ const caption = figure.querySelector("figcaption");
8142
+ if (caption?.textContent?.trim()) parts.push(`Caption: ${caption.textContent.trim()}`);
8143
+ }
8144
+ const container = img.parentElement?.parentElement || img.parentElement;
8145
+ if (container) {
8146
+ const heading = container.querySelector("h1, h2, h3, h4, h5, h6");
8147
+ if (heading?.textContent?.trim()) parts.push(`Nearby heading: ${heading.textContent.trim()}`);
8148
+ }
8149
+ return parts.join(". ");
8171
8150
  }
8172
8151
  function applyAltText(img, altText2) {
8173
8152
  img.setAttribute("alt", altText2);
@@ -8176,12 +8155,7 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8176
8155
  if (enabled) {
8177
8156
  img.classList.add("accessify-alt-done");
8178
8157
  img.classList.remove("accessify-alt-generating");
8179
- addInfoButton(img);
8180
8158
  }
8181
- img.removeEventListener("mouseenter", onMouseEnter);
8182
- img.removeEventListener("mouseleave", onMouseLeave);
8183
- img.addEventListener("mouseenter", onMouseEnter);
8184
- img.addEventListener("mouseleave", onMouseLeave);
8185
8159
  }
8186
8160
  async function generateAll() {
8187
8161
  if (autoGenerating || !enabled || !aiService) return;
@@ -8190,7 +8164,8 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8190
8164
  const uncached = [];
8191
8165
  for (const img of missingAltImages) {
8192
8166
  if (processedImages.has(img)) continue;
8193
- const cached = await getCachedAltText(img.src);
8167
+ const src = getImageSrc(img);
8168
+ const cached = await getCachedAltText(src);
8194
8169
  if (cached) {
8195
8170
  applyAltText(img, cached);
8196
8171
  } else {
@@ -8209,16 +8184,17 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8209
8184
  await Promise.all(batch.map(async (img) => {
8210
8185
  if (!enabled) return;
8211
8186
  try {
8187
+ const src = getImageSrc(img);
8212
8188
  const ctx = gatherImageContext(img);
8213
- const alt = await aiService.generateAltText(img.src, ctx, lang());
8189
+ const alt = await aiService.generateAltText(src, ctx, lang());
8214
8190
  if (alt && enabled) {
8215
8191
  applyAltText(img, alt);
8216
- setCachedAltText(img.src, alt, lang()).catch(() => {
8192
+ setCachedAltText(src, alt, lang()).catch(() => {
8217
8193
  });
8218
- if (siteKey) persistAltTextToServer(siteKey, proxyUrl, img.src, alt, lang());
8194
+ if (siteKey) persistAltTextToServer(siteKey, proxyUrl, src, alt, lang());
8219
8195
  }
8220
8196
  } catch (err) {
8221
- console.warn("[Accessify] Alt-text generation failed:", img.src, err);
8197
+ console.warn("[Accessify] Alt-text generation failed:", getImageSrc(img), err);
8222
8198
  img.classList.remove("accessify-alt-generating");
8223
8199
  }
8224
8200
  }));
@@ -8228,7 +8204,8 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8228
8204
  }
8229
8205
  async function generateSingle(img) {
8230
8206
  if (!aiService || !enabled) return;
8231
- const cached = await getCachedAltText(img.src);
8207
+ const src = getImageSrc(img);
8208
+ const cached = await getCachedAltText(src);
8232
8209
  if (cached) {
8233
8210
  applyAltText(img, cached);
8234
8211
  updateBadge();
@@ -8237,16 +8214,16 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8237
8214
  img.classList.add("accessify-alt-generating");
8238
8215
  try {
8239
8216
  const ctx = gatherImageContext(img);
8240
- const alt = await aiService.generateAltText(img.src, ctx, lang());
8217
+ const alt = await aiService.generateAltText(src, ctx, lang());
8241
8218
  if (alt && enabled) {
8242
8219
  applyAltText(img, alt);
8243
- setCachedAltText(img.src, alt, lang()).catch(() => {
8220
+ setCachedAltText(src, alt, lang()).catch(() => {
8244
8221
  });
8245
- if (siteKey) persistAltTextToServer(siteKey, proxyUrl, img.src, alt, lang());
8222
+ if (siteKey) persistAltTextToServer(siteKey, proxyUrl, src, alt, lang());
8246
8223
  updateBadge();
8247
8224
  }
8248
8225
  } catch (err) {
8249
- console.warn("[Accessify] Alt-text generation failed:", img.src, err);
8226
+ console.warn("[Accessify] Alt-text generation failed:", src, err);
8250
8227
  img.classList.remove("accessify-alt-generating");
8251
8228
  }
8252
8229
  }
@@ -8254,38 +8231,35 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8254
8231
  removeBadge();
8255
8232
  const remaining = missingAltImages.filter((img) => !processedImages.has(img)).length;
8256
8233
  const generating = missingAltImages.filter((img) => img.classList.contains("accessify-alt-generating")).length;
8257
- const generated = processedImages.size;
8258
- const withExisting = existingAltImages.length;
8259
- const totalImages = missingAltImages.length + withExisting;
8260
8234
  if (generating > 0) {
8261
8235
  showBadge(isDE() ? `${generating} Bilder werden analysiert…` : `Analyzing ${generating} images…`, "#f77f00", generating);
8262
8236
  return;
8263
8237
  }
8264
- if (remaining === 0 && totalImages > 0) {
8265
- const parts = [];
8266
- if (withExisting > 0) parts.push(isDE() ? `${withExisting} ✓` : `${withExisting} ✓`);
8267
- if (generated > 0) parts.push(isDE() ? `${generated} erzeugt` : `${generated} generated`);
8268
- const summary = `${totalImages} ${isDE() ? "Bilder" : "images"}: ${parts.join(", ")}`;
8269
- showBadge(summary, "#2a9d8f", 0);
8270
- setTimeout(() => removeBadge(), 8e3);
8238
+ if (remaining === 0 && processedImages.size > 0) {
8239
+ showBadge(
8240
+ isDE() ? `✓ ${processedImages.size} Bildbeschreibungen erstellt` : `✓ ${processedImages.size} descriptions generated`,
8241
+ "#2a9d8f",
8242
+ 0
8243
+ );
8244
+ setTimeout(() => removeBadge(), 6e3);
8271
8245
  return;
8272
8246
  }
8273
8247
  if (remaining > 0) {
8274
- const statusParts = [];
8275
- if (withExisting > 0) statusParts.push(`${withExisting} ✓`);
8276
- statusParts.push(isDE() ? `${remaining} ohne Alt-Text` : `${remaining} missing`);
8277
- const text = `${totalImages} ${isDE() ? "Bilder" : "images"}: ${statusParts.join(", ")}`;
8278
- showBadge(text, "#e63946", remaining);
8248
+ showBadge(
8249
+ isDE() ? `${remaining} Bilder ohne Beschreibung` : `${remaining} images without description`,
8250
+ "#e63946",
8251
+ remaining
8252
+ );
8279
8253
  }
8280
8254
  }
8281
8255
  function showBadge(text, color, count) {
8282
8256
  const badge = document.createElement("div");
8283
- badge.className = BADGE_CLASS;
8257
+ badge.className = "accessify-alt-badge";
8284
8258
  badge.setAttribute("role", "status");
8285
8259
  badge.setAttribute("aria-live", "polite");
8286
8260
  if (count > 0) {
8287
8261
  const c = document.createElement("span");
8288
- c.className = `${BADGE_CLASS}-count`;
8262
+ c.className = "accessify-alt-badge-count";
8289
8263
  c.style.background = color;
8290
8264
  c.textContent = String(count);
8291
8265
  badge.appendChild(c);
@@ -8300,108 +8274,67 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8300
8274
  badgeEl?.remove();
8301
8275
  badgeEl = null;
8302
8276
  }
8303
- function gatherImageContext(img) {
8304
- const parts = [];
8305
- try {
8306
- const url = new URL(img.src, window.location.href);
8307
- const filename = url.pathname.split("/").pop();
8308
- if (filename) parts.push(`Filename: ${filename}`);
8309
- } catch {
8310
- }
8311
- const figure = img.closest("figure");
8312
- if (figure) {
8313
- const caption = figure.querySelector("figcaption");
8314
- if (caption?.textContent?.trim()) parts.push(`Caption: ${caption.textContent.trim()}`);
8315
- }
8316
- const parent = img.parentElement;
8317
- if (parent) {
8318
- const heading = parent.querySelector("h1, h2, h3, h4, h5, h6");
8319
- if (heading?.textContent?.trim()) parts.push(`Nearby heading: ${heading.textContent.trim()}`);
8320
- }
8321
- const describedBy = img.getAttribute("aria-describedby");
8322
- if (describedBy) {
8323
- const descEl = document.getElementById(describedBy);
8324
- if (descEl?.textContent?.trim()) parts.push(`Description: ${descEl.textContent.trim()}`);
8325
- }
8326
- return parts.join(". ");
8327
- }
8328
- let domObserver = null;
8329
8277
  function tryRegisterImage(img) {
8330
8278
  if (!enabled) return;
8331
8279
  if (img.closest("#accessify-root")) return;
8332
- if (missingAltImages.includes(img) || existingAltImages.includes(img)) return;
8280
+ if (missingAltImages.includes(img)) return;
8333
8281
  function addIfValid() {
8334
- if (!enabled || missingAltImages.includes(img) || existingAltImages.includes(img)) return;
8282
+ if (!enabled || missingAltImages.includes(img)) return;
8335
8283
  if (img.naturalWidth < 20 || img.naturalHeight < 20) return;
8336
- const alt = img.getAttribute("alt");
8337
- const isMissing = alt === null || isGenericAlt(alt) || alt === "" && !isDecorativeImage(img);
8338
- if (isMissing) {
8284
+ img.dataset.a11yDescribable = "true";
8285
+ if (needsAltText(img)) {
8339
8286
  img.classList.add(HIGHLIGHT_CLASS);
8340
8287
  missingAltImages.push(img);
8341
8288
  updateBadge();
8342
8289
  generateSingle(img);
8343
- } else if (alt && alt.trim()) {
8344
- existingAltImages.push(img);
8345
- img.classList.add(HAS_ALT_CLASS);
8346
- img.removeEventListener("mouseenter", onMouseEnter);
8347
- img.removeEventListener("mouseleave", onMouseLeave);
8348
- img.addEventListener("mouseenter", onMouseEnter);
8349
- img.addEventListener("mouseleave", onMouseLeave);
8350
- updateBadge();
8351
8290
  }
8352
8291
  }
8353
8292
  if (img.complete && img.naturalWidth > 0) addIfValid();
8354
8293
  else img.addEventListener("load", addIfValid, { once: true });
8355
8294
  }
8356
- function isDecorativeImage(img) {
8357
- const role = img.getAttribute("role");
8358
- if (role === "presentation" || role === "none") return true;
8359
- if (img.complete && img.naturalWidth > 0 && (img.naturalWidth < 50 || img.naturalHeight < 50)) return true;
8360
- return false;
8361
- }
8362
8295
  function activate() {
8363
8296
  if (enabled) return;
8364
8297
  enabled = true;
8365
8298
  injectStyles();
8366
- const alreadyApplied = scanAutoApplied();
8367
- for (const img of alreadyApplied) {
8368
- const alt = img.getAttribute("alt");
8369
- processedImages.set(img, { generatedAlt: alt });
8370
- missingAltImages.push(img);
8371
- img.classList.add(HIGHLIGHT_CLASS, "accessify-alt-done");
8372
- addInfoButton(img);
8373
- img.removeEventListener("mouseenter", onMouseEnter);
8374
- img.removeEventListener("mouseleave", onMouseLeave);
8375
- img.addEventListener("mouseenter", onMouseEnter);
8376
- img.addEventListener("mouseleave", onMouseLeave);
8377
- }
8299
+ const alreadyApplied = document.querySelectorAll('img[data-accessify-alt="auto"]');
8300
+ alreadyApplied.forEach((img) => {
8301
+ if (img.closest("#accessify-root")) return;
8302
+ if (!processedImages.has(img)) {
8303
+ processedImages.set(img, { generatedAlt: img.getAttribute("alt") || "" });
8304
+ missingAltImages.push(img);
8305
+ img.classList.add(HIGHLIGHT_CLASS, "accessify-alt-done");
8306
+ }
8307
+ });
8378
8308
  const missing = scanForMissingAlt();
8379
8309
  for (const img of missing) {
8380
- missingAltImages.push(img);
8381
- img.classList.add(HIGHLIGHT_CLASS);
8382
- }
8383
- const withAlt = scanWithExistingAlt(missingAltImages);
8384
- for (const img of withAlt) {
8385
- existingAltImages.push(img);
8386
- img.classList.add(HAS_ALT_CLASS);
8387
- img.removeEventListener("mouseenter", onMouseEnter);
8388
- img.removeEventListener("mouseleave", onMouseLeave);
8389
- img.addEventListener("mouseenter", onMouseEnter);
8390
- img.addEventListener("mouseleave", onMouseLeave);
8310
+ if (!missingAltImages.includes(img)) {
8311
+ missingAltImages.push(img);
8312
+ img.classList.add(HIGHLIGHT_CLASS);
8313
+ }
8391
8314
  }
8392
8315
  updateBadge();
8393
8316
  generateAll();
8317
+ enableClickToDescribe();
8394
8318
  document.querySelectorAll("img").forEach((img) => {
8395
8319
  if (!img.complete) tryRegisterImage(img);
8396
8320
  });
8397
8321
  setTimeout(() => {
8398
- if (enabled) document.querySelectorAll("img").forEach(tryRegisterImage);
8322
+ if (enabled) {
8323
+ document.querySelectorAll("img").forEach(tryRegisterImage);
8324
+ }
8399
8325
  }, 2e3);
8400
8326
  domObserver = new MutationObserver((mutations) => {
8401
8327
  for (const m of mutations) {
8402
8328
  for (const node of m.addedNodes) {
8403
8329
  if (node instanceof HTMLImageElement) tryRegisterImage(node);
8404
- else if (node instanceof HTMLElement) node.querySelectorAll("img").forEach(tryRegisterImage);
8330
+ else if (node instanceof HTMLElement) {
8331
+ node.querySelectorAll("img").forEach(tryRegisterImage);
8332
+ node.querySelectorAll("img").forEach((img) => {
8333
+ if (!img.closest("#accessify-root") && !img.closest("accessify-widget")) {
8334
+ img.dataset.a11yDescribable = "true";
8335
+ }
8336
+ });
8337
+ }
8405
8338
  }
8406
8339
  }
8407
8340
  });
@@ -8412,26 +8345,20 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8412
8345
  autoGenerating = false;
8413
8346
  domObserver?.disconnect();
8414
8347
  domObserver = null;
8415
- hideTooltip();
8416
- closeReportPanel();
8417
- removeInfoButtons();
8348
+ disableClickToDescribe();
8418
8349
  removeBadge();
8419
8350
  missingAltImages.forEach((img) => {
8420
8351
  img.classList.remove(HIGHLIGHT_CLASS, "accessify-alt-done", "accessify-alt-generating");
8421
8352
  });
8422
8353
  missingAltImages = [];
8423
- existingAltImages.forEach((img) => {
8424
- img.classList.remove(HAS_ALT_CLASS);
8425
- });
8426
- existingAltImages = [];
8427
8354
  removeStyles();
8428
8355
  }
8429
8356
  autoApplyCachedAltTexts({ siteKey, proxyUrl, lang: initialLang }).catch(() => {
8430
8357
  });
8431
8358
  return {
8432
8359
  id: "alt-text",
8433
- name: () => isDE() ? "Alt-Text erzeugen" : "Alt-Text Generator",
8434
- description: isDE() ? "Bildbeschreibungen automatisch erstellen" : "Auto-generate image descriptions",
8360
+ name: () => isDE() ? "Bildbeschreibung" : "Image Description",
8361
+ description: isDE() ? "Bilder per Klick beschreiben lassen" : "Click images to get AI descriptions",
8435
8362
  icon: "alt-text",
8436
8363
  category: "ai",
8437
8364
  activate,
@@ -8440,7 +8367,7 @@ function createAltTextModule(aiService, initialLang = "de", serverConfig) {
8440
8367
  id: "alt-text",
8441
8368
  enabled,
8442
8369
  value: {
8443
- missingCount: missingAltImages.length,
8370
+ missingCount: missingAltImages.filter((img) => !processedImages.has(img)).length,
8444
8371
  processedCount: processedImages.size
8445
8372
  }
8446
8373
  })
@@ -8637,4 +8564,4 @@ export {
8637
8564
  init as i,
8638
8565
  t
8639
8566
  };
8640
- //# sourceMappingURL=index-DwxnT3JK.js.map
8567
+ //# sourceMappingURL=index-DlEXo7aY.js.map