@intlayer/design-system 7.5.3 → 7.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/esm/components/Accordion/Accordion.mjs +1 -1
  2. package/dist/esm/components/Avatar/index.mjs +1 -1
  3. package/dist/esm/components/Breadcrumb/index.mjs +1 -1
  4. package/dist/esm/components/Browser/Browser.content.mjs +36 -17
  5. package/dist/esm/components/Browser/Browser.content.mjs.map +1 -1
  6. package/dist/esm/components/Browser/Browser.mjs +191 -131
  7. package/dist/esm/components/Browser/Browser.mjs.map +1 -1
  8. package/dist/esm/components/CollapsibleTable/CollapsibleTable.mjs +1 -1
  9. package/dist/esm/components/ContentEditor/ContentEditor.mjs +1 -1
  10. package/dist/esm/components/ContentEditor/ContentEditorInput.mjs +1 -1
  11. package/dist/esm/components/ContentEditor/ContentEditorTextArea.mjs +2 -2
  12. package/dist/esm/components/CopyButton/index.mjs +1 -1
  13. package/dist/esm/components/CopyToClipboard/index.mjs +1 -1
  14. package/dist/esm/components/DictionaryEditor/DictionaryEditor.mjs +1 -1
  15. package/dist/esm/components/DictionaryEditor/NodeWrapper/FileWrapper.mjs +1 -1
  16. package/dist/esm/components/DictionaryEditor/NodeWrapper/StringWrapper.mjs +1 -1
  17. package/dist/esm/components/DictionaryEditor/NodeWrapper/index.mjs +1 -1
  18. package/dist/esm/components/DictionaryFieldEditor/ContentEditor.mjs +1 -1
  19. package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs +3 -3
  20. package/dist/esm/components/DictionaryFieldEditor/DictionaryCreationForm/DictionaryCreationForm.mjs +1 -1
  21. package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.mjs +4 -4
  22. package/dist/esm/components/DictionaryFieldEditor/DictionaryFieldEditor.mjs +2 -2
  23. package/dist/esm/components/DictionaryFieldEditor/JSONEditor.mjs +1 -1
  24. package/dist/esm/components/DictionaryFieldEditor/NavigationView/NavigationViewNode.mjs +2 -2
  25. package/dist/esm/components/DictionaryFieldEditor/SaveForm/SaveForm.mjs +4 -4
  26. package/dist/esm/components/DictionaryFieldEditor/StructureEditor.mjs +1 -1
  27. package/dist/esm/components/DictionaryFieldEditor/StructureView/StructureView.mjs +1 -1
  28. package/dist/esm/components/EditableField/EditableFieldLayout.mjs +1 -1
  29. package/dist/esm/components/HideShow/index.mjs +1 -1
  30. package/dist/esm/components/IDE/FileTree.mjs +1 -1
  31. package/dist/esm/components/Input/InputPassword.mjs +1 -1
  32. package/dist/esm/components/Input/OTPInput.mjs +1 -1
  33. package/dist/esm/components/KeyboardScreenAdapter/index.mjs +1 -1
  34. package/dist/esm/components/KeyboardShortcut/KeyboardShortcut.mjs +1 -1
  35. package/dist/esm/components/LocaleSwitcherContentDropDown/LocaleSwitcherContent.mjs +2 -2
  36. package/dist/esm/components/LocaleSwitcherDropDown/LocaleSwitcher.mjs +1 -1
  37. package/dist/esm/components/Modal/Modal.mjs +3 -3
  38. package/dist/esm/components/Navbar/MobileNavbar.mjs +2 -2
  39. package/dist/esm/components/Pagination/Pagination.mjs +2 -2
  40. package/dist/esm/components/RightDrawer/RightDrawer.mjs +3 -3
  41. package/dist/esm/components/Select/Multiselect.mjs +1 -1
  42. package/dist/esm/components/SwitchSelector/index.mjs +1 -1
  43. package/dist/esm/components/Tab/Tab.mjs +2 -1
  44. package/dist/esm/components/Tab/Tab.mjs.map +1 -1
  45. package/dist/esm/components/TabSelector/TabSelector.mjs +3 -3
  46. package/dist/esm/components/TabSelector/TabSelector.mjs.map +1 -1
  47. package/dist/esm/components/Table/Table.mjs +1 -1
  48. package/dist/esm/components/TextArea/AutocompleteTextArea.mjs +1 -1
  49. package/dist/esm/components/ThemeSwitcherDropDown/DesktopThemeSwitcher.mjs +1 -1
  50. package/dist/esm/components/ThemeSwitcherDropDown/MobileThemeSwitcher.mjs +1 -1
  51. package/dist/esm/hooks/useAuth/useOAuth2.mjs +1 -1
  52. package/dist/esm/hooks/useAuth/useSession.mjs +1 -1
  53. package/dist/esm/libs/auth.mjs +1 -1
  54. package/dist/esm/providers/ReactQueryProvider.mjs +1 -1
  55. package/dist/types/components/Badge/index.d.ts +2 -2
  56. package/dist/types/components/Breadcrumb/breadcrumb.content.d.ts +3 -3
  57. package/dist/types/components/Breadcrumb/index.d.ts +2 -2
  58. package/dist/types/components/Browser/Browser.content.d.ts +52 -11
  59. package/dist/types/components/Browser/Browser.content.d.ts.map +1 -1
  60. package/dist/types/components/Browser/Browser.d.ts +18 -60
  61. package/dist/types/components/Browser/Browser.d.ts.map +1 -1
  62. package/dist/types/components/Button/Button.d.ts +3 -3
  63. package/dist/types/components/CollapsibleTable/CollapsibleTable.d.ts +3 -3
  64. package/dist/types/components/Command/index.d.ts +20 -20
  65. package/dist/types/components/Command/index.d.ts.map +1 -1
  66. package/dist/types/components/Container/index.d.ts +7 -7
  67. package/dist/types/components/Container/index.d.ts.map +1 -1
  68. package/dist/types/components/CopyButton/CopyButton.content.d.ts +3 -3
  69. package/dist/types/components/CopyButton/CopyButton.content.d.ts.map +1 -1
  70. package/dist/types/components/DictionaryFieldEditor/DictionaryCreationForm/dictionaryCreationForm.content.d.ts +25 -25
  71. package/dist/types/components/DictionaryFieldEditor/DictionaryCreationForm/useDictionaryFormSchema.content.d.ts +9 -9
  72. package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/dictionaryDetails.content.d.ts +33 -33
  73. package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/useDictionaryDetailsSchema.content.d.ts +25 -25
  74. package/dist/types/components/DictionaryFieldEditor/NavigationView/navigationViewNode.content.d.ts +25 -25
  75. package/dist/types/components/DictionaryFieldEditor/SaveForm/saveForm.content.d.ts +33 -33
  76. package/dist/types/components/DictionaryFieldEditor/StructureView/structureView.content.d.ts +9 -9
  77. package/dist/types/components/DictionaryFieldEditor/VersionSwitcherDropDown/versionSwitcherDropDown.content.d.ts +7 -7
  78. package/dist/types/components/DictionaryFieldEditor/dictionaryFieldEditor.content.d.ts +5 -5
  79. package/dist/types/components/DictionaryFieldEditor/nodeTypeSelector.content.d.ts +31 -31
  80. package/dist/types/components/ExpandCollapse/expandCollapse.content.d.ts +3 -3
  81. package/dist/types/components/Form/FormBase.d.ts +2 -2
  82. package/dist/types/components/Form/FormBase.d.ts.map +1 -1
  83. package/dist/types/components/Form/FormField.d.ts +2 -2
  84. package/dist/types/components/Form/FormField.d.ts.map +1 -1
  85. package/dist/types/components/Form/FormItem.d.ts +2 -2
  86. package/dist/types/components/Form/elements/MultiselectElement.d.ts +2 -2
  87. package/dist/types/components/Form/elements/OTPElement.d.ts +2 -2
  88. package/dist/types/components/Form/elements/SwitchSelectorElement.d.ts +2 -2
  89. package/dist/types/components/IDE/CodeContext.d.ts +2 -2
  90. package/dist/types/components/IDE/CodeContext.d.ts.map +1 -1
  91. package/dist/types/components/IDE/FileTree.d.ts.map +1 -1
  92. package/dist/types/components/IDE/code.content.d.ts +5 -5
  93. package/dist/types/components/IDE/code.content.d.ts.map +1 -1
  94. package/dist/types/components/IDE/copyCode.content.d.ts +5 -5
  95. package/dist/types/components/IDE/copyCode.content.d.ts.map +1 -1
  96. package/dist/types/components/IDE/selectors.content.d.ts +7 -7
  97. package/dist/types/components/Input/Checkbox.d.ts +4 -4
  98. package/dist/types/components/Input/Checkbox.d.ts.map +1 -1
  99. package/dist/types/components/Input/Input.d.ts +2 -2
  100. package/dist/types/components/Input/Input.d.ts.map +1 -1
  101. package/dist/types/components/Input/OTPInput.d.ts +7 -7
  102. package/dist/types/components/Input/OTPInput.d.ts.map +1 -1
  103. package/dist/types/components/Input/SearchInput.d.ts +2 -2
  104. package/dist/types/components/Input/SearchInput.d.ts.map +1 -1
  105. package/dist/types/components/Link/Link.d.ts +4 -4
  106. package/dist/types/components/Link/Link.d.ts.map +1 -1
  107. package/dist/types/components/Loader/index.content.d.ts +3 -3
  108. package/dist/types/components/Loader/index.content.d.ts.map +1 -1
  109. package/dist/types/components/Loader/spinner.d.ts +2 -2
  110. package/dist/types/components/LocaleSwitcherContentDropDown/localeSwitcher.content.d.ts +17 -17
  111. package/dist/types/components/LocaleSwitcherContentDropDown/localeSwitcher.content.d.ts.map +1 -1
  112. package/dist/types/components/LocaleSwitcherDropDown/localeSwitcher.content.d.ts +13 -13
  113. package/dist/types/components/LocaleSwitcherDropDown/localeSwitcher.content.d.ts.map +1 -1
  114. package/dist/types/components/MaxWidthSmoother/index.d.ts +2 -2
  115. package/dist/types/components/MaxWidthSmoother/index.d.ts.map +1 -1
  116. package/dist/types/components/Navbar/Burger.d.ts +2 -2
  117. package/dist/types/components/Navbar/DesktopNavbar.d.ts +2 -2
  118. package/dist/types/components/Navbar/MobileNavbar.d.ts +2 -2
  119. package/dist/types/components/Navbar/index.d.ts +2 -2
  120. package/dist/types/components/Navbar/index.d.ts.map +1 -1
  121. package/dist/types/components/Pagination/Pagination.d.ts +3 -3
  122. package/dist/types/components/Pagination/pagination.content.d.ts +11 -11
  123. package/dist/types/components/Select/Select.d.ts +3 -3
  124. package/dist/types/components/Select/Select.d.ts.map +1 -1
  125. package/dist/types/components/SocialNetworks/index.d.ts +2 -2
  126. package/dist/types/components/SocialNetworks/index.d.ts.map +1 -1
  127. package/dist/types/components/SwitchSelector/index.d.ts +6 -6
  128. package/dist/types/components/SwitchSelector/index.d.ts.map +1 -1
  129. package/dist/types/components/Tab/Tab.d.ts +5 -5
  130. package/dist/types/components/Tab/Tab.d.ts.map +1 -1
  131. package/dist/types/components/Tab/TabContext.d.ts +2 -2
  132. package/dist/types/components/Tab/TabContext.d.ts.map +1 -1
  133. package/dist/types/components/TabSelector/TabSelector.d.ts +4 -4
  134. package/dist/types/components/Table/table.content.d.ts +3 -3
  135. package/dist/types/components/Table/table.content.d.ts.map +1 -1
  136. package/dist/types/components/Tag/index.d.ts +2 -2
  137. package/dist/types/components/Terminal/terminal.content.d.ts +5 -5
  138. package/dist/types/components/Terminal/terminal.content.d.ts.map +1 -1
  139. package/dist/types/components/Toaster/Toast.d.ts +1 -1
  140. package/dist/types/components/Toaster/Toast.d.ts.map +1 -1
  141. package/dist/types/components/Toaster/Toaster.d.ts +2 -2
  142. package/dist/types/components/Toaster/Toaster.d.ts.map +1 -1
  143. package/package.json +14 -14
@@ -3,8 +3,8 @@
3
3
  import { cn } from "../../utils/cn.mjs";
4
4
  import { Button, ButtonColor, ButtonVariant } from "../Button/Button.mjs";
5
5
  import { MaxHeightSmoother } from "../MaxHeightSmoother/index.mjs";
6
- import { ChevronDown } from "lucide-react";
7
6
  import { useId, useState } from "react";
7
+ import { ChevronDown } from "lucide-react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
 
10
10
  //#region src/components/Accordion/Accordion.tsx
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../../utils/cn.mjs";
2
2
  import { Loader } from "../Loader/index.mjs";
3
- import { User } from "lucide-react";
4
3
  import { useMemo } from "react";
4
+ import { User } from "lucide-react";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
 
7
7
  //#region src/components/Avatar/index.tsx
@@ -3,8 +3,8 @@
3
3
  import { cn } from "../../utils/cn.mjs";
4
4
  import { Button, ButtonVariant } from "../Button/Button.mjs";
5
5
  import { Link, LinkColor } from "../Link/Link.mjs";
6
- import { ChevronRightIcon } from "lucide-react";
7
6
  import { Fragment, createElement } from "react";
7
+ import { ChevronRightIcon } from "lucide-react";
8
8
  import { cva } from "class-variance-authority";
9
9
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
10
  import { getIntlayer } from "@intlayer/core";
@@ -44,23 +44,42 @@ const browserContent = {
44
44
  }),
45
45
  urlPlaceholder: "https://example.com",
46
46
  errorMessage: t({
47
- en: "Invalid URL. Try something like \"https://example.com\".",
48
- "en-GB": "Invalid URL. Try something like \"https://example.com\".",
49
- fr: "URL invalide. Essayez quelque chose comme \"https://example.com\".",
50
- es: "URL no válida. Intente algo como \"https://example.com\".",
51
- de: "Ungültige URL. Versuchen Sie etwas wie \"https://example.com\".",
52
- ja: "無効なURL。\"https://example.com\"のようなものを試してください。",
53
- ko: "잘못된 URL입니다. \"https://example.com\" 같은 것을 시도하십시오.",
54
- zh: "无效的URL。尝试类似\"https://example.com\"的内容。",
55
- it: "URL non valido. Prova qualcosa come \"https://example.com\".",
56
- pt: "URL inválido. Tente algo como \"https://example.com\".",
57
- hi: "अमान्य URL। \"https://example.com\" जैसा कुछ प्रयास करें।",
58
- ar: "رابط غير صالح. جرب شيئًا مثل \"https://example.com\".",
59
- ru: "Неверный URL. Попробуйте что-то вроде \"https://example.com\".",
60
- tr: "Geçersiz URL. \"https://example.com\" gibi bir şey deneyin.",
61
- pl: "Nieprawidłowy adres URL. Wypróbuj coś w rodzaju \"https://example.com\".",
62
- id: "URL tidak valid. Coba misalnya \"https://example.com\".",
63
- vi: "URL không hợp lệ. Thử dụ như \"https://example.com\"."
47
+ en: "Invalid URL. Try a path like \"/page\" or a full URL.",
48
+ "en-GB": "Invalid URL. Try a path like \"/page\" or a full URL.",
49
+ fr: "URL invalide. Essayez un chemin comme \"/page\" ou une URL complète.",
50
+ es: "URL no válida. Intente una ruta como \"/page\" o una URL completa.",
51
+ de: "Ungültige URL. Versuchen Sie einen Pfad wie \"/page\" oder eine vollständige URL.",
52
+ ja: "無効なURL。\"/page\"のようなパスまたは完全なURLを試してください。",
53
+ ko: "잘못된 URL입니다. \"/page\" 같은 경로 또는 전체 URL을 시도하십시오.",
54
+ zh: "无效的URL。尝试像\"/page\"这样的路径或完整的URL。",
55
+ it: "URL non valido. Prova un percorso come \"/page\" o un URL completo.",
56
+ pt: "URL inválido. Tente um caminho como \"/page\" ou uma URL completa.",
57
+ hi: "अमान्य URL। \"/page\" जैसा पथ या पूर्ण URL प्रयास करें।",
58
+ ar: "رابط غير صالح. جرب مسارًا مثل \"/page\" أو رابطًا كاملاً.",
59
+ ru: "Неверный URL. Попробуйте путь вроде \"/page\" или полный URL.",
60
+ tr: "Geçersiz URL. \"/page\" gibi bir yol veya tam URL deneyin.",
61
+ pl: "Nieprawidłowy adres URL. Wypróbuj ścieżkę jak \"/page\" lub pełny URL.",
62
+ id: "URL tidak valid. Coba jalur seperti \"/page\" atau URL lengkap.",
63
+ vi: "URL không hợp lệ. Thử đường dẫn như \"/page\" hoặc URL đầy đủ."
64
+ }),
65
+ domainRestrictionError: t({
66
+ en: "URL must match the application domain.",
67
+ "en-GB": "URL must match the application domain.",
68
+ fr: "L'URL doit correspondre au domaine de l'application.",
69
+ es: "La URL debe coincidir con el dominio de la aplicación.",
70
+ de: "Die URL muss mit der Anwendungsdomäne übereinstimmen.",
71
+ ja: "URLはアプリケーションドメインと一致する必要があります。",
72
+ ko: "URL은 애플리케이션 도메인과 일치해야 합니다.",
73
+ zh: "URL必须与应用程序域名匹配。",
74
+ it: "L'URL deve corrispondere al dominio dell'applicazione.",
75
+ pt: "A URL deve corresponder ao domínio da aplicação.",
76
+ hi: "URL एप्लिकेशन डोमेन से मेल खाना चाहिए।",
77
+ ar: "يجب أن يتطابق الرابط مع نطاق التطبيق.",
78
+ ru: "URL должен соответствовать домену приложения.",
79
+ tr: "URL, uygulama etki alanıyla eşleşmelidir.",
80
+ pl: "Adres URL musi pasować do domeny aplikacji.",
81
+ id: "URL harus cocok dengan domain aplikasi.",
82
+ vi: "URL phải khớp với tên miền ứng dụng."
64
83
  }),
65
84
  reloadButtonTitle: t({
66
85
  en: "Reload page",
@@ -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 }),\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 }),\n urlPlaceholder: 'https://example.com',\n errorMessage: t({\n en: 'Invalid URL. Try something like \"https://example.com\".',\n 'en-GB': 'Invalid URL. Try something like \"https://example.com\".',\n fr: 'URL invalide. Essayez quelque chose comme \"https://example.com\".',\n es: 'URL no válida. Intente algo como \"https://example.com\".',\n de: 'Ungültige URL. Versuchen Sie etwas wie \"https://example.com\".',\n ja: '無効なURL。\"https://example.com\"のようなものを試してください。',\n ko: '잘못된 URL입니다. \"https://example.com\" 같은 것을 시도하십시오.',\n zh: '无效的URL。尝试类似\"https://example.com\"的内容。',\n it: 'URL non valido. Prova qualcosa come \"https://example.com\".',\n pt: 'URL inválido. Tente algo como \"https://example.com\".',\n hi: 'अमान्य URL। \"https://example.com\" जैसा कुछ प्रयास करें।',\n ar: 'رابط غير صالح. جرب شيئًا مثل \"https://example.com\".',\n ru: 'Неверный URL. Попробуйте что-то вроде \"https://example.com\".',\n tr: 'Geçersiz URL. \"https://example.com\" gibi bir şey deneyin.',\n pl: 'Nieprawidłowy adres URL. Wypróbuj coś w rodzaju \"https://example.com\".',\n id: 'URL tidak valid. Coba misalnya \"https://example.com\".',\n vi: 'URL không hợp lệ. Thử dụ như \"https://example.com\".',\n }),\n reloadButtonTitle: t({\n en: 'Reload page',\n 'en-GB': 'Reload page',\n fr: 'Recharger la page',\n es: 'Recargar página',\n de: 'Seite neu laden',\n ja: 'ページを再読み込み',\n ko: '페이지 새로고침',\n zh: '重新加载页面',\n it: 'Ricarica pagina',\n pt: 'Recarregar página',\n hi: 'पृष्ठ को फिर से लोड करें',\n ar: 'إعادة تحميل الصفحة',\n ru: 'Перезагрузить страницу',\n tr: 'Sayfayı yenile',\n pl: 'Przeładuj stronę',\n id: 'Muat ulang halaman',\n vi: 'Tải lại trang',\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 }),\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;GACL,CAAC;EACF,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;GACL,CAAC;EACF,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;GACL,CAAC;EACF,mBAAmB,EAAE;GACnB,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;GACL,CAAC;EACF,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;GACL,CAAC;EACH;CACD,OAAO;CACP,aACE;CACF,MAAM;EAAC;EAAgB;EAAoB;EAAS;CACrD;AAED,8BAAe"}
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 }),\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 }),\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 }),\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 }),\n reloadButtonTitle: t({\n en: 'Reload page',\n 'en-GB': 'Reload page',\n fr: 'Recharger la page',\n es: 'Recargar página',\n de: 'Seite neu laden',\n ja: 'ページを再読み込み',\n ko: '페이지 새로고침',\n zh: '重新加载页面',\n it: 'Ricarica pagina',\n pt: 'Recarregar página',\n hi: 'पृष्ठ को फिर से लोड करें',\n ar: 'إعادة تحميل الصفحة',\n ru: 'Перезагрузить страницу',\n tr: 'Sayfayı yenile',\n pl: 'Przeładuj stronę',\n id: 'Muat ulang halaman',\n vi: 'Tải lại trang',\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 }),\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;GACL,CAAC;EACF,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;GACL,CAAC;EACF,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;GACL,CAAC;EACF,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;GACL,CAAC;EACF,mBAAmB,EAAE;GACnB,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;GACL,CAAC;EACF,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;GACL,CAAC;EACH;CACD,OAAO;CACP,aACE;CACF,MAAM;EAAC;EAAgB;EAAoB;EAAS;CACrD;AAED,8BAAe"}
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import { cn } from "../../utils/cn.mjs";
4
- import { RotateCw } from "lucide-react";
5
- import { useEffect, useRef, useState } from "react";
4
+ import { useEffect, useImperativeHandle, useRef, useState } from "react";
5
+ import { ArrowLeft, ArrowRight, RotateCw } from "lucide-react";
6
6
  import { cva } from "class-variance-authority";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { useIntlayer } from "react-intlayer";
@@ -18,179 +18,239 @@ const browserVariants = cva("flex w-full flex-col overflow-hidden rounded-xl bg-
18
18
  } },
19
19
  defaultVariants: { size: "md" }
20
20
  });
21
- /**
22
- * Browser component that renders an iframe with a visible, editable URL bar.
23
- * Allows users to view, edit, and navigate to different URLs within an embedded browser interface.
24
- *
25
- * Features:
26
- * - Editable URL bar with strict validation (before navigation)
27
- * - Automatic protocol addition (adds https:// if missing)
28
- * - Integrated reload button inside the URL input
29
- * - Error handling with visual feedback for invalid URLs
30
- * - Responsive iframe with standardized sizes
31
- * - Full accessibility support with ARIA attributes
32
- * - Sandbox security for iframe content
33
- * - Dark-themed UI matching modern browser aesthetics
34
- * - Cross-browser compatibility (Chrome, Firefox, Safari)
35
- *
36
- * @example
37
- * // Basic usage
38
- * <Browser initialUrl="https://example.com" size="md" />
39
- *
40
- * @example
41
- * // With custom size and styling
42
- * <Browser
43
- * initialUrl="https://example.com"
44
- * size="lg"
45
- * className="shadow-2xl"
46
- * aria-label="Documentation viewer"
47
- * />
48
- *
49
- * @example
50
- * // For content preview
51
- * <Browser
52
- * initialUrl="https://youtube.com/embed/VIDEO_ID"
53
- * size="xl"
54
- * aria-label="Video content browser"
55
- * />
56
- *
57
- * @example
58
- * // With custom sandbox restrictions
59
- * <Browser
60
- * initialUrl="https://example.com"
61
- * sandbox="allow-scripts allow-same-origin"
62
- * aria-label="Restricted content browser"
63
- * />
64
- *
65
- * @param initialUrl - The initial URL to load in the iframe (default: 'https://example.com')
66
- * @param className - Additional CSS classes for the main container element
67
- * @param style - Inline CSS styles for the main container element
68
- * @param size - Size of the browser window: 'xs' (400px), 'sm' (500px), 'md' (600px), 'lg' (800px), 'xl' (1000px). Defaults to 'md'
69
- * @param aria-label - Accessible label for screen readers describing the browser's purpose (default: 'Embedded browser')
70
- * @param sandbox - Sandbox attribute for the iframe to control security restrictions (default: 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads')
71
- */
72
- const Browser = ({ initialUrl = "https://example.com", className, style, size = "md", "aria-label": ariaLabel, sandbox = "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads" }) => {
21
+ 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 }) => {
73
22
  const [inputUrl, setInputUrl] = useState(initialUrl);
74
23
  const [currentUrl, setCurrentUrl] = useState(initialUrl);
24
+ const [history, setHistory] = useState([initialUrl]);
25
+ const [currentIndex, setCurrentIndex] = useState(0);
75
26
  const [error, setError] = useState(null);
76
27
  const [submitted, setSubmitted] = useState(false);
77
- const iframeRef = useRef(null);
28
+ const internalIframeRef = useRef(null);
29
+ useImperativeHandle(ref, () => internalIframeRef.current, []);
78
30
  const content = useIntlayer("browser");
79
31
  useEffect(() => {
80
32
  setInputUrl(initialUrl);
81
33
  setCurrentUrl(initialUrl);
34
+ setHistory([initialUrl]);
35
+ setCurrentIndex(0);
82
36
  setError(null);
83
37
  setSubmitted(false);
84
38
  }, [initialUrl]);
39
+ useEffect(() => {
40
+ if (!path) return;
41
+ try {
42
+ const baseOrigin = domainRestriction ?? initialUrl;
43
+ const fullUrl = `${new URL(baseOrigin).origin}${path}`;
44
+ setInputUrl(fullUrl);
45
+ let isAlreadyAtUrl = false;
46
+ if (internalIframeRef.current?.contentWindow) try {
47
+ const currentIframeHref = internalIframeRef.current.contentWindow.location.href;
48
+ if (new URL(currentIframeHref).href === new URL(fullUrl).href) isAlreadyAtUrl = true;
49
+ } catch {}
50
+ if (!isAlreadyAtUrl) {
51
+ setCurrentUrl(fullUrl);
52
+ setHistory((prev) => {
53
+ const newHistory = prev.slice(0, currentIndex + 1);
54
+ newHistory.push(fullUrl);
55
+ return newHistory;
56
+ });
57
+ setCurrentIndex((prev) => prev + 1);
58
+ }
59
+ setError(null);
60
+ } catch {}
61
+ }, [
62
+ path,
63
+ domainRestriction,
64
+ initialUrl
65
+ ]);
66
+ const handleNavigateTo = (url) => {
67
+ try {
68
+ const validated = normalizeUrl(url);
69
+ if (validated === currentUrl) {
70
+ handleReload();
71
+ return;
72
+ }
73
+ setCurrentUrl(validated);
74
+ setInputUrl(validated);
75
+ setError(null);
76
+ const newHistory = history.slice(0, currentIndex + 1);
77
+ newHistory.push(validated);
78
+ setHistory(newHistory);
79
+ setCurrentIndex(newHistory.length - 1);
80
+ } catch (e) {
81
+ if (e instanceof Error && e.message === "URL does not match allowed domain" && domainRestriction) setError(content.domainRestrictionError?.value ?? `Only URLs from ${domainRestriction} are allowed.`);
82
+ else setError(content.errorMessage.value);
83
+ }
84
+ };
85
+ const handleBack = () => {
86
+ if (currentIndex > 0) {
87
+ const newIndex = currentIndex - 1;
88
+ const prevUrl = history[newIndex];
89
+ setCurrentIndex(newIndex);
90
+ setCurrentUrl(prevUrl);
91
+ setInputUrl(prevUrl);
92
+ setError(null);
93
+ }
94
+ };
95
+ const handleForward = () => {
96
+ if (currentIndex < history.length - 1) {
97
+ const newIndex = currentIndex + 1;
98
+ const nextUrl = history[newIndex];
99
+ setCurrentIndex(newIndex);
100
+ setCurrentUrl(nextUrl);
101
+ setInputUrl(nextUrl);
102
+ setError(null);
103
+ }
104
+ };
105
+ const handleSubmit = (e) => {
106
+ e.preventDefault();
107
+ setSubmitted(true);
108
+ handleNavigateTo(inputUrl);
109
+ };
110
+ const handleReload = () => {
111
+ if (internalIframeRef.current) {
112
+ const src = internalIframeRef.current.src;
113
+ internalIframeRef.current.src = "";
114
+ setTimeout(() => {
115
+ if (internalIframeRef.current) internalIframeRef.current.src = src;
116
+ }, 50);
117
+ }
118
+ };
85
119
  const isValidHostname = (host) => {
120
+ if (host === "localhost") return true;
121
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(host)) return true;
122
+ if (/^[a-f0-9:]+$/i.test(host)) return true;
86
123
  if (!/^[a-z0-9.-]+$/i.test(host)) return false;
87
124
  if (/^[-.]/.test(host) || /[-.]$/.test(host)) return false;
88
125
  if (host.includes("..")) return false;
89
126
  if (!host.includes(".")) return false;
90
127
  return true;
91
128
  };
129
+ const getRestrictionOrigin = () => {
130
+ if (!domainRestriction) return null;
131
+ try {
132
+ return new URL(domainRestriction);
133
+ } catch {
134
+ return null;
135
+ }
136
+ };
92
137
  const normalizeUrl = (raw) => {
93
138
  const trimmed = raw.trim();
94
139
  if (!trimmed || /\s/.test(trimmed)) throw new Error("Invalid");
140
+ const restrictionOrigin = getRestrictionOrigin();
141
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
142
+ if (restrictionOrigin) return new URL(`${restrictionOrigin.origin}${trimmed}`).toString();
143
+ return new URL(`${new URL(currentUrl).origin}${trimmed}`).toString();
144
+ }
95
145
  const candidate = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed) ? trimmed : `https://${trimmed}`;
96
146
  const url = new URL(candidate);
97
147
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("Only http(s) is allowed");
98
148
  if (!isValidHostname(url.hostname)) throw new Error("Invalid host");
99
- return url.toString();
100
- };
101
- const validateAndNavigate = (url) => {
102
- try {
103
- const validated = normalizeUrl(url);
104
- setCurrentUrl(validated);
105
- setInputUrl(validated);
106
- setError(null);
107
- } catch {
108
- setError(content.errorMessage.value);
109
- }
110
- };
111
- const handleSubmit = (e) => {
112
- e.preventDefault();
113
- setSubmitted(true);
114
- validateAndNavigate(inputUrl);
115
- };
116
- const handleReload = () => {
117
- if (iframeRef.current) {
118
- const url = iframeRef.current.src;
119
- iframeRef.current.src = "";
120
- iframeRef.current.src = url;
149
+ if (restrictionOrigin) {
150
+ if (!(url.hostname === restrictionOrigin.hostname && url.protocol === restrictionOrigin.protocol && (restrictionOrigin.port === "" || url.port === restrictionOrigin.port || url.host === restrictionOrigin.host))) throw new Error("URL does not match allowed domain");
121
151
  }
152
+ return url.toString();
122
153
  };
123
154
  const showError = submitted && !!error;
155
+ const canGoBack = currentIndex > 0;
156
+ const canGoForward = currentIndex < history.length - 1;
124
157
  return /* @__PURE__ */ jsxs("section", {
125
158
  className: cn(browserVariants({ size }), className),
126
159
  style,
127
160
  "aria-label": ariaLabel ?? content.ariaLabel.value,
128
161
  children: [/* @__PURE__ */ jsxs("div", {
129
- className: "relative z-10 flex shrink-0 flex-col gap-1 rounded-t-xl bg-neutral-900 px-4 py-2.5 shadow-[0_3px_4px_0_rgba(0,0,0,0.25)]",
130
- children: [/* @__PURE__ */ jsxs("form", {
131
- onSubmit: handleSubmit,
132
- className: "relative flex-1",
133
- noValidate: true,
134
- children: [
135
- /* @__PURE__ */ jsx("label", {
136
- htmlFor: "browser-url",
137
- className: "sr-only",
138
- children: content.urlLabel.value
139
- }),
140
- /* @__PURE__ */ jsx("input", {
141
- id: "browser-url",
142
- type: "text",
143
- inputMode: "url",
144
- spellCheck: false,
145
- autoCapitalize: "off",
146
- autoCorrect: "off",
147
- value: inputUrl,
148
- onChange: (e) => {
149
- setInputUrl(e.target.value);
150
- if (showError) setError(null);
151
- },
152
- placeholder: content.urlPlaceholder.value,
153
- className: cn("w-full rounded-lg px-4 py-2 pr-11 text-sm leading-[1.4]", "bg-neutral-950 text-neutral-300", "placeholder:text-neutral-400", "transition-all focus:outline-none focus:ring-1 focus:ring-neutral-400/30", showError ? "border border-red-500" : "border border-transparent"),
154
- "aria-label": content.urlLabel.value,
155
- "aria-invalid": showError,
156
- "aria-describedby": showError ? "browser-url-error" : void 0
157
- }),
158
- /* @__PURE__ */ jsx("button", {
162
+ className: "relative z-10 flex shrink-0 items-center gap-3 rounded-t-xl bg-neutral-900 px-4 py-2.5",
163
+ children: [
164
+ /* @__PURE__ */ jsxs("div", {
165
+ className: "flex items-center gap-1",
166
+ children: [/* @__PURE__ */ jsx("button", {
167
+ type: "button",
168
+ onClick: handleBack,
169
+ disabled: !canGoBack,
170
+ className: cn("flex h-8 w-8 items-center justify-center rounded-md text-neutral-400 transition-colors", canGoBack ? "hover:bg-neutral-800 hover:text-neutral-200" : "cursor-not-allowed opacity-40"),
171
+ "aria-label": "Go back",
172
+ title: "Go back",
173
+ children: /* @__PURE__ */ jsx(ArrowLeft, { size: 18 })
174
+ }), /* @__PURE__ */ jsx("button", {
159
175
  type: "button",
160
- onClick: handleReload,
161
- className: cn("-translate-y-1/2 absolute top-1/2 right-2", "flex h-8 w-8 items-center justify-center rounded-md", "transition-colors hover:bg-neutral-800", "focus:outline-none focus:ring-1 focus:ring-neutral-400/30"),
162
- title: content.reloadButtonTitle.value,
163
- "aria-label": content.reloadButtonTitle.value,
164
- tabIndex: 0,
165
- children: /* @__PURE__ */ jsx(RotateCw, {
166
- size: 18,
167
- className: "text-neutral-400",
168
- strokeWidth: 2
176
+ onClick: handleForward,
177
+ disabled: !canGoForward,
178
+ className: cn("flex h-8 w-8 items-center justify-center rounded-md text-neutral-400 transition-colors", canGoForward ? "hover:bg-neutral-800 hover:text-neutral-200" : "cursor-not-allowed opacity-40"),
179
+ "aria-label": "Go forward",
180
+ title: "Go forward",
181
+ children: /* @__PURE__ */ jsx(ArrowRight, { size: 18 })
182
+ })]
183
+ }),
184
+ /* @__PURE__ */ jsxs("form", {
185
+ onSubmit: handleSubmit,
186
+ className: "relative flex-1",
187
+ noValidate: true,
188
+ children: [
189
+ /* @__PURE__ */ jsx("label", {
190
+ htmlFor: "browser-url",
191
+ className: "sr-only",
192
+ children: content.urlLabel.value
193
+ }),
194
+ /* @__PURE__ */ jsx("input", {
195
+ id: "browser-url",
196
+ type: "text",
197
+ inputMode: "url",
198
+ spellCheck: false,
199
+ autoCapitalize: "off",
200
+ autoCorrect: "off",
201
+ value: inputUrl,
202
+ onChange: (e) => {
203
+ setInputUrl(e.target.value);
204
+ if (showError) setError(null);
205
+ },
206
+ placeholder: content.urlPlaceholder.value,
207
+ className: cn("w-full rounded-lg px-4 py-2 pr-11 text-sm leading-[1.4]", "bg-neutral-950 text-neutral-300", "placeholder:text-neutral-400", "transition-all focus:outline-none focus:ring-1 focus:ring-neutral-400/30", showError ? "border border-red-500" : "border border-transparent"),
208
+ "aria-label": content.urlLabel.value,
209
+ "aria-invalid": showError,
210
+ "aria-describedby": showError ? "browser-url-error" : void 0
211
+ }),
212
+ /* @__PURE__ */ jsx("button", {
213
+ type: "button",
214
+ onClick: handleReload,
215
+ className: cn("-translate-y-1/2 absolute top-1/2 right-2", "flex h-8 w-8 items-center justify-center rounded-md", "transition-colors hover:bg-neutral-800", "focus:outline-none focus:ring-1 focus:ring-neutral-400/30"),
216
+ title: content.reloadButtonTitle.value,
217
+ "aria-label": content.reloadButtonTitle.value,
218
+ tabIndex: 0,
219
+ children: /* @__PURE__ */ jsx(RotateCw, {
220
+ size: 18,
221
+ className: "text-neutral-400",
222
+ strokeWidth: 2
223
+ })
224
+ }),
225
+ /* @__PURE__ */ jsx("button", {
226
+ type: "submit",
227
+ className: "sr-only absolute",
228
+ tabIndex: -1
169
229
  })
170
- }),
171
- /* @__PURE__ */ jsx("button", {
172
- type: "submit",
173
- className: "sr-only absolute",
174
- tabIndex: -1
230
+ ]
231
+ }),
232
+ showError && /* @__PURE__ */ jsx("div", {
233
+ className: "absolute top-full left-4 z-20 mt-1",
234
+ children: /* @__PURE__ */ jsx("p", {
235
+ id: "browser-url-error",
236
+ role: "alert",
237
+ "aria-live": "assertive",
238
+ className: "rounded-md bg-red-900/90 px-3 py-1.5 text-red-100 text-xs shadow-md backdrop-blur-sm",
239
+ children: error
175
240
  })
176
- ]
177
- }), showError && /* @__PURE__ */ jsx("p", {
178
- id: "browser-url-error",
179
- role: "alert",
180
- "aria-live": "assertive",
181
- className: "px-1 text-red-400 text-xs",
182
- children: error
183
- })]
241
+ })
242
+ ]
184
243
  }), /* @__PURE__ */ jsx("div", {
185
244
  className: "relative z-0 min-h-0 w-full flex-1 overflow-hidden rounded-b-xl bg-background",
186
245
  children: /* @__PURE__ */ jsx("iframe", {
187
- ref: iframeRef,
246
+ ref: internalIframeRef,
188
247
  src: currentUrl,
189
248
  title: content.iframeTitle.value,
190
249
  className: "h-full w-full rounded-b-xl border-0",
191
250
  sandbox,
192
251
  loading: "lazy",
193
- "aria-live": "polite"
252
+ "aria-live": "polite",
253
+ ...props
194
254
  })
195
255
  })]
196
256
  });
@@ -1 +1 @@
1
- {"version":3,"file":"Browser.mjs","names":["Browser: FC<BrowserProps>"],"sources":["../../../../src/components/Browser/Browser.tsx"],"sourcesContent":["'use client';\n\nimport { cva } from 'class-variance-authority';\nimport { RotateCw } from 'lucide-react';\nimport {\n type CSSProperties,\n type FC,\n type FormEvent,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport { useIntlayer } from 'react-intlayer';\nimport { cn } from '../../utils/cn';\n\nconst browserVariants = cva(\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 {\n variants: {\n size: {\n xs: 'h-[400px]',\n sm: 'h-[500px]',\n md: 'h-[600px]',\n lg: 'h-[800px]',\n xl: 'h-[1000px]',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n }\n);\n\nexport type BrowserProps = {\n /** Initial URL to load in the iframe */\n initialUrl?: string;\n /** Additional CSS classes for the container */\n className?: string;\n /** Inline styles for the container */\n style?: CSSProperties;\n /** Size of the browser window */\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n /** Accessible label for screen readers describing the browser purpose */\n 'aria-label'?: string;\n /** Sandbox attribute for the iframe to control security restrictions */\n sandbox?: string;\n};\n\n/**\n * Browser component that renders an iframe with a visible, editable URL bar.\n * Allows users to view, edit, and navigate to different URLs within an embedded browser interface.\n *\n * Features:\n * - Editable URL bar with strict validation (before navigation)\n * - Automatic protocol addition (adds https:// if missing)\n * - Integrated reload button inside the URL input\n * - Error handling with visual feedback for invalid URLs\n * - Responsive iframe with standardized sizes\n * - Full accessibility support with ARIA attributes\n * - Sandbox security for iframe content\n * - Dark-themed UI matching modern browser aesthetics\n * - Cross-browser compatibility (Chrome, Firefox, Safari)\n *\n * @example\n * // Basic usage\n * <Browser initialUrl=\"https://example.com\" size=\"md\" />\n *\n * @example\n * // With custom size and styling\n * <Browser\n * initialUrl=\"https://example.com\"\n * size=\"lg\"\n * className=\"shadow-2xl\"\n * aria-label=\"Documentation viewer\"\n * />\n *\n * @example\n * // For content preview\n * <Browser\n * initialUrl=\"https://youtube.com/embed/VIDEO_ID\"\n * size=\"xl\"\n * aria-label=\"Video content browser\"\n * />\n *\n * @example\n * // With custom sandbox restrictions\n * <Browser\n * initialUrl=\"https://example.com\"\n * sandbox=\"allow-scripts allow-same-origin\"\n * aria-label=\"Restricted content browser\"\n * />\n *\n * @param initialUrl - The initial URL to load in the iframe (default: 'https://example.com')\n * @param className - Additional CSS classes for the main container element\n * @param style - Inline CSS styles for the main container element\n * @param size - Size of the browser window: 'xs' (400px), 'sm' (500px), 'md' (600px), 'lg' (800px), 'xl' (1000px). Defaults to 'md'\n * @param aria-label - Accessible label for screen readers describing the browser's purpose (default: 'Embedded browser')\n * @param sandbox - Sandbox attribute for the iframe to control security restrictions (default: 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads')\n */\nexport const Browser: FC<BrowserProps> = ({\n initialUrl = 'https://example.com',\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}) => {\n const [inputUrl, setInputUrl] = useState(initialUrl);\n const [currentUrl, setCurrentUrl] = useState(initialUrl);\n const [error, setError] = useState<string | null>(null);\n const [submitted, setSubmitted] = useState(false); // show errors only after attempt\n const iframeRef = useRef<HTMLIFrameElement>(null);\n\n // Load internationalized content\n const content = useIntlayer('browser');\n\n useEffect(() => {\n setInputUrl(initialUrl);\n setCurrentUrl(initialUrl);\n setError(null);\n setSubmitted(false);\n }, [initialUrl]);\n\n // --- Validation helpers ----------------------------------------------------\n const isValidHostname = (host: string) => {\n // allowed chars\n if (!/^[a-z0-9.-]+$/i.test(host)) return false;\n // no leading/trailing dot or hyphen\n if (/^[-.]/.test(host) || /[-.]$/.test(host)) return false;\n // no double dots\n if (host.includes('..')) return false;\n // must have at least one dot\n if (!host.includes('.')) return false;\n\n return true;\n };\n\n const normalizeUrl = (raw: string) => {\n const trimmed = raw.trim();\n if (!trimmed || /\\s/.test(trimmed)) throw new Error('Invalid');\n\n // Add https:// if protocol is missing\n const candidate = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)\n ? trimmed\n : `https://${trimmed}`;\n\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)) {\n throw new Error('Invalid host');\n }\n\n return url.toString();\n };\n\n const validateAndNavigate = (url: string) => {\n try {\n const validated = normalizeUrl(url);\n setCurrentUrl(validated);\n setInputUrl(validated);\n setError(null);\n } catch {\n setError(content.errorMessage.value);\n }\n };\n // --------------------------------------------------------------------------\n\n const handleSubmit = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n setSubmitted(true);\n validateAndNavigate(inputUrl);\n };\n\n const handleReload = () => {\n if (iframeRef.current) {\n const url = iframeRef.current.src;\n iframeRef.current.src = '';\n iframeRef.current.src = url;\n }\n };\n\n const showError = submitted && !!error;\n\n return (\n <section\n className={cn(browserVariants({ size }), className)}\n style={style}\n aria-label={ariaLabel ?? content.ariaLabel.value}\n >\n {/* Top bar */}\n <div className=\"relative z-10 flex shrink-0 flex-col gap-1 rounded-t-xl bg-neutral-900 px-4 py-2.5 shadow-[0_3px_4px_0_rgba(0,0,0,0.25)]\">\n <form onSubmit={handleSubmit} className=\"relative flex-1\" noValidate>\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 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 className={cn(\n 'w-full rounded-lg px-4 py-2 pr-11 text-sm leading-[1.4]',\n 'bg-neutral-950 text-neutral-300',\n 'placeholder:text-neutral-400',\n 'transition-all focus:outline-none focus:ring-1 focus:ring-neutral-400/30',\n showError ? 'border border-red-500' : 'border border-transparent'\n )}\n aria-label={content.urlLabel.value}\n aria-invalid={showError}\n aria-describedby={showError ? 'browser-url-error' : undefined}\n />\n\n {/* Absolutely positioned button inside the input */}\n <button\n type=\"button\"\n onClick={handleReload}\n className={cn(\n '-translate-y-1/2 absolute top-1/2 right-2',\n 'flex h-8 w-8 items-center justify-center rounded-md',\n 'transition-colors hover:bg-neutral-800',\n 'focus:outline-none focus:ring-1 focus:ring-neutral-400/30'\n )}\n title={content.reloadButtonTitle.value}\n aria-label={content.reloadButtonTitle.value}\n tabIndex={0}\n >\n <RotateCw size={18} className=\"text-neutral-400\" strokeWidth={2} />\n </button>\n\n {/* invisible submit to allow Enter to work semantically */}\n <button type=\"submit\" className=\"sr-only absolute\" tabIndex={-1} />\n </form>\n\n {/* subtle inline error text */}\n {showError && (\n <p\n id=\"browser-url-error\"\n role=\"alert\"\n aria-live=\"assertive\"\n className=\"px-1 text-red-400 text-xs\"\n >\n {error}\n </p>\n )}\n </div>\n\n {/* Iframe */}\n <div className=\"relative z-0 min-h-0 w-full flex-1 overflow-hidden rounded-b-xl bg-background\">\n <iframe\n ref={iframeRef}\n src={currentUrl}\n title={content.iframeTitle.value}\n className=\"h-full w-full rounded-b-xl border-0\"\n sandbox={sandbox}\n loading=\"lazy\"\n aria-live=\"polite\"\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 { cva } from 'class-variance-authority';\nimport { ArrowLeft, ArrowRight, RotateCw } from 'lucide-react';\nimport {\n type CSSProperties,\n type FormEvent,\n type HTMLAttributes,\n type RefObject,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport { useIntlayer } from 'react-intlayer';\nimport { cn } from '../../utils/cn';\n\nconst browserVariants = cva(\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 {\n variants: {\n size: {\n xs: 'h-[400px]',\n sm: 'h-[500px]',\n md: 'h-[600px]',\n lg: 'h-[800px]',\n xl: 'h-[1000px]',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n }\n);\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 // 1. Update Input\n setInputUrl(fullUrl);\n\n // 2. 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 // 3. Navigate if needed (Push to history)\n if (!isAlreadyAtUrl) {\n // If the path prop forces a change, we treat it as a new navigation\n setCurrentUrl(fullUrl);\n\n // Update History Stack\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 setError(null);\n } catch {\n // Ignore invalid paths\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\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: FormEvent<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(browserVariants({ size }), className)}\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-neutral-900 px-4 py-2.5\">\n {/* Navigation Controls */}\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={handleBack}\n disabled={!canGoBack}\n className={cn(\n 'flex h-8 w-8 items-center justify-center rounded-md text-neutral-400 transition-colors',\n canGoBack\n ? 'hover:bg-neutral-800 hover:text-neutral-200'\n : 'cursor-not-allowed opacity-40'\n )}\n aria-label=\"Go back\"\n title=\"Go back\"\n >\n <ArrowLeft size={18} />\n </button>\n\n <button\n type=\"button\"\n onClick={handleForward}\n disabled={!canGoForward}\n className={cn(\n 'flex h-8 w-8 items-center justify-center rounded-md text-neutral-400 transition-colors',\n canGoForward\n ? 'hover:bg-neutral-800 hover:text-neutral-200'\n : 'cursor-not-allowed opacity-40'\n )}\n aria-label=\"Go forward\"\n title=\"Go forward\"\n >\n <ArrowRight size={18} />\n </button>\n </div>\n\n {/* URL Bar */}\n <form onSubmit={handleSubmit} className=\"relative flex-1\" noValidate>\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 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 className={cn(\n 'w-full rounded-lg px-4 py-2 pr-11 text-sm leading-[1.4]',\n 'bg-neutral-950 text-neutral-300',\n 'placeholder:text-neutral-400',\n 'transition-all focus:outline-none focus:ring-1 focus:ring-neutral-400/30',\n showError ? 'border border-red-500' : 'border border-transparent'\n )}\n aria-label={content.urlLabel.value}\n aria-invalid={showError}\n aria-describedby={showError ? 'browser-url-error' : undefined}\n />\n\n {/* Reload Button */}\n <button\n type=\"button\"\n onClick={handleReload}\n className={cn(\n '-translate-y-1/2 absolute top-1/2 right-2',\n 'flex h-8 w-8 items-center justify-center rounded-md',\n 'transition-colors hover:bg-neutral-800',\n 'focus:outline-none focus:ring-1 focus:ring-neutral-400/30'\n )}\n title={content.reloadButtonTitle.value}\n aria-label={content.reloadButtonTitle.value}\n tabIndex={0}\n >\n <RotateCw size={18} className=\"text-neutral-400\" strokeWidth={2} />\n </button>\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 min-h-0 w-full flex-1 overflow-hidden rounded-b-xl bg-background\">\n <iframe\n ref={internalIframeRef}\n src={currentUrl}\n title={content.iframeTitle.value}\n className=\"h-full w-full rounded-b-xl border-0\"\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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"}
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { cn } from "../../utils/cn.mjs";
4
4
  import { MaxHeightSmoother } from "../MaxHeightSmoother/index.mjs";
5
- import { ChevronRight } from "lucide-react";
6
5
  import { useState } from "react";
6
+ import { ChevronRight } from "lucide-react";
7
7
  import { cva } from "class-variance-authority";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
 
@@ -3,8 +3,8 @@
3
3
  import { cn } from "../../utils/cn.mjs";
4
4
  import { InputVariant } from "../Input/Input.mjs";
5
5
  import { AutoSizedTextArea } from "../TextArea/AutoSizeTextArea.mjs";
6
- import { Check, X } from "lucide-react";
7
6
  import { useState } from "react";
7
+ import { Check, X } from "lucide-react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
 
10
10
  //#region src/components/ContentEditor/ContentEditor.tsx
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { Button, ButtonColor, ButtonSize, ButtonVariant } from "../Button/Button.mjs";
4
4
  import { Input, InputVariant } from "../Input/Input.mjs";
5
- import { Check, X } from "lucide-react";
6
5
  import { useEffect, useState } from "react";
6
+ import { Check, X } from "lucide-react";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
8
 
9
9
  //#region src/components/ContentEditor/ContentEditorInput.tsx
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { Button, ButtonColor, ButtonSize, ButtonVariant } from "../Button/Button.mjs";
4
3
  import { useUser } from "../../hooks/useUser/index.mjs";
4
+ import { Button, ButtonColor, ButtonSize, ButtonVariant } from "../Button/Button.mjs";
5
5
  import { AutoCompleteTextarea } from "../TextArea/AutocompleteTextArea.mjs";
6
- import { Check, X } from "lucide-react";
7
6
  import { useEffect, useState } from "react";
7
+ import { Check, X } from "lucide-react";
8
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
9
 
10
10
  //#region src/components/ContentEditor/ContentEditorTextArea.tsx