@intlayer/design-system 8.9.8 → 8.10.0-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/Browser/Browser.content.mjs +120 -0
- package/dist/esm/components/Browser/Browser.content.mjs.map +1 -1
- package/dist/esm/components/Browser/Browser.mjs +131 -9
- package/dist/esm/components/Browser/Browser.mjs.map +1 -1
- package/dist/esm/components/Browser/extractUrlFromSitemap.mjs +44 -0
- package/dist/esm/components/Browser/extractUrlFromSitemap.mjs.map +1 -0
- package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs +49 -1
- package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/KeyPathBreadcrumb.mjs +1 -1
- package/dist/esm/components/DictionaryFieldEditor/KeyPathBreadcrumb.mjs.map +1 -1
- package/dist/esm/components/LocaleSwitcherContentDropDown/LocaleSwitcherContent.mjs +1 -1
- package/dist/esm/components/LocaleSwitcherContentDropDown/LocaleSwitcherContentContext.mjs +8 -1
- package/dist/esm/components/LocaleSwitcherContentDropDown/LocaleSwitcherContentContext.mjs.map +1 -1
- package/dist/esm/components/LocaleSwitcherDropDown/LocaleSwitcher.mjs +1 -1
- package/dist/esm/components/WithResizer/index.mjs +12 -9
- package/dist/esm/components/WithResizer/index.mjs.map +1 -1
- package/dist/esm/components/index.mjs +2 -2
- package/dist/esm/hooks/useAuth/useOAuth2.mjs +1 -1
- package/dist/esm/hooks/useAuth/useSession.mjs +1 -1
- package/dist/types/components/Badge/index.d.ts +2 -2
- package/dist/types/components/Browser/Browser.content.d.ts +120 -0
- package/dist/types/components/Browser/Browser.d.ts.map +1 -1
- package/dist/types/components/Browser/extractUrlFromSitemap.d.ts +5 -0
- package/dist/types/components/Browser/extractUrlFromSitemap.d.ts.map +1 -0
- package/dist/types/components/Button/Button.d.ts +5 -5
- package/dist/types/components/CollapsibleTable/CollapsibleTable.d.ts +2 -2
- package/dist/types/components/Command/index.d.ts +3 -3
- package/dist/types/components/Container/index.d.ts +6 -6
- package/dist/types/components/DictionaryFieldEditor/ContentEditorView/TextEditor.d.ts.map +1 -1
- package/dist/types/components/Input/Checkbox.d.ts +2 -2
- package/dist/types/components/Link/Link.d.ts +4 -4
- package/dist/types/components/LocaleSwitcherContentDropDown/LocaleSwitcherContentContext.d.ts.map +1 -1
- package/dist/types/components/Pagination/Pagination.d.ts +2 -2
- package/dist/types/components/SwitchSelector/SwitchSelector.d.ts +1 -1
- package/dist/types/components/SwitchSelector/VerticalSwitchSelector.d.ts +1 -1
- package/dist/types/components/Tab/Tab.d.ts +2 -2
- package/dist/types/components/TabSelector/TabSelector.d.ts +1 -1
- package/dist/types/components/Tag/index.d.ts +3 -3
- package/dist/types/components/Toaster/Toast.d.ts +1 -1
- package/package.json +20 -20
|
@@ -144,6 +144,126 @@ const browserContent = {
|
|
|
144
144
|
id: "Halaman web tertanam",
|
|
145
145
|
vi: "Trang web được nhúng",
|
|
146
146
|
uk: "Вбудована веб-сторінка"
|
|
147
|
+
}),
|
|
148
|
+
sitemapButtonLabel: t({
|
|
149
|
+
en: "Scan sitemap",
|
|
150
|
+
"en-GB": "Scan sitemap",
|
|
151
|
+
fr: "Scanner le sitemap",
|
|
152
|
+
es: "Escanear sitemap",
|
|
153
|
+
de: "Sitemap scannen",
|
|
154
|
+
ja: "サイトマップをスキャン",
|
|
155
|
+
ko: "사이트맵 스캔",
|
|
156
|
+
zh: "扫描网站地图",
|
|
157
|
+
it: "Scansiona sitemap",
|
|
158
|
+
pt: "Escanear sitemap",
|
|
159
|
+
hi: "साइटमैप स्कैन करें",
|
|
160
|
+
ar: "مسح خريطة الموقع",
|
|
161
|
+
ru: "Сканировать карту сайта",
|
|
162
|
+
tr: "Site haritasını tara",
|
|
163
|
+
pl: "Skanuj mapę witryny",
|
|
164
|
+
id: "Pindai sitemap",
|
|
165
|
+
vi: "Quét sitemap",
|
|
166
|
+
uk: "Сканувати карту сайту"
|
|
167
|
+
}),
|
|
168
|
+
sitemapSearchPlaceholder: t({
|
|
169
|
+
en: "Filter URLs…",
|
|
170
|
+
"en-GB": "Filter URLs…",
|
|
171
|
+
fr: "Filtrer les URLs…",
|
|
172
|
+
es: "Filtrar URLs…",
|
|
173
|
+
de: "URLs filtern…",
|
|
174
|
+
ja: "URLを絞り込む…",
|
|
175
|
+
ko: "URL 필터링…",
|
|
176
|
+
zh: "过滤URL…",
|
|
177
|
+
it: "Filtra URL…",
|
|
178
|
+
pt: "Filtrar URLs…",
|
|
179
|
+
hi: "URL फ़िल्टर करें…",
|
|
180
|
+
ar: "تصفية الروابط…",
|
|
181
|
+
ru: "Фильтр URL…",
|
|
182
|
+
tr: "URL filtrele…",
|
|
183
|
+
pl: "Filtruj URL…",
|
|
184
|
+
id: "Filter URL…",
|
|
185
|
+
vi: "Lọc URL…",
|
|
186
|
+
uk: "Фільтр URL…"
|
|
187
|
+
}),
|
|
188
|
+
sitemapSearchAriaLabel: t({
|
|
189
|
+
en: "Search sitemap URLs",
|
|
190
|
+
"en-GB": "Search sitemap URLs",
|
|
191
|
+
fr: "Rechercher les URLs du sitemap",
|
|
192
|
+
es: "Buscar URLs del sitemap",
|
|
193
|
+
de: "Sitemap-URLs durchsuchen",
|
|
194
|
+
ja: "サイトマップURLを検索",
|
|
195
|
+
ko: "사이트맵 URL 검색",
|
|
196
|
+
zh: "搜索网站地图URL",
|
|
197
|
+
it: "Cerca URL del sitemap",
|
|
198
|
+
pt: "Pesquisar URLs do sitemap",
|
|
199
|
+
hi: "साइटमैप URL खोजें",
|
|
200
|
+
ar: "ابحث في روابط خريطة الموقع",
|
|
201
|
+
ru: "Поиск URL карты сайта",
|
|
202
|
+
tr: "Site haritası URL ara",
|
|
203
|
+
pl: "Szukaj URL mapy witryny",
|
|
204
|
+
id: "Cari URL sitemap",
|
|
205
|
+
vi: "Tìm kiếm URL sitemap",
|
|
206
|
+
uk: "Пошук URL карти сайту"
|
|
207
|
+
}),
|
|
208
|
+
sitemapLoading: t({
|
|
209
|
+
en: "Scanning sitemap…",
|
|
210
|
+
"en-GB": "Scanning sitemap…",
|
|
211
|
+
fr: "Scan du sitemap en cours…",
|
|
212
|
+
es: "Escaneando sitemap…",
|
|
213
|
+
de: "Sitemap wird gescannt…",
|
|
214
|
+
ja: "サイトマップをスキャン中…",
|
|
215
|
+
ko: "사이트맵 스캔 중…",
|
|
216
|
+
zh: "正在扫描网站地图…",
|
|
217
|
+
it: "Scansione sitemap in corso…",
|
|
218
|
+
pt: "Escaneando sitemap…",
|
|
219
|
+
hi: "साइटमैप स्कैन हो रहा है…",
|
|
220
|
+
ar: "جارٍ مسح خريطة الموقع…",
|
|
221
|
+
ru: "Сканирование карты сайта…",
|
|
222
|
+
tr: "Site haritası taranıyor…",
|
|
223
|
+
pl: "Skanowanie mapy witryny…",
|
|
224
|
+
id: "Memindai sitemap…",
|
|
225
|
+
vi: "Đang quét sitemap…",
|
|
226
|
+
uk: "Сканування карти сайту…"
|
|
227
|
+
}),
|
|
228
|
+
sitemapEmpty: t({
|
|
229
|
+
en: "No URLs found",
|
|
230
|
+
"en-GB": "No URLs found",
|
|
231
|
+
fr: "Aucune URL trouvée",
|
|
232
|
+
es: "No se encontraron URLs",
|
|
233
|
+
de: "Keine URLs gefunden",
|
|
234
|
+
ja: "URLが見つかりません",
|
|
235
|
+
ko: "URL을 찾을 수 없습니다",
|
|
236
|
+
zh: "未找到URL",
|
|
237
|
+
it: "Nessun URL trovato",
|
|
238
|
+
pt: "Nenhuma URL encontrada",
|
|
239
|
+
hi: "कोई URL नहीं मिला",
|
|
240
|
+
ar: "لم يتم العثور على روابط",
|
|
241
|
+
ru: "URL не найдены",
|
|
242
|
+
tr: "URL bulunamadı",
|
|
243
|
+
pl: "Nie znaleziono URL",
|
|
244
|
+
id: "Tidak ada URL yang ditemukan",
|
|
245
|
+
vi: "Không tìm thấy URL",
|
|
246
|
+
uk: "URL не знайдено"
|
|
247
|
+
}),
|
|
248
|
+
sitemapError: t({
|
|
249
|
+
en: "Could not load sitemap",
|
|
250
|
+
"en-GB": "Could not load sitemap",
|
|
251
|
+
fr: "Impossible de charger le sitemap",
|
|
252
|
+
es: "No se pudo cargar el sitemap",
|
|
253
|
+
de: "Sitemap konnte nicht geladen werden",
|
|
254
|
+
ja: "サイトマップを読み込めませんでした",
|
|
255
|
+
ko: "사이트맵을 불러올 수 없습니다",
|
|
256
|
+
zh: "无法加载网站地图",
|
|
257
|
+
it: "Impossibile caricare il sitemap",
|
|
258
|
+
pt: "Não foi possível carregar o sitemap",
|
|
259
|
+
hi: "साइटमैप लोड नहीं हो सका",
|
|
260
|
+
ar: "تعذّر تحميل خريطة الموقع",
|
|
261
|
+
ru: "Не удалось загрузить карту сайта",
|
|
262
|
+
tr: "Site haritası yüklenemedi",
|
|
263
|
+
pl: "Nie można załadować mapy witryny",
|
|
264
|
+
id: "Tidak dapat memuat sitemap",
|
|
265
|
+
vi: "Không thể tải sitemap",
|
|
266
|
+
uk: "Не вдалося завантажити карту сайту"
|
|
147
267
|
})
|
|
148
268
|
},
|
|
149
269
|
title: "Embedded browser component",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Browser.content.mjs","names":[],"sources":["../../../../src/components/Browser/Browser.content.ts"],"sourcesContent":["import { type Dictionary, t } from 'intlayer';\n\nexport const browserContent = {\n key: 'browser',\n content: {\n ariaLabel: t({\n en: 'Embedded browser',\n 'en-GB': 'Embedded browser',\n fr: 'Navigateur intégré',\n es: 'Navegador integrado',\n de: 'Eingebetteter Browser',\n ja: '埋め込みブラウザ',\n ko: '임베디드 브라우저',\n zh: '嵌入式浏览器',\n it: 'Browser incorporato',\n pt: 'Navegador incorporado',\n hi: 'एम्बेडेड ब्राउज़र',\n ar: 'متصفح مدمج',\n ru: 'Встроенный браузер',\n tr: 'Gömülü tarayıcı',\n pl: 'Osadzona przeglądarka',\n id: 'Peramban tertanam',\n vi: 'Trình duyệt nhúng',\n uk: 'Вбудований браузер',\n }),\n urlLabel: t({\n en: 'URL address bar',\n 'en-GB': 'URL address bar',\n fr: \"Barre d'adresse URL\",\n es: 'Barra de direcciones URL',\n de: 'URL-Adressleiste',\n ja: 'URLアドレスバー',\n ko: 'URL 주소 표시줄',\n zh: 'URL地址栏',\n it: 'Barra degli indirizzi URL',\n pt: 'Barra de endereços URL',\n hi: 'URL पता बार',\n ar: 'شريط عنوان URL',\n ru: 'Адресная строка URL',\n tr: 'URL adres çubuğu',\n pl: 'Pasek adresu URL',\n id: 'Bilah alamat URL',\n vi: 'Thanh địa chỉ URL',\n uk: 'Рядок адреси URL',\n }),\n urlPlaceholder: 'https://example.com',\n errorMessage: t({\n en: 'Invalid URL. Try a path like \"/page\" or a full URL.',\n 'en-GB': 'Invalid URL. Try a path like \"/page\" or a full URL.',\n fr: 'URL invalide. Essayez un chemin comme \"/page\" ou une URL complète.',\n es: 'URL no válida. Intente una ruta como \"/page\" o una URL completa.',\n de: 'Ungültige URL. Versuchen Sie einen Pfad wie \"/page\" oder eine vollständige URL.',\n ja: '無効なURL。\"/page\"のようなパスまたは完全なURLを試してください。',\n ko: '잘못된 URL입니다. \"/page\"와 같은 경로 또는 전체 URL을 시도하십시오.',\n zh: '无效的URL。尝试像\"/page\"这样的路径或完整的URL。',\n it: 'URL non valido. Prova un percorso come \"/page\" o un URL completo.',\n pt: 'URL inválido. Tente um caminho como \"/page\" ou uma URL completa.',\n hi: 'अमान्य URL। \"/page\" जैसा पथ या पूर्ण URL प्रयास करें।',\n ar: 'رابط غير صالح. جرب مسارًا مثل \"/page\" أو رابطًا كاملاً.',\n ru: 'Неверный URL. Попробуйте путь вроде \"/page\" или полный URL.',\n tr: 'Geçersiz URL. \"/page\" gibi bir yol veya tam URL deneyin.',\n pl: 'Nieprawidłowy adres URL. Wypróbuj ścieżkę jak \"/page\" lub pełny URL.',\n id: 'URL tidak valid. Coba jalur seperti \"/page\" atau URL lengkap.',\n vi: 'URL không hợp lệ. Thử đường dẫn như \"/page\" hoặc URL đầy đủ.',\n uk: 'Недійсний URL. Спробуйте шлях на кшталт \"/page\" або повну URL-адресу.',\n }),\n domainRestrictionError: t({\n en: 'URL must match the application domain.',\n 'en-GB': 'URL must match the application domain.',\n fr: \"L'URL doit correspondre au domaine de l'application.\",\n es: 'La URL debe coincidir con el dominio de la aplicación.',\n de: 'Die URL muss mit der Anwendungsdomäne übereinstimmen.',\n ja: 'URLはアプリケーションドメインと一致する必要があります。',\n ko: 'URL은 애플리케이션 도메인과 일치해야 합니다.',\n zh: 'URL必须与应用程序域名匹配。',\n it: \"L'URL deve corrispondere al dominio dell'applicazione.\",\n pt: 'A URL deve corresponder ao domínio da aplicação.',\n hi: 'URL एप्लिकेशन डोमेन से मेल खाना चाहिए।',\n ar: 'يجب أن يتطابق الرابط مع نطاق التطبيق.',\n ru: 'URL должен соответствовать домену приложения.',\n tr: 'URL, uygulama etki alanıyla eşleşmelidir.',\n pl: 'Adres URL musi pasować do domeny aplikacji.',\n id: 'URL harus cocok dengan domain aplikasi.',\n vi: 'URL phải khớp với tên miền ứng dụng.',\n uk: 'URL має відповідати домену застосунку.',\n }),\n backButtonLabel: t({\n en: 'Go back',\n 'en-GB': 'Go back',\n fr: 'Retour',\n es: 'Atrás',\n de: 'Zurück',\n ja: '戻る',\n ko: '뒤로 가기',\n zh: '后退',\n it: 'Indietro',\n pt: 'Voltar',\n hi: 'वापस जाएं',\n ar: 'رجوع',\n ru: 'Назад',\n tr: 'Geri',\n pl: 'Wstecz',\n id: 'Kembali',\n vi: 'Quay lại',\n uk: 'Назад',\n }),\n forwardButtonLabel: t({\n en: 'Go forward',\n 'en-GB': 'Go forward',\n fr: 'Avancer',\n es: 'Adelante',\n de: 'Vorwärts',\n ja: '進む',\n ko: '앞으로 가기',\n zh: '前进',\n it: 'Avanti',\n pt: 'Avançar',\n hi: 'आगे जाएं',\n ar: 'إلى الأمام',\n ru: 'Вперёд',\n tr: 'İleri',\n pl: 'Naprzód',\n id: 'Maju',\n vi: 'Tiến tới',\n uk: 'Вперед',\n }),\n iframeTitle: t({\n en: 'Embedded web page',\n 'en-GB': 'Embedded web page',\n fr: 'Page web intégrée',\n es: 'Página web integrada',\n de: 'Eingebettete Webseite',\n ja: '埋め込みウェブページ',\n ko: '임베디드 웹페이지',\n zh: '嵌入式网页',\n it: 'Pagina web incorporata',\n pt: 'Página web incorporada',\n hi: 'एम्बेडेड वेब पेज',\n ar: 'صفحة ويب مدمجة',\n ru: 'Встроенная веб-страница',\n tr: 'Gömülü web sayfası',\n pl: 'Osadzona strona internetowa',\n id: 'Halaman web tertanam',\n vi: 'Trang web được nhúng',\n uk: 'Вбудована веб-сторінка',\n }),\n },\n title: 'Embedded browser component',\n description:\n 'Content declaration for the embedded browser UI component, including ARIA labels, URL placeholder, and error messages for displaying web pages within an iframe.',\n tags: ['UI component', 'embedded browser', 'iframe'],\n} satisfies Dictionary;\n\nexport default browserContent;\n"],"mappings":";;;AAEA,MAAa,iBAAiB;CAC5B,KAAK;CACL,SAAS;EACP,WAAW,EAAE;GACX,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;EACD,UAAU,EAAE;GACV,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;EACD,gBAAgB;EAChB,cAAc,EAAE;GACd,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;EACD,wBAAwB,EAAE;GACxB,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;EACD,iBAAiB,EAAE;GACjB,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;EACD,oBAAoB,EAAE;GACpB,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;EACD,aAAa,EAAE;GACb,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH;CACA,OAAO;CACP,aACE;CACF,MAAM;EAAC;EAAgB;EAAoB;CAAQ;AACrD"}
|
|
1
|
+
{"version":3,"file":"Browser.content.mjs","names":[],"sources":["../../../../src/components/Browser/Browser.content.ts"],"sourcesContent":["import { type Dictionary, t } from 'intlayer';\n\nexport const browserContent = {\n key: 'browser',\n content: {\n ariaLabel: t({\n en: 'Embedded browser',\n 'en-GB': 'Embedded browser',\n fr: 'Navigateur intégré',\n es: 'Navegador integrado',\n de: 'Eingebetteter Browser',\n ja: '埋め込みブラウザ',\n ko: '임베디드 브라우저',\n zh: '嵌入式浏览器',\n it: 'Browser incorporato',\n pt: 'Navegador incorporado',\n hi: 'एम्बेडेड ब्राउज़र',\n ar: 'متصفح مدمج',\n ru: 'Встроенный браузер',\n tr: 'Gömülü tarayıcı',\n pl: 'Osadzona przeglądarka',\n id: 'Peramban tertanam',\n vi: 'Trình duyệt nhúng',\n uk: 'Вбудований браузер',\n }),\n urlLabel: t({\n en: 'URL address bar',\n 'en-GB': 'URL address bar',\n fr: \"Barre d'adresse URL\",\n es: 'Barra de direcciones URL',\n de: 'URL-Adressleiste',\n ja: 'URLアドレスバー',\n ko: 'URL 주소 표시줄',\n zh: 'URL地址栏',\n it: 'Barra degli indirizzi URL',\n pt: 'Barra de endereços URL',\n hi: 'URL पता बार',\n ar: 'شريط عنوان URL',\n ru: 'Адресная строка URL',\n tr: 'URL adres çubuğu',\n pl: 'Pasek adresu URL',\n id: 'Bilah alamat URL',\n vi: 'Thanh địa chỉ URL',\n uk: 'Рядок адреси URL',\n }),\n urlPlaceholder: 'https://example.com',\n errorMessage: t({\n en: 'Invalid URL. Try a path like \"/page\" or a full URL.',\n 'en-GB': 'Invalid URL. Try a path like \"/page\" or a full URL.',\n fr: 'URL invalide. Essayez un chemin comme \"/page\" ou une URL complète.',\n es: 'URL no válida. Intente una ruta como \"/page\" o una URL completa.',\n de: 'Ungültige URL. Versuchen Sie einen Pfad wie \"/page\" oder eine vollständige URL.',\n ja: '無効なURL。\"/page\"のようなパスまたは完全なURLを試してください。',\n ko: '잘못된 URL입니다. \"/page\"와 같은 경로 또는 전체 URL을 시도하십시오.',\n zh: '无效的URL。尝试像\"/page\"这样的路径或完整的URL。',\n it: 'URL non valido. Prova un percorso come \"/page\" o un URL completo.',\n pt: 'URL inválido. Tente um caminho como \"/page\" ou uma URL completa.',\n hi: 'अमान्य URL। \"/page\" जैसा पथ या पूर्ण URL प्रयास करें।',\n ar: 'رابط غير صالح. جرب مسارًا مثل \"/page\" أو رابطًا كاملاً.',\n ru: 'Неверный URL. Попробуйте путь вроде \"/page\" или полный URL.',\n tr: 'Geçersiz URL. \"/page\" gibi bir yol veya tam URL deneyin.',\n pl: 'Nieprawidłowy adres URL. Wypróbuj ścieżkę jak \"/page\" lub pełny URL.',\n id: 'URL tidak valid. Coba jalur seperti \"/page\" atau URL lengkap.',\n vi: 'URL không hợp lệ. Thử đường dẫn như \"/page\" hoặc URL đầy đủ.',\n uk: 'Недійсний URL. Спробуйте шлях на кшталт \"/page\" або повну URL-адресу.',\n }),\n domainRestrictionError: t({\n en: 'URL must match the application domain.',\n 'en-GB': 'URL must match the application domain.',\n fr: \"L'URL doit correspondre au domaine de l'application.\",\n es: 'La URL debe coincidir con el dominio de la aplicación.',\n de: 'Die URL muss mit der Anwendungsdomäne übereinstimmen.',\n ja: 'URLはアプリケーションドメインと一致する必要があります。',\n ko: 'URL은 애플리케이션 도메인과 일치해야 합니다.',\n zh: 'URL必须与应用程序域名匹配。',\n it: \"L'URL deve corrispondere al dominio dell'applicazione.\",\n pt: 'A URL deve corresponder ao domínio da aplicação.',\n hi: 'URL एप्लिकेशन डोमेन से मेल खाना चाहिए।',\n ar: 'يجب أن يتطابق الرابط مع نطاق التطبيق.',\n ru: 'URL должен соответствовать домену приложения.',\n tr: 'URL, uygulama etki alanıyla eşleşmelidir.',\n pl: 'Adres URL musi pasować do domeny aplikacji.',\n id: 'URL harus cocok dengan domain aplikasi.',\n vi: 'URL phải khớp với tên miền ứng dụng.',\n uk: 'URL має відповідати домену застосунку.',\n }),\n backButtonLabel: t({\n en: 'Go back',\n 'en-GB': 'Go back',\n fr: 'Retour',\n es: 'Atrás',\n de: 'Zurück',\n ja: '戻る',\n ko: '뒤로 가기',\n zh: '后退',\n it: 'Indietro',\n pt: 'Voltar',\n hi: 'वापस जाएं',\n ar: 'رجوع',\n ru: 'Назад',\n tr: 'Geri',\n pl: 'Wstecz',\n id: 'Kembali',\n vi: 'Quay lại',\n uk: 'Назад',\n }),\n forwardButtonLabel: t({\n en: 'Go forward',\n 'en-GB': 'Go forward',\n fr: 'Avancer',\n es: 'Adelante',\n de: 'Vorwärts',\n ja: '進む',\n ko: '앞으로 가기',\n zh: '前进',\n it: 'Avanti',\n pt: 'Avançar',\n hi: 'आगे जाएं',\n ar: 'إلى الأمام',\n ru: 'Вперёд',\n tr: 'İleri',\n pl: 'Naprzód',\n id: 'Maju',\n vi: 'Tiến tới',\n uk: 'Вперед',\n }),\n iframeTitle: t({\n en: 'Embedded web page',\n 'en-GB': 'Embedded web page',\n fr: 'Page web intégrée',\n es: 'Página web integrada',\n de: 'Eingebettete Webseite',\n ja: '埋め込みウェブページ',\n ko: '임베디드 웹페이지',\n zh: '嵌入式网页',\n it: 'Pagina web incorporata',\n pt: 'Página web incorporada',\n hi: 'एम्बेडेड वेब पेज',\n ar: 'صفحة ويب مدمجة',\n ru: 'Встроенная веб-страница',\n tr: 'Gömülü web sayfası',\n pl: 'Osadzona strona internetowa',\n id: 'Halaman web tertanam',\n vi: 'Trang web được nhúng',\n uk: 'Вбудована веб-сторінка',\n }),\n sitemapButtonLabel: t({\n en: 'Scan sitemap',\n 'en-GB': 'Scan sitemap',\n fr: 'Scanner le sitemap',\n es: 'Escanear sitemap',\n de: 'Sitemap scannen',\n ja: 'サイトマップをスキャン',\n ko: '사이트맵 스캔',\n zh: '扫描网站地图',\n it: 'Scansiona sitemap',\n pt: 'Escanear sitemap',\n hi: 'साइटमैप स्कैन करें',\n ar: 'مسح خريطة الموقع',\n ru: 'Сканировать карту сайта',\n tr: 'Site haritasını tara',\n pl: 'Skanuj mapę witryny',\n id: 'Pindai sitemap',\n vi: 'Quét sitemap',\n uk: 'Сканувати карту сайту',\n }),\n sitemapSearchPlaceholder: t({\n en: 'Filter URLs…',\n 'en-GB': 'Filter URLs…',\n fr: 'Filtrer les URLs…',\n es: 'Filtrar URLs…',\n de: 'URLs filtern…',\n ja: 'URLを絞り込む…',\n ko: 'URL 필터링…',\n zh: '过滤URL…',\n it: 'Filtra URL…',\n pt: 'Filtrar URLs…',\n hi: 'URL फ़िल्टर करें…',\n ar: 'تصفية الروابط…',\n ru: 'Фильтр URL…',\n tr: 'URL filtrele…',\n pl: 'Filtruj URL…',\n id: 'Filter URL…',\n vi: 'Lọc URL…',\n uk: 'Фільтр URL…',\n }),\n sitemapSearchAriaLabel: t({\n en: 'Search sitemap URLs',\n 'en-GB': 'Search sitemap URLs',\n fr: 'Rechercher les URLs du sitemap',\n es: 'Buscar URLs del sitemap',\n de: 'Sitemap-URLs durchsuchen',\n ja: 'サイトマップURLを検索',\n ko: '사이트맵 URL 검색',\n zh: '搜索网站地图URL',\n it: 'Cerca URL del sitemap',\n pt: 'Pesquisar URLs do sitemap',\n hi: 'साइटमैप URL खोजें',\n ar: 'ابحث في روابط خريطة الموقع',\n ru: 'Поиск URL карты сайта',\n tr: 'Site haritası URL ara',\n pl: 'Szukaj URL mapy witryny',\n id: 'Cari URL sitemap',\n vi: 'Tìm kiếm URL sitemap',\n uk: 'Пошук URL карти сайту',\n }),\n sitemapLoading: t({\n en: 'Scanning sitemap…',\n 'en-GB': 'Scanning sitemap…',\n fr: 'Scan du sitemap en cours…',\n es: 'Escaneando sitemap…',\n de: 'Sitemap wird gescannt…',\n ja: 'サイトマップをスキャン中…',\n ko: '사이트맵 스캔 중…',\n zh: '正在扫描网站地图…',\n it: 'Scansione sitemap in corso…',\n pt: 'Escaneando sitemap…',\n hi: 'साइटमैप स्कैन हो रहा है…',\n ar: 'جارٍ مسح خريطة الموقع…',\n ru: 'Сканирование карты сайта…',\n tr: 'Site haritası taranıyor…',\n pl: 'Skanowanie mapy witryny…',\n id: 'Memindai sitemap…',\n vi: 'Đang quét sitemap…',\n uk: 'Сканування карти сайту…',\n }),\n sitemapEmpty: t({\n en: 'No URLs found',\n 'en-GB': 'No URLs found',\n fr: 'Aucune URL trouvée',\n es: 'No se encontraron URLs',\n de: 'Keine URLs gefunden',\n ja: 'URLが見つかりません',\n ko: 'URL을 찾을 수 없습니다',\n zh: '未找到URL',\n it: 'Nessun URL trovato',\n pt: 'Nenhuma URL encontrada',\n hi: 'कोई URL नहीं मिला',\n ar: 'لم يتم العثور على روابط',\n ru: 'URL не найдены',\n tr: 'URL bulunamadı',\n pl: 'Nie znaleziono URL',\n id: 'Tidak ada URL yang ditemukan',\n vi: 'Không tìm thấy URL',\n uk: 'URL не знайдено',\n }),\n sitemapError: t({\n en: 'Could not load sitemap',\n 'en-GB': 'Could not load sitemap',\n fr: 'Impossible de charger le sitemap',\n es: 'No se pudo cargar el sitemap',\n de: 'Sitemap konnte nicht geladen werden',\n ja: 'サイトマップを読み込めませんでした',\n ko: '사이트맵을 불러올 수 없습니다',\n zh: '无法加载网站地图',\n it: 'Impossibile caricare il sitemap',\n pt: 'Não foi possível carregar o sitemap',\n hi: 'साइटमैप लोड नहीं हो सका',\n ar: 'تعذّر تحميل خريطة الموقع',\n ru: 'Не удалось загрузить карту сайта',\n tr: 'Site haritası yüklenemedi',\n pl: 'Nie można załadować mapy witryny',\n id: 'Tidak dapat memuat sitemap',\n vi: 'Không thể tải sitemap',\n uk: 'Не вдалося завантажити карту сайту',\n }),\n },\n title: 'Embedded browser component',\n description:\n 'Content declaration for the embedded browser UI component, including ARIA labels, URL placeholder, and error messages for displaying web pages within an iframe.',\n tags: ['UI component', 'embedded browser', 'iframe'],\n} satisfies Dictionary;\n\nexport default browserContent;\n"],"mappings":"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"}
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { cn } from "../../utils/cn.mjs";
|
|
4
|
+
import { Container } from "../Container/index.mjs";
|
|
4
5
|
import { Button } from "../Button/Button.mjs";
|
|
6
|
+
import { ClickOutsideDiv } from "../ClickOutsideDiv/index.mjs";
|
|
7
|
+
import { DropDown } from "../DropDown/index.mjs";
|
|
5
8
|
import { Input, inputVariants } from "../Input/Input.mjs";
|
|
6
|
-
import { useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
7
|
-
import { ArrowLeft, ArrowRight, RotateCw } from "lucide-react";
|
|
9
|
+
import { useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
10
|
+
import { ArrowLeft, ArrowRight, RotateCw, ScanSearch } from "lucide-react";
|
|
8
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
12
|
import { useIntlayer } from "react-intlayer";
|
|
10
13
|
|
|
11
14
|
//#region src/components/Browser/Browser.tsx
|
|
15
|
+
const getUrlPath = (url) => {
|
|
16
|
+
try {
|
|
17
|
+
const { pathname, search, hash } = new URL(url);
|
|
18
|
+
return `${pathname}${search}${hash}` || "/";
|
|
19
|
+
} catch {
|
|
20
|
+
return url;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
12
23
|
const Browser = ({ initialUrl = "https://example.com", path, className, style, size = "md", "aria-label": ariaLabel, sandbox = "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads", ref, domainRestriction, ...props }) => {
|
|
13
24
|
const [inputUrl, setInputUrl] = useState(initialUrl);
|
|
14
25
|
const [currentUrl, setCurrentUrl] = useState(initialUrl);
|
|
@@ -17,6 +28,13 @@ const Browser = ({ initialUrl = "https://example.com", path, className, style, s
|
|
|
17
28
|
const [error, setError] = useState(null);
|
|
18
29
|
const [submitted, setSubmitted] = useState(false);
|
|
19
30
|
const internalIframeRef = useRef(null);
|
|
31
|
+
const [sitemapOpen, setSitemapOpen] = useState(false);
|
|
32
|
+
const [sitemapUrls, setSitemapUrls] = useState([]);
|
|
33
|
+
const [sitemapSearch, setSitemapSearch] = useState("");
|
|
34
|
+
const [sitemapLoading, setSitemapLoading] = useState(false);
|
|
35
|
+
const [sitemapFetched, setSitemapFetched] = useState(false);
|
|
36
|
+
const [sitemapError, setSitemapError] = useState(false);
|
|
37
|
+
const sitemapInputRef = useRef(null);
|
|
20
38
|
useImperativeHandle(ref, () => internalIframeRef.current, []);
|
|
21
39
|
const content = useIntlayer("browser");
|
|
22
40
|
useEffect(() => {
|
|
@@ -26,6 +44,11 @@ const Browser = ({ initialUrl = "https://example.com", path, className, style, s
|
|
|
26
44
|
setCurrentIndex(0);
|
|
27
45
|
setError(null);
|
|
28
46
|
setSubmitted(false);
|
|
47
|
+
setSitemapOpen(false);
|
|
48
|
+
setSitemapUrls([]);
|
|
49
|
+
setSitemapSearch("");
|
|
50
|
+
setSitemapFetched(false);
|
|
51
|
+
setSitemapError(false);
|
|
29
52
|
}, [initialUrl]);
|
|
30
53
|
useEffect(() => {
|
|
31
54
|
if (!path) return;
|
|
@@ -53,6 +76,11 @@ const Browser = ({ initialUrl = "https://example.com", path, className, style, s
|
|
|
53
76
|
domainRestriction,
|
|
54
77
|
initialUrl
|
|
55
78
|
]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const iframe = internalIframeRef.current;
|
|
81
|
+
if (!iframe) return;
|
|
82
|
+
if (iframe.src !== currentUrl) iframe.src = currentUrl;
|
|
83
|
+
}, [currentUrl]);
|
|
56
84
|
const handleNavigateTo = (url) => {
|
|
57
85
|
try {
|
|
58
86
|
const validated = normalizeUrl(url);
|
|
@@ -98,13 +126,13 @@ const Browser = ({ initialUrl = "https://example.com", path, className, style, s
|
|
|
98
126
|
handleNavigateTo(inputUrl);
|
|
99
127
|
};
|
|
100
128
|
const handleReload = () => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
129
|
+
const iframe = internalIframeRef.current;
|
|
130
|
+
if (!iframe) return;
|
|
131
|
+
const src = iframe.src;
|
|
132
|
+
iframe.src = "";
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
if (internalIframeRef.current) internalIframeRef.current.src = src;
|
|
135
|
+
}, 50);
|
|
108
136
|
};
|
|
109
137
|
const isValidHostname = (host) => {
|
|
110
138
|
if (host === "localhost") return true;
|
|
@@ -141,6 +169,31 @@ const Browser = ({ initialUrl = "https://example.com", path, className, style, s
|
|
|
141
169
|
}
|
|
142
170
|
return url.toString();
|
|
143
171
|
};
|
|
172
|
+
const handleSitemapToggle = async () => {
|
|
173
|
+
const nextOpen = !sitemapOpen;
|
|
174
|
+
setSitemapOpen(nextOpen);
|
|
175
|
+
if (nextOpen && !sitemapFetched) {
|
|
176
|
+
setSitemapLoading(true);
|
|
177
|
+
setSitemapError(false);
|
|
178
|
+
setSitemapFetched(true);
|
|
179
|
+
try {
|
|
180
|
+
const { extractUrlFromSitemap } = await import("./extractUrlFromSitemap.mjs");
|
|
181
|
+
const urls = await extractUrlFromSitemap(currentUrl);
|
|
182
|
+
setSitemapUrls(urls);
|
|
183
|
+
if (urls.length === 0) setSitemapError(false);
|
|
184
|
+
} catch {
|
|
185
|
+
setSitemapError(true);
|
|
186
|
+
} finally {
|
|
187
|
+
setSitemapLoading(false);
|
|
188
|
+
}
|
|
189
|
+
setTimeout(() => sitemapInputRef.current?.focus(), 50);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const filteredSitemapUrls = useMemo(() => {
|
|
193
|
+
const query = sitemapSearch.trim().toLowerCase();
|
|
194
|
+
if (!query) return sitemapUrls;
|
|
195
|
+
return sitemapUrls.filter((url) => url.toLowerCase().includes(query));
|
|
196
|
+
}, [sitemapUrls, sitemapSearch]);
|
|
144
197
|
const showError = submitted && !!error;
|
|
145
198
|
const canGoBack = currentIndex > 0;
|
|
146
199
|
const canGoForward = currentIndex < history.length - 1;
|
|
@@ -217,6 +270,75 @@ const Browser = ({ initialUrl = "https://example.com", path, className, style, s
|
|
|
217
270
|
})
|
|
218
271
|
]
|
|
219
272
|
}),
|
|
273
|
+
/* @__PURE__ */ jsx(ClickOutsideDiv, {
|
|
274
|
+
onClickOutSide: () => setSitemapOpen(false),
|
|
275
|
+
disabled: !sitemapOpen,
|
|
276
|
+
role: "none",
|
|
277
|
+
children: /* @__PURE__ */ jsxs(DropDown, {
|
|
278
|
+
identifier: "sitemap-explorer",
|
|
279
|
+
children: [/* @__PURE__ */ jsx(DropDown.Trigger, {
|
|
280
|
+
identifier: "sitemap-explorer-trigger",
|
|
281
|
+
type: "button",
|
|
282
|
+
color: "text",
|
|
283
|
+
onClick: handleSitemapToggle,
|
|
284
|
+
variant: "hoverable",
|
|
285
|
+
size: "icon-md",
|
|
286
|
+
label: content.sitemapButtonLabel.value,
|
|
287
|
+
Icon: ScanSearch
|
|
288
|
+
}), /* @__PURE__ */ jsx(DropDown.Panel, {
|
|
289
|
+
identifier: "sitemap-explorer",
|
|
290
|
+
isHidden: !sitemapOpen,
|
|
291
|
+
align: "end",
|
|
292
|
+
isFocusable: true,
|
|
293
|
+
isOverable: true,
|
|
294
|
+
children: /* @__PURE__ */ jsxs(Container, {
|
|
295
|
+
className: "min-w-28 rounded-md!",
|
|
296
|
+
roundedSize: "sm",
|
|
297
|
+
border: true,
|
|
298
|
+
borderColor: "neutral",
|
|
299
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
300
|
+
className: "p-2",
|
|
301
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
302
|
+
type: "search",
|
|
303
|
+
ref: sitemapInputRef,
|
|
304
|
+
"aria-label": content.sitemapSearchAriaLabel.value,
|
|
305
|
+
placeholder: content.sitemapSearchPlaceholder.value,
|
|
306
|
+
onChange: (e) => setSitemapSearch(e.target.value),
|
|
307
|
+
value: sitemapSearch,
|
|
308
|
+
size: "sm"
|
|
309
|
+
})
|
|
310
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
311
|
+
className: "max-h-64 divide-y divide-dotted divide-neutral/30 overflow-y-auto p-1 text-center",
|
|
312
|
+
"aria-label": content.sitemapButtonLabel.value,
|
|
313
|
+
children: sitemapLoading ? /* @__PURE__ */ jsx("li", {
|
|
314
|
+
className: "px-3 py-4 text-center text-neutral text-xs",
|
|
315
|
+
children: content.sitemapLoading.value
|
|
316
|
+
}) : sitemapError || !sitemapLoading && filteredSitemapUrls.length === 0 ? /* @__PURE__ */ jsx("li", {
|
|
317
|
+
className: "px-3 py-4 text-center text-neutral text-xs",
|
|
318
|
+
children: sitemapError ? content.sitemapError.value : content.sitemapEmpty.value
|
|
319
|
+
}) : filteredSitemapUrls.map((url) => /* @__PURE__ */ jsx("li", {
|
|
320
|
+
className: "py-0.5",
|
|
321
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
322
|
+
variant: "hoverable",
|
|
323
|
+
color: "text",
|
|
324
|
+
size: "sm",
|
|
325
|
+
className: "w-full text-left",
|
|
326
|
+
label: url,
|
|
327
|
+
onClick: () => {
|
|
328
|
+
handleNavigateTo(url);
|
|
329
|
+
setSitemapOpen(false);
|
|
330
|
+
},
|
|
331
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
332
|
+
className: "max-w-64 truncate text-left text-base",
|
|
333
|
+
children: getUrlPath(url)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
}, url))
|
|
337
|
+
})]
|
|
338
|
+
})
|
|
339
|
+
})]
|
|
340
|
+
})
|
|
341
|
+
}),
|
|
220
342
|
showError && /* @__PURE__ */ jsx("div", {
|
|
221
343
|
className: "absolute top-full left-4 z-20 mt-1",
|
|
222
344
|
children: /* @__PURE__ */ jsx("p", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Browser.mjs","names":[],"sources":["../../../../src/components/Browser/Browser.tsx"],"sourcesContent":["'use client';\n\nimport { cn } from '@utils/cn';\nimport { ArrowLeft, ArrowRight, RotateCw } from 'lucide-react';\nimport {\n type CSSProperties,\n type HTMLAttributes,\n type RefObject,\n type SubmitEvent,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport { useIntlayer } from 'react-intlayer';\nimport { Button } from '../Button';\nimport { Input, inputVariants } from '../Input';\n\nexport type BrowserProps = {\n initialUrl?: string;\n path?: string;\n className?: string;\n style?: CSSProperties;\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n 'aria-label'?: string;\n sandbox?: string;\n ref?: RefObject<HTMLIFrameElement | null>;\n domainRestriction?: string;\n} & HTMLAttributes<HTMLIFrameElement>;\n\nexport const Browser = ({\n initialUrl = 'https://example.com',\n path,\n className,\n style,\n size = 'md',\n 'aria-label': ariaLabel,\n sandbox = 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads',\n ref,\n domainRestriction,\n ...props\n}: BrowserProps) => {\n // --- State -----------------------------------------------------------------\n const [inputUrl, setInputUrl] = useState(initialUrl);\n const [currentUrl, setCurrentUrl] = useState(initialUrl);\n\n // History Management\n const [history, setHistory] = useState<string[]>([initialUrl]);\n const [currentIndex, setCurrentIndex] = useState(0);\n\n const [error, setError] = useState<string | null>(null);\n const [submitted, setSubmitted] = useState(false);\n const internalIframeRef = useRef<HTMLIFrameElement>(null);\n\n useImperativeHandle(ref, () => internalIframeRef.current!, []);\n const content = useIntlayer('browser');\n\n // --- Effects ---------------------------------------------------------------\n\n // Reset everything if initialUrl changes completely\n useEffect(() => {\n setInputUrl(initialUrl);\n setCurrentUrl(initialUrl);\n setHistory([initialUrl]);\n setCurrentIndex(0);\n setError(null);\n setSubmitted(false);\n }, [initialUrl]);\n\n // Sync external path changes with the URL bar and History\n useEffect(() => {\n if (!path) return;\n\n try {\n const baseOrigin = domainRestriction ?? initialUrl;\n const origin = new URL(baseOrigin).origin;\n const fullUrl = `${origin}${path}`;\n\n // Update Input (Always update the visual bar)\n setInputUrl(fullUrl);\n\n // Check internal iframe state to avoid reload if already there\n let isAlreadyAtUrl = false;\n if (internalIframeRef.current?.contentWindow) {\n try {\n const currentIframeHref =\n internalIframeRef.current.contentWindow.location.href;\n if (new URL(currentIframeHref).href === new URL(fullUrl).href) {\n isAlreadyAtUrl = true;\n }\n } catch {\n // Cross-origin access ignored\n }\n }\n\n // Update History Stack\n // We perform this check regardless of `isAlreadyAtUrl`.\n // If the path changed (even internally), we want to record it in the arrow stack.\n if (history[currentIndex] !== fullUrl) {\n setHistory((prev) => {\n const newHistory = prev.slice(0, currentIndex + 1);\n newHistory.push(fullUrl);\n return newHistory;\n });\n setCurrentIndex((prev) => prev + 1);\n }\n\n // Navigate (Update src) only if NOT already there\n // This prevents the iframe from refreshing when the user navigated inside it.\n if (!isAlreadyAtUrl) {\n setCurrentUrl(fullUrl);\n }\n\n setError(null);\n } catch {\n // Ignore invalid paths\n }\n }, [path, domainRestriction, initialUrl]); // Removed currentIndex dependency to prevent loops\n\n // --- Navigation Logic ------------------------------------------------------\n\n const handleNavigateTo = (url: string) => {\n try {\n const validated = normalizeUrl(url);\n\n // If we are navigating to the exact same URL, just reload\n if (validated === currentUrl) {\n handleReload();\n return;\n }\n\n setCurrentUrl(validated);\n setInputUrl(validated);\n setError(null);\n\n // Update History: Slice future if we went back, then push new\n const newHistory = history.slice(0, currentIndex + 1);\n newHistory.push(validated);\n setHistory(newHistory);\n setCurrentIndex(newHistory.length - 1);\n } catch (e) {\n if (\n e instanceof Error &&\n e.message === 'URL does not match allowed domain' &&\n domainRestriction\n ) {\n setError(\n content.domainRestrictionError?.value ??\n `Only URLs from ${domainRestriction} are allowed.`\n );\n } else {\n setError(content.errorMessage.value);\n }\n }\n };\n\n const handleBack = () => {\n if (currentIndex > 0) {\n const newIndex = currentIndex - 1;\n const prevUrl = history[newIndex];\n setCurrentIndex(newIndex);\n setCurrentUrl(prevUrl);\n setInputUrl(prevUrl);\n setError(null);\n }\n };\n\n const handleForward = () => {\n if (currentIndex < history.length - 1) {\n const newIndex = currentIndex + 1;\n const nextUrl = history[newIndex];\n setCurrentIndex(newIndex);\n setCurrentUrl(nextUrl);\n setInputUrl(nextUrl);\n setError(null);\n }\n };\n\n const handleSubmit = (e: SubmitEvent<HTMLFormElement>) => {\n e.preventDefault();\n setSubmitted(true);\n handleNavigateTo(inputUrl);\n };\n\n const handleReload = () => {\n if (internalIframeRef.current) {\n // Create a clean reload effect\n const src = internalIframeRef.current.src;\n internalIframeRef.current.src = '';\n setTimeout(() => {\n if (internalIframeRef.current) internalIframeRef.current.src = src;\n }, 50);\n }\n };\n\n // --- Validation Helpers ----------------------------------------------------\n const isValidHostname = (host: string) => {\n if (host === 'localhost') return true;\n if (/^(\\d{1,3}\\.){3}\\d{1,3}$/.test(host)) return true;\n if (/^[a-f0-9:]+$/i.test(host)) return true;\n if (!/^[a-z0-9.-]+$/i.test(host)) return false;\n if (/^[-.]/.test(host) || /[-.]$/.test(host)) return false;\n if (host.includes('..')) return false;\n if (!host.includes('.')) return false;\n return true;\n };\n\n const getRestrictionOrigin = (): URL | null => {\n if (!domainRestriction) return null;\n try {\n return new URL(domainRestriction);\n } catch {\n return null;\n }\n };\n\n const normalizeUrl = (raw: string) => {\n const trimmed = raw.trim();\n if (!trimmed || /\\s/.test(trimmed)) throw new Error('Invalid');\n\n const restrictionOrigin = getRestrictionOrigin();\n const isRelativePath = trimmed.startsWith('/') && !trimmed.startsWith('//');\n\n if (isRelativePath) {\n if (restrictionOrigin) {\n return new URL(`${restrictionOrigin.origin}${trimmed}`).toString();\n }\n return new URL(`${new URL(currentUrl).origin}${trimmed}`).toString();\n }\n\n const hasProtocol = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed);\n const candidate = hasProtocol ? trimmed : `https://${trimmed}`;\n const url = new URL(candidate);\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error('Only http(s) is allowed');\n }\n\n if (!isValidHostname(url.hostname)) throw new Error('Invalid host');\n\n if (restrictionOrigin) {\n const urlMatches =\n url.hostname === restrictionOrigin.hostname &&\n url.protocol === restrictionOrigin.protocol &&\n (restrictionOrigin.port === '' ||\n url.port === restrictionOrigin.port ||\n url.host === restrictionOrigin.host);\n\n if (!urlMatches) throw new Error('URL does not match allowed domain');\n }\n\n return url.toString();\n };\n\n const showError = submitted && !!error;\n const canGoBack = currentIndex > 0;\n const canGoForward = currentIndex < history.length - 1;\n\n return (\n <section\n className={cn(\n 'flex w-full flex-col overflow-hidden rounded-xl bg-background shadow-[0_4px_12px_rgba(0,0,0,0.4),0_0_1px_rgba(0,0,0,0.2)]',\n className\n )}\n style={style}\n aria-label={ariaLabel ?? content.ariaLabel.value}\n >\n {/* Top bar */}\n <div className=\"relative z-10 flex shrink-0 items-center gap-3 rounded-t-xl bg-text/15 px-4 py-2\">\n {/* Navigation Controls */}\n <div className=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n onClick={handleBack}\n disabled={!canGoBack}\n variant=\"hoverable\"\n size=\"icon-md\"\n label={content.backButtonLabel.value}\n Icon={ArrowLeft}\n />\n <Button\n type=\"button\"\n onClick={handleForward}\n disabled={!canGoForward}\n variant=\"hoverable\"\n size=\"icon-md\"\n label={content.forwardButtonLabel.value}\n Icon={ArrowRight}\n />\n </div>\n\n {/* URL Bar */}\n <form\n onSubmit={handleSubmit}\n noValidate\n className={cn(\n inputVariants(),\n 'flex w-full gap-2 rounded-xl p-0.5! supports-[corner-shape:squircle]:rounded-2xl',\n 'bg-neutral/10 text-text/50 placeholder:text-neutral/80'\n )}\n >\n <label htmlFor=\"browser-url\" className=\"sr-only\">\n {content.urlLabel.value}\n </label>\n <Input\n id=\"browser-url\"\n type=\"text\"\n inputMode=\"url\"\n spellCheck={false}\n autoCapitalize=\"off\"\n variant=\"invisible\"\n className=\"ml-3 p-0!\"\n size=\"sm\"\n autoCorrect=\"off\"\n value={inputUrl}\n onChange={(e) => {\n setInputUrl(e.target.value);\n if (showError) setError(null);\n }}\n placeholder={content.urlPlaceholder.value}\n aria-label={content.urlLabel.value}\n aria-invalid={showError}\n aria-describedby={showError ? 'browser-url-error' : undefined}\n />\n\n <Button\n type=\"button\"\n onClick={handleReload}\n variant=\"hoverable\"\n size=\"icon-md\"\n className=\"p-1!\"\n label={'content.reloadButtonTitle.value'}\n Icon={RotateCw}\n />\n\n {/* invisible submit */}\n <button type=\"submit\" className=\"sr-only absolute\" tabIndex={-1} />\n </form>\n\n {/* Error Message Tooltip */}\n {showError && (\n <div className=\"absolute top-full left-4 z-20 mt-1\">\n <p\n id=\"browser-url-error\"\n role=\"alert\"\n aria-live=\"assertive\"\n className=\"rounded-md bg-red-900/90 px-3 py-1.5 text-red-100 text-xs shadow-md backdrop-blur-sm\"\n >\n {error}\n </p>\n </div>\n )}\n </div>\n\n {/* Iframe */}\n <div className=\"relative z-0 flex min-h-0 w-full flex-1 flex-col overflow-hidden rounded-b-xl bg-background\">\n <iframe\n ref={internalIframeRef}\n src={currentUrl}\n title={content.iframeTitle.value}\n className=\"size-full flex-1\"\n sandbox={sandbox}\n loading=\"lazy\"\n aria-live=\"polite\"\n {...props}\n />\n </div>\n </section>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Browser.mjs","names":[],"sources":["../../../../src/components/Browser/Browser.tsx"],"sourcesContent":["'use client';\n\nimport { Container } from '@components/Container';\nimport { cn } from '@utils/cn';\nimport {\n ArrowLeft,\n ArrowRight,\n Globe,\n List,\n RotateCw,\n ScanSearch,\n} from 'lucide-react';\nimport {\n type CSSProperties,\n type HTMLAttributes,\n type RefObject,\n type SubmitEvent,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useIntlayer } from 'react-intlayer';\nimport { Button } from '../Button';\nimport { ClickOutsideDiv } from '../ClickOutsideDiv';\nimport { DropDown } from '../DropDown';\nimport { Input, inputVariants } from '../Input';\n\nexport type BrowserProps = {\n initialUrl?: string;\n path?: string;\n className?: string;\n style?: CSSProperties;\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n 'aria-label'?: string;\n sandbox?: string;\n ref?: RefObject<HTMLIFrameElement | null>;\n domainRestriction?: string;\n} & HTMLAttributes<HTMLIFrameElement>;\n\nconst getUrlPath = (url: string) => {\n try {\n const { pathname, search, hash } = new URL(url);\n return `${pathname}${search}${hash}` || '/';\n } catch {\n return url;\n }\n};\n\nexport const Browser = ({\n initialUrl = 'https://example.com',\n path,\n className,\n style,\n size = 'md',\n 'aria-label': ariaLabel,\n sandbox = 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads',\n ref,\n domainRestriction,\n ...props\n}: BrowserProps) => {\n // --- State -----------------------------------------------------------------\n const [inputUrl, setInputUrl] = useState(initialUrl);\n const [currentUrl, setCurrentUrl] = useState(initialUrl);\n\n // History Management\n const [history, setHistory] = useState<string[]>([initialUrl]);\n const [currentIndex, setCurrentIndex] = useState(0);\n\n const [error, setError] = useState<string | null>(null);\n const [submitted, setSubmitted] = useState(false);\n const internalIframeRef = useRef<HTMLIFrameElement>(null);\n\n // Sitemap explorer state\n const [sitemapOpen, setSitemapOpen] = useState(false);\n const [sitemapUrls, setSitemapUrls] = useState<string[]>([]);\n const [sitemapSearch, setSitemapSearch] = useState('');\n const [sitemapLoading, setSitemapLoading] = useState(false);\n const [sitemapFetched, setSitemapFetched] = useState(false);\n const [sitemapError, setSitemapError] = useState(false);\n const sitemapInputRef = useRef<HTMLInputElement>(null);\n\n useImperativeHandle(ref, () => internalIframeRef.current!, []);\n const content = useIntlayer('browser');\n\n // --- Effects ---------------------------------------------------------------\n\n // Reset everything if initialUrl changes completely\n useEffect(() => {\n setInputUrl(initialUrl);\n setCurrentUrl(initialUrl);\n setHistory([initialUrl]);\n setCurrentIndex(0);\n setError(null);\n setSubmitted(false);\n setSitemapOpen(false);\n setSitemapUrls([]);\n setSitemapSearch('');\n setSitemapFetched(false);\n setSitemapError(false);\n }, [initialUrl]);\n\n // Sync external path changes with the URL bar and History\n useEffect(() => {\n if (!path) return;\n\n try {\n const baseOrigin = domainRestriction ?? initialUrl;\n const origin = new URL(baseOrigin).origin;\n const fullUrl = `${origin}${path}`;\n\n // Update Input (Always update the visual bar)\n setInputUrl(fullUrl);\n\n // Check internal iframe state to avoid reload if already there\n let isAlreadyAtUrl = false;\n if (internalIframeRef.current?.contentWindow) {\n try {\n const currentIframeHref =\n internalIframeRef.current.contentWindow.location.href;\n if (new URL(currentIframeHref).href === new URL(fullUrl).href) {\n isAlreadyAtUrl = true;\n }\n } catch {\n // Cross-origin access ignored\n }\n }\n\n // Update History Stack regardless of isAlreadyAtUrl so arrow navigation is always correct\n if (history[currentIndex] !== fullUrl) {\n setHistory((prev) => {\n const newHistory = prev.slice(0, currentIndex + 1);\n newHistory.push(fullUrl);\n return newHistory;\n });\n setCurrentIndex((prev) => prev + 1);\n }\n\n // Navigate only if NOT already there to avoid refreshing on internal iframe navigation\n if (!isAlreadyAtUrl) {\n setCurrentUrl(fullUrl);\n }\n\n setError(null);\n } catch {\n // Ignore invalid paths\n }\n }, [path, domainRestriction, initialUrl]); // Removed currentIndex dependency to prevent loops\n\n // Imperatively keep the iframe src in sync with currentUrl.\n // React's attribute reconciliation can be unreliable for cross-origin iframes after\n // internal navigation, so this effect is the source of truth for navigation.\n useEffect(() => {\n const iframe = internalIframeRef.current;\n if (!iframe) return;\n if (iframe.src !== currentUrl) {\n iframe.src = currentUrl;\n }\n }, [currentUrl]);\n\n // --- Navigation Logic ------------------------------------------------------\n\n const handleNavigateTo = (url: string) => {\n try {\n const validated = normalizeUrl(url);\n\n // If we are navigating to the exact same URL, just reload\n if (validated === currentUrl) {\n handleReload();\n return;\n }\n\n setCurrentUrl(validated);\n setInputUrl(validated);\n setError(null);\n\n // Update History: Slice future if we went back, then push new\n const newHistory = history.slice(0, currentIndex + 1);\n newHistory.push(validated);\n setHistory(newHistory);\n setCurrentIndex(newHistory.length - 1);\n } catch (e) {\n if (\n e instanceof Error &&\n e.message === 'URL does not match allowed domain' &&\n domainRestriction\n ) {\n setError(\n content.domainRestrictionError?.value ??\n `Only URLs from ${domainRestriction} are allowed.`\n );\n } else {\n setError(content.errorMessage.value);\n }\n }\n };\n\n const handleBack = () => {\n if (currentIndex > 0) {\n const newIndex = currentIndex - 1;\n const prevUrl = history[newIndex];\n setCurrentIndex(newIndex);\n setCurrentUrl(prevUrl);\n setInputUrl(prevUrl);\n setError(null);\n }\n };\n\n const handleForward = () => {\n if (currentIndex < history.length - 1) {\n const newIndex = currentIndex + 1;\n const nextUrl = history[newIndex];\n setCurrentIndex(newIndex);\n setCurrentUrl(nextUrl);\n setInputUrl(nextUrl);\n setError(null);\n }\n };\n\n const handleSubmit = (e: SubmitEvent<HTMLFormElement>) => {\n e.preventDefault();\n setSubmitted(true);\n handleNavigateTo(inputUrl);\n };\n\n const handleReload = () => {\n const iframe = internalIframeRef.current;\n if (!iframe) return;\n const src = iframe.src;\n iframe.src = '';\n setTimeout(() => {\n if (internalIframeRef.current) internalIframeRef.current.src = src;\n }, 50);\n };\n\n // --- Validation Helpers ----------------------------------------------------\n const isValidHostname = (host: string) => {\n if (host === 'localhost') return true;\n if (/^(\\d{1,3}\\.){3}\\d{1,3}$/.test(host)) return true;\n if (/^[a-f0-9:]+$/i.test(host)) return true;\n if (!/^[a-z0-9.-]+$/i.test(host)) return false;\n if (/^[-.]/.test(host) || /[-.]$/.test(host)) return false;\n if (host.includes('..')) return false;\n if (!host.includes('.')) return false;\n return true;\n };\n\n const getRestrictionOrigin = (): URL | null => {\n if (!domainRestriction) return null;\n try {\n return new URL(domainRestriction);\n } catch {\n return null;\n }\n };\n\n const normalizeUrl = (raw: string) => {\n const trimmed = raw.trim();\n if (!trimmed || /\\s/.test(trimmed)) throw new Error('Invalid');\n\n const restrictionOrigin = getRestrictionOrigin();\n const isRelativePath = trimmed.startsWith('/') && !trimmed.startsWith('//');\n\n if (isRelativePath) {\n if (restrictionOrigin) {\n return new URL(`${restrictionOrigin.origin}${trimmed}`).toString();\n }\n return new URL(`${new URL(currentUrl).origin}${trimmed}`).toString();\n }\n\n const hasProtocol = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed);\n const candidate = hasProtocol ? trimmed : `https://${trimmed}`;\n const url = new URL(candidate);\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error('Only http(s) is allowed');\n }\n\n if (!isValidHostname(url.hostname)) throw new Error('Invalid host');\n\n if (restrictionOrigin) {\n const urlMatches =\n url.hostname === restrictionOrigin.hostname &&\n url.protocol === restrictionOrigin.protocol &&\n (restrictionOrigin.port === '' ||\n url.port === restrictionOrigin.port ||\n url.host === restrictionOrigin.host);\n\n if (!urlMatches) throw new Error('URL does not match allowed domain');\n }\n\n return url.toString();\n };\n\n const handleSitemapToggle = async () => {\n const nextOpen = !sitemapOpen;\n setSitemapOpen(nextOpen);\n\n if (nextOpen && !sitemapFetched) {\n setSitemapLoading(true);\n setSitemapError(false);\n setSitemapFetched(true);\n try {\n const { extractUrlFromSitemap } = await import(\n './extractUrlFromSitemap'\n );\n const urls = await extractUrlFromSitemap(currentUrl);\n setSitemapUrls(urls);\n if (urls.length === 0) setSitemapError(false);\n } catch {\n setSitemapError(true);\n } finally {\n setSitemapLoading(false);\n }\n setTimeout(() => sitemapInputRef.current?.focus(), 50);\n }\n };\n\n const filteredSitemapUrls = useMemo(() => {\n const query = sitemapSearch.trim().toLowerCase();\n if (!query) return sitemapUrls;\n return sitemapUrls.filter((url) => url.toLowerCase().includes(query));\n }, [sitemapUrls, sitemapSearch]);\n\n const showError = submitted && !!error;\n const canGoBack = currentIndex > 0;\n const canGoForward = currentIndex < history.length - 1;\n\n return (\n <section\n className={cn(\n 'flex w-full flex-col overflow-hidden rounded-xl bg-background shadow-[0_4px_12px_rgba(0,0,0,0.4),0_0_1px_rgba(0,0,0,0.2)]',\n className\n )}\n style={style}\n aria-label={ariaLabel ?? content.ariaLabel.value}\n >\n {/* Top bar */}\n <div className=\"relative z-10 flex shrink-0 items-center gap-3 rounded-t-xl bg-text/15 px-4 py-2\">\n {/* Navigation Controls */}\n <div className=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n onClick={handleBack}\n disabled={!canGoBack}\n variant=\"hoverable\"\n size=\"icon-md\"\n label={content.backButtonLabel.value}\n Icon={ArrowLeft}\n />\n <Button\n type=\"button\"\n onClick={handleForward}\n disabled={!canGoForward}\n variant=\"hoverable\"\n size=\"icon-md\"\n label={content.forwardButtonLabel.value}\n Icon={ArrowRight}\n />\n </div>\n\n {/* URL Bar */}\n <form\n onSubmit={handleSubmit}\n noValidate\n className={cn(\n inputVariants(),\n 'flex w-full gap-2 rounded-xl p-0.5! supports-[corner-shape:squircle]:rounded-2xl',\n 'bg-neutral/10 text-text/50 placeholder:text-neutral/80'\n )}\n >\n <label htmlFor=\"browser-url\" className=\"sr-only\">\n {content.urlLabel.value}\n </label>\n <Input\n id=\"browser-url\"\n type=\"text\"\n inputMode=\"url\"\n spellCheck={false}\n autoCapitalize=\"off\"\n variant=\"invisible\"\n className=\"ml-3 p-0!\"\n size=\"sm\"\n autoCorrect=\"off\"\n value={inputUrl}\n onChange={(e) => {\n setInputUrl(e.target.value);\n if (showError) setError(null);\n }}\n placeholder={content.urlPlaceholder.value}\n aria-label={content.urlLabel.value}\n aria-invalid={showError}\n aria-describedby={showError ? 'browser-url-error' : undefined}\n />\n\n <Button\n type=\"button\"\n onClick={handleReload}\n variant=\"hoverable\"\n size=\"icon-md\"\n className=\"p-1!\"\n label={'content.reloadButtonTitle.value'}\n Icon={RotateCw}\n />\n\n {/* invisible submit */}\n <button type=\"submit\" className=\"sr-only absolute\" tabIndex={-1} />\n </form>\n\n {/* Sitemap Explorer */}\n <ClickOutsideDiv\n onClickOutSide={() => setSitemapOpen(false)}\n disabled={!sitemapOpen}\n role=\"none\"\n >\n <DropDown identifier=\"sitemap-explorer\">\n <DropDown.Trigger\n identifier=\"sitemap-explorer-trigger\"\n type=\"button\"\n color=\"text\"\n onClick={handleSitemapToggle}\n variant=\"hoverable\"\n size=\"icon-md\"\n label={content.sitemapButtonLabel.value}\n Icon={ScanSearch}\n />\n\n <DropDown.Panel\n identifier=\"sitemap-explorer\"\n isHidden={!sitemapOpen}\n align=\"end\"\n isFocusable\n isOverable\n >\n <Container\n className=\"min-w-28 rounded-md!\"\n roundedSize=\"sm\"\n border\n borderColor=\"neutral\"\n >\n <div className=\"p-2\">\n <Input\n type=\"search\"\n ref={sitemapInputRef}\n aria-label={content.sitemapSearchAriaLabel.value}\n placeholder={content.sitemapSearchPlaceholder.value}\n onChange={(e) => setSitemapSearch(e.target.value)}\n value={sitemapSearch}\n size=\"sm\"\n />\n </div>\n <ul\n className=\"max-h-64 divide-y divide-dotted divide-neutral/30 overflow-y-auto p-1 text-center\"\n aria-label={content.sitemapButtonLabel.value}\n >\n {sitemapLoading ? (\n <li className=\"px-3 py-4 text-center text-neutral text-xs\">\n {content.sitemapLoading.value}\n </li>\n ) : sitemapError ||\n (!sitemapLoading && filteredSitemapUrls.length === 0) ? (\n <li className=\"px-3 py-4 text-center text-neutral text-xs\">\n {sitemapError\n ? content.sitemapError.value\n : content.sitemapEmpty.value}\n </li>\n ) : (\n filteredSitemapUrls.map((url) => (\n <li key={url} className=\"py-0.5\">\n <Button\n variant=\"hoverable\"\n color=\"text\"\n size=\"sm\"\n className=\"w-full text-left\"\n label={url}\n onClick={() => {\n handleNavigateTo(url);\n setSitemapOpen(false);\n }}\n >\n <span className=\"max-w-64 truncate text-left text-base\">\n {getUrlPath(url)}\n </span>\n </Button>\n </li>\n ))\n )}\n </ul>\n </Container>\n </DropDown.Panel>\n </DropDown>\n </ClickOutsideDiv>\n\n {/* Error Message Tooltip */}\n {showError && (\n <div className=\"absolute top-full left-4 z-20 mt-1\">\n <p\n id=\"browser-url-error\"\n role=\"alert\"\n aria-live=\"assertive\"\n className=\"rounded-md bg-red-900/90 px-3 py-1.5 text-red-100 text-xs shadow-md backdrop-blur-sm\"\n >\n {error}\n </p>\n </div>\n )}\n </div>\n\n {/* Iframe */}\n <div className=\"relative z-0 flex min-h-0 w-full flex-1 flex-col overflow-hidden rounded-b-xl bg-background\">\n <iframe\n ref={internalIframeRef}\n src={currentUrl}\n title={content.iframeTitle.value}\n className=\"size-full flex-1\"\n sandbox={sandbox}\n loading=\"lazy\"\n aria-live=\"polite\"\n {...props}\n />\n </div>\n </section>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/components/Browser/extractUrlFromSitemap.ts
|
|
2
|
+
const fetchWithTimeout = (url, timeout = 5e3) => {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
5
|
+
return fetch(url, { signal: controller.signal }).finally(() => clearTimeout(id));
|
|
6
|
+
};
|
|
7
|
+
const parseLocs = (xml) => {
|
|
8
|
+
return (xml.match(/<loc>([\s\S]*?)<\/loc>/g) ?? []).map((m) => m.replace(/<\/?loc>/g, "").trim()).filter(Boolean);
|
|
9
|
+
};
|
|
10
|
+
const fetchSitemapUrls = async (sitemapUrl, visited) => {
|
|
11
|
+
if (visited.has(sitemapUrl)) return [];
|
|
12
|
+
visited.add(sitemapUrl);
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetchWithTimeout(sitemapUrl);
|
|
15
|
+
if (!res.ok) return [];
|
|
16
|
+
const xml = await res.text();
|
|
17
|
+
if (xml.includes("<sitemapindex")) {
|
|
18
|
+
const childUrls = parseLocs(xml);
|
|
19
|
+
return (await Promise.all(childUrls.map((url) => fetchSitemapUrls(url, visited)))).flat();
|
|
20
|
+
}
|
|
21
|
+
return parseLocs(xml);
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const extractUrlFromSitemap = async (pageUrl) => {
|
|
27
|
+
const origin = new URL(pageUrl).origin;
|
|
28
|
+
const candidateUrls = [];
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetchWithTimeout(`${origin}/robots.txt`, 3e3);
|
|
31
|
+
if (res.ok) ((await res.text()).match(/^Sitemap:\s*(.+)$/gim) ?? []).forEach((m) => {
|
|
32
|
+
const url = m.replace(/^Sitemap:\s*/i, "").trim();
|
|
33
|
+
if (url) candidateUrls.push(url);
|
|
34
|
+
});
|
|
35
|
+
} catch {}
|
|
36
|
+
if (candidateUrls.length === 0) candidateUrls.push(`${origin}/sitemap.xml`);
|
|
37
|
+
const visited = /* @__PURE__ */ new Set();
|
|
38
|
+
const results = await Promise.all(candidateUrls.map((url) => fetchSitemapUrls(url, visited)));
|
|
39
|
+
return Array.from(new Set(results.flat())).sort();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { extractUrlFromSitemap };
|
|
44
|
+
//# sourceMappingURL=extractUrlFromSitemap.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractUrlFromSitemap.mjs","names":[],"sources":["../../../../src/components/Browser/extractUrlFromSitemap.ts"],"sourcesContent":["const fetchWithTimeout = (url: string, timeout = 5000): Promise<Response> => {\n const controller = new AbortController();\n const id = setTimeout(() => controller.abort(), timeout);\n return fetch(url, { signal: controller.signal }).finally(() =>\n clearTimeout(id)\n );\n};\n\nconst parseLocs = (xml: string): string[] => {\n const matches = xml.match(/<loc>([\\s\\S]*?)<\\/loc>/g) ?? [];\n return matches.map((m) => m.replace(/<\\/?loc>/g, '').trim()).filter(Boolean);\n};\n\nconst fetchSitemapUrls = async (\n sitemapUrl: string,\n visited: Set<string>\n): Promise<string[]> => {\n if (visited.has(sitemapUrl)) return [];\n visited.add(sitemapUrl);\n\n try {\n const res = await fetchWithTimeout(sitemapUrl);\n if (!res.ok) return [];\n const xml = await res.text();\n\n if (xml.includes('<sitemapindex')) {\n const childUrls = parseLocs(xml);\n const results = await Promise.all(\n childUrls.map((url) => fetchSitemapUrls(url, visited))\n );\n return results.flat();\n }\n\n return parseLocs(xml);\n } catch {\n return [];\n }\n};\n\nexport const extractUrlFromSitemap = async (\n pageUrl: string\n): Promise<string[]> => {\n const origin = new URL(pageUrl).origin;\n const candidateUrls: string[] = [];\n\n try {\n const res = await fetchWithTimeout(`${origin}/robots.txt`, 3000);\n if (res.ok) {\n const text = await res.text();\n const matches = text.match(/^Sitemap:\\s*(.+)$/gim) ?? [];\n matches.forEach((m) => {\n const url = m.replace(/^Sitemap:\\s*/i, '').trim();\n if (url) candidateUrls.push(url);\n });\n }\n } catch {\n // Ignore network / CORS errors for robots.txt\n }\n\n if (candidateUrls.length === 0) {\n candidateUrls.push(`${origin}/sitemap.xml`);\n }\n\n const visited = new Set<string>();\n const results = await Promise.all(\n candidateUrls.map((url) => fetchSitemapUrls(url, visited))\n );\n\n return Array.from(new Set(results.flat())).sort();\n};\n"],"mappings":";AAAA,MAAM,oBAAoB,KAAa,UAAU,QAA4B;CAC3E,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,KAAK,iBAAiB,WAAW,MAAM,GAAG,OAAO;CACvD,OAAO,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EAAE,cAC/C,aAAa,EAAE,CACjB;AACF;AAEA,MAAM,aAAa,QAA0B;CAE3C,QADgB,IAAI,MAAM,yBAAyB,KAAK,CAAC,GAC1C,KAAK,MAAM,EAAE,QAAQ,aAAa,EAAE,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAC7E;AAEA,MAAM,mBAAmB,OACvB,YACA,YACsB;CACtB,IAAI,QAAQ,IAAI,UAAU,GAAG,OAAO,CAAC;CACrC,QAAQ,IAAI,UAAU;CAEtB,IAAI;EACF,MAAM,MAAM,MAAM,iBAAiB,UAAU;EAC7C,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC;EACrB,MAAM,MAAM,MAAM,IAAI,KAAK;EAE3B,IAAI,IAAI,SAAS,eAAe,GAAG;GACjC,MAAM,YAAY,UAAU,GAAG;GAI/B,QAAO,MAHe,QAAQ,IAC5B,UAAU,KAAK,QAAQ,iBAAiB,KAAK,OAAO,CAAC,CACvD,GACe,KAAK;EACtB;EAEA,OAAO,UAAU,GAAG;CACtB,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,MAAa,wBAAwB,OACnC,YACsB;CACtB,MAAM,SAAS,IAAI,IAAI,OAAO,EAAE;CAChC,MAAM,gBAA0B,CAAC;CAEjC,IAAI;EACF,MAAM,MAAM,MAAM,iBAAiB,GAAG,OAAO,cAAc,GAAI;EAC/D,IAAI,IAAI,IAGN,EADgB,MADG,IAAI,KAAK,GACP,MAAM,sBAAsB,KAAK,CAAC,GAC/C,SAAS,MAAM;GACrB,MAAM,MAAM,EAAE,QAAQ,iBAAiB,EAAE,EAAE,KAAK;GAChD,IAAI,KAAK,cAAc,KAAK,GAAG;EACjC,CAAC;CAEL,QAAQ,CAER;CAEA,IAAI,cAAc,WAAW,GAC3B,cAAc,KAAK,GAAG,OAAO,aAAa;CAG5C,MAAM,0BAAU,IAAI,IAAY;CAChC,MAAM,UAAU,MAAM,QAAQ,IAC5B,cAAc,KAAK,QAAQ,iBAAiB,KAAK,OAAO,CAAC,CAC3D;CAEA,OAAO,MAAM,KAAK,IAAI,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,KAAK;AAClD"}
|
|
@@ -13,6 +13,7 @@ import { ContentEditorTextArea as ContentEditorTextArea$1 } from "../../ContentE
|
|
|
13
13
|
import { renameKey } from "./object.mjs";
|
|
14
14
|
import { Label } from "../../Label/index.mjs";
|
|
15
15
|
import { EnumKeyInput } from "../EnumKeyInput.mjs";
|
|
16
|
+
import { SafeHtmlRenderer } from "./SafeHtmlRenderer.mjs";
|
|
16
17
|
import { Fragment, Suspense, lazy, memo, useState } from "react";
|
|
17
18
|
import { Plus, Trash, WandSparkles } from "lucide-react";
|
|
18
19
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -308,6 +309,47 @@ const GenderTextEditor = ({ section, keyPath, dictionary, renderSection }) => {
|
|
|
308
309
|
})
|
|
309
310
|
});
|
|
310
311
|
};
|
|
312
|
+
const PLURAL_CATEGORIES = [
|
|
313
|
+
"zero",
|
|
314
|
+
"one",
|
|
315
|
+
"two",
|
|
316
|
+
"few",
|
|
317
|
+
"many",
|
|
318
|
+
"other"
|
|
319
|
+
];
|
|
320
|
+
const PluralTextEditor = ({ section, keyPath, dictionary, renderSection }) => {
|
|
321
|
+
const content = section[NodeTypes.PLURAL];
|
|
322
|
+
return /* @__PURE__ */ jsx("table", {
|
|
323
|
+
className: "w-full",
|
|
324
|
+
children: /* @__PURE__ */ jsx("tbody", {
|
|
325
|
+
className: "flex w-full flex-col gap-2",
|
|
326
|
+
children: PLURAL_CATEGORIES.map((pluralKey) => {
|
|
327
|
+
const uniqueKey = `${JSON.stringify(keyPath)}-plural-${pluralKey}`;
|
|
328
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("tr", {
|
|
329
|
+
className: "mt-2 block w-full p-2 text-xs",
|
|
330
|
+
children: /* @__PURE__ */ jsx("td", {
|
|
331
|
+
className: "flex w-full",
|
|
332
|
+
children: pluralKey
|
|
333
|
+
})
|
|
334
|
+
}), /* @__PURE__ */ jsx("tr", {
|
|
335
|
+
className: "block w-full",
|
|
336
|
+
children: /* @__PURE__ */ jsx("td", {
|
|
337
|
+
className: "flex w-full",
|
|
338
|
+
children: /* @__PURE__ */ jsx(TextEditorContainer, {
|
|
339
|
+
section: content[pluralKey] ?? getEmptyNode(content.other),
|
|
340
|
+
keyPath: [...keyPath, {
|
|
341
|
+
type: NodeTypes.PLURAL,
|
|
342
|
+
key: pluralKey
|
|
343
|
+
}],
|
|
344
|
+
dictionary,
|
|
345
|
+
renderSection
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
})] }, uniqueKey);
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
});
|
|
352
|
+
};
|
|
311
353
|
const ArrayTextEditor = ({ section, keyPath, dictionary, renderSection }) => {
|
|
312
354
|
const { addEditedContent } = useEditedContent();
|
|
313
355
|
const { addNewElement, removeElement } = useIntlayer("navigation-view");
|
|
@@ -468,7 +510,7 @@ const MarkdownTextEditor = ({ section, keyPath, dictionary, isDarkMode }) => {
|
|
|
468
510
|
value: 1
|
|
469
511
|
}];
|
|
470
512
|
const childKeyPath = [...keyPath, { type: NodeTypes.MARKDOWN }];
|
|
471
|
-
const content = section[NodeTypes.MARKDOWN];
|
|
513
|
+
const content = section[NodeTypes.MARKDOWN] ?? "";
|
|
472
514
|
return /* @__PURE__ */ jsxs("div", {
|
|
473
515
|
className: "flex w-full flex-col justify-center gap-6 p-2",
|
|
474
516
|
children: [/* @__PURE__ */ jsx(SwitchSelector, {
|
|
@@ -598,6 +640,12 @@ const TextEditor = memo(function TextEditor({ section, keyPath, dictionary, rend
|
|
|
598
640
|
keyPath,
|
|
599
641
|
section
|
|
600
642
|
});
|
|
643
|
+
if (nodeType === NodeTypes.PLURAL) return /* @__PURE__ */ jsx(PluralTextEditor, {
|
|
644
|
+
dictionary,
|
|
645
|
+
renderSection,
|
|
646
|
+
keyPath,
|
|
647
|
+
section
|
|
648
|
+
});
|
|
601
649
|
if (nodeType === NodeTypes.INSERTION) return /* @__PURE__ */ jsx(InsertionTextEditor, {
|
|
602
650
|
dictionary,
|
|
603
651
|
renderSection,
|