diff --git a/chrome/src/popup/popup.html b/chrome/src/popup/popup.html index d16a9319..a1937101 100644 --- a/chrome/src/popup/popup.html +++ b/chrome/src/popup/popup.html @@ -173,6 +173,50 @@

Auto Refresh

+
+

Remark Mode

+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+

Cache

diff --git a/src/_locales/be/messages.json b/src/_locales/be/messages.json index 5b386d3d..14e0e04c 100644 --- a/src/_locales/be/messages.json +++ b/src/_locales/be/messages.json @@ -391,6 +391,58 @@ "message": "Скасаваць і выдаліць", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Рэжым анатацый", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Аўтавыдаленне пустых нататак", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Закрыць укладку пасля капіявання", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Колер па змаўчанні", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Гатова", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Памер шрыфту", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Стыль вылучэння", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Непразрыстасць", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Скінуць налады", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Фон", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Рамка", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Падкрэсленне", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Хвалісты", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Змяніць колер", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Націсніце, каб рэдагаваць", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Вылучыце тэкст, каб дадаць нататку", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Вылучыце тэкст, каб дадаць заўвагі", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Рэжым заўваг", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Захаваць", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Налады", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Заўвагі", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/da/messages.json b/src/_locales/da/messages.json index 7c0e6fc1..a6a9000c 100644 --- a/src/_locales/da/messages.json +++ b/src/_locales/da/messages.json @@ -391,6 +391,58 @@ "message": "Annullér og fjern", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Annotationstilstand", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Slet tomme noter automatisk", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Luk fane efter kopiering", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Standardfarve", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Færdig", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Skriftstørrelse", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Fremhævningsstil", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Gennemsigtighed", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Nulstil til standard", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Baggrund", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Ramme", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Understreget", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Bølget", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Skift farve", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Klik for at redigere", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Vælg tekst for at tilføje noter", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Vælg tekst for at tilføje kommentarer", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Kommentar-tilstand", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Gem", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Indstillinger", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Kommentarer", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/de/messages.json b/src/_locales/de/messages.json index f6c5eed1..6ceef0c8 100644 --- a/src/_locales/de/messages.json +++ b/src/_locales/de/messages.json @@ -391,6 +391,58 @@ "message": "Abbrechen und entfernen", "description": "Cancel button in the annotation popup" }, + "remark_cfg_title": { + "message": "Anmerkungsmodus", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Leere Anmerkungen automatisch löschen", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Tab nach dem Kopieren schließen", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Standardfarbe", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Fertig", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Schriftgröße", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Hervorhebungsstil", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Deckkraft", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Auf Standard zurücksetzen", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Hintergrund", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Rahmen", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Unterstrichen", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Wellig", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Farbe ändern", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Klicken zum Bearbeiten", "description": "Tooltip on a remark note in the sidebar indicating it is editable" }, + "remark_empty": { + "message": "Text auswählen, um Anmerkungen hinzuzufügen", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Text auswählen, um Anmerkungen hinzuzufügen", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Anmerkungsmodus", "description": "Toolbar button tooltip to enter Remark Mode" }, + "remark_save": { + "message": "Speichern", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Einstellungen", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Anmerkungen", "description": "Title of the Remark Mode sidebar panel" diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 1ef9d307..0698b41b 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -388,9 +388,61 @@ "description": "Placeholder for the note input in the annotation popup and sidebar" }, "remark_cancel": { - "message": "Cancel & remove", + "message": "Cancel", "description": "Cancel button in the annotation popup" }, + "remark_cfg_title": { + "message": "Remark Mode", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Auto-delete empty remarks", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Close tab after copy", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Default color", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Done", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Font size", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Highlight style", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Opacity", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Reset to defaults", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Background", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Border", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Underline", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Wavy", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Change color", "description": "Tooltip for changing the remark color" @@ -420,11 +472,11 @@ "description": "Feedback shown after remarks are successfully copied" }, "remark_copy_btn": { - "message": "Copy remarks", + "message": "Copy", "description": "Button label to copy all remarks to clipboard and exit Remark Mode" }, "remark_copy_failed": { - "message": "Copy failed", + "message": "Failed", "description": "Feedback shown when copying remarks fails" }, "remark_copy_tooltip": { @@ -443,6 +495,10 @@ "message": "Click to edit", "description": "Tooltip on a remark note in the sidebar indicating it is editable" }, + "remark_empty": { + "message": "Select text to add remarks", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Select text to add remarks", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Remark Mode", "description": "Toolbar button tooltip to enter Remark Mode" }, + "remark_save": { + "message": "Save", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Settings", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Remarks", "description": "Title of the Remark Mode sidebar panel" diff --git a/src/_locales/es/messages.json b/src/_locales/es/messages.json index 80f9cf16..2bb980f7 100644 --- a/src/_locales/es/messages.json +++ b/src/_locales/es/messages.json @@ -391,6 +391,58 @@ "message": "Cancelar y quitar", "description": "Cancel button in the annotation popup" }, + "remark_cfg_title": { + "message": "Modo de anotación", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Eliminar notas vacías automáticamente", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Cerrar pestaña después de copiar", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Color predeterminado", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Listo", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Tamaño de fuente", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Estilo de resaltado", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Opacidad", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Restablecer valores predeterminados", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Fondo", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Borde", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Subrayado", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Ondulado", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Cambiar color", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Haz clic para editar", "description": "Tooltip on a remark note in the sidebar indicating it is editable" }, + "remark_empty": { + "message": "Selecciona texto para añadir notas", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Selecciona texto para añadir observaciones", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Modo de observaciones", "description": "Toolbar button tooltip to enter Remark Mode" }, + "remark_save": { + "message": "Guardar", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Configuración", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Observaciones", "description": "Title of the Remark Mode sidebar panel" diff --git a/src/_locales/et/messages.json b/src/_locales/et/messages.json index 05429322..8844753a 100644 --- a/src/_locales/et/messages.json +++ b/src/_locales/et/messages.json @@ -391,6 +391,58 @@ "message": "Tühista ja eemalda", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Märkuste režiim", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Kustuta tühjad märkmed automaatselt", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Sulge vahekaart pärast kopeerimist", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Vaikevärv", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Valmis", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Kirjasuurus", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Esiletõstmise stiil", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Läbipaistmatus", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Lähtesta vaikeseaded", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Taust", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Raam", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Allajoonitud", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Laineline", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Muuda värvi", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Klõpsa muutmiseks", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Valige tekst märkmete lisamiseks", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Vali tekst märkmete lisamiseks", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Märkmete režiim", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Salvesta", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Seaded", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Märkmed", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/fi/messages.json b/src/_locales/fi/messages.json index 4cf41e4c..f47a4baa 100644 --- a/src/_locales/fi/messages.json +++ b/src/_locales/fi/messages.json @@ -391,6 +391,58 @@ "message": "Peruuta ja poista", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Huomautustila", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Poista tyhjät muistiinpanot automaattisesti", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Sulje välilehti kopioinnin jälkeen", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Oletusväri", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Valmis", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Fonttikoko", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Korostustyyli", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Peittävyys", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Palauta oletukset", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Tausta", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Kehys", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Alleviivaus", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Aaltoileva", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Vaihda väri", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Napsauta muokataksesi", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Valitse tekstiä lisätäksesi muistiinpanoja", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Valitse teksti lisätäksesi huomautuksia", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Huomautustila", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Tallenna", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Asetukset", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Huomautukset", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/fr/messages.json b/src/_locales/fr/messages.json index 4214fe72..49f4472c 100644 --- a/src/_locales/fr/messages.json +++ b/src/_locales/fr/messages.json @@ -391,6 +391,58 @@ "message": "Annuler et supprimer", "description": "Cancel button in the annotation popup" }, + "remark_cfg_title": { + "message": "Mode annotation", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Supprimer automatiquement les remarques vides", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Fermer l'onglet après la copie", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Couleur par défaut", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Terminé", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Taille de police", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Style de surlignage", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Opacité", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Réinitialiser les paramètres", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Arrière-plan", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Bordure", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Souligné", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Ondulé", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Changer la couleur", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Cliquer pour modifier", "description": "Tooltip on a remark note in the sidebar indicating it is editable" }, + "remark_empty": { + "message": "Sélectionnez du texte pour ajouter des remarques", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Sélectionnez du texte pour ajouter des remarques", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Mode remarques", "description": "Toolbar button tooltip to enter Remark Mode" }, + "remark_save": { + "message": "Enregistrer", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Paramètres", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Remarques", "description": "Title of the Remark Mode sidebar panel" diff --git a/src/_locales/hi/messages.json b/src/_locales/hi/messages.json index 6110b091..0aa7fd7f 100644 --- a/src/_locales/hi/messages.json +++ b/src/_locales/hi/messages.json @@ -391,6 +391,58 @@ "message": "रद्द करें और हटाएँ", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "टिप्पणी मोड", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "खाली टिप्पणियाँ स्वचालित रूप से हटाएँ", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "कॉपी करने के बाद टैब बंद करें", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "डिफ़ॉल्ट रंग", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "हो गया", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "फ़ॉन्ट आकार", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "हाइलाइट शैली", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "अपारदर्शिता", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "डिफ़ॉल्ट पर रीसेट करें", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "पृष्ठभूमि", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "बॉर्डर", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "रेखांकित", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "लहरदार", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "रंग बदलें", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "संपादित करने के लिए क्लिक करें", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "टिप्पणी जोड़ने के लिए टेक्स्ट चुनें", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "टिप्पणी जोड़ने के लिए पाठ चुनें", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "टिप्पणी मोड", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "सहेजें", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "सेटिंग्स", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "टिप्पणियाँ", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/id/messages.json b/src/_locales/id/messages.json index 63af1b9a..a0f2409a 100644 --- a/src/_locales/id/messages.json +++ b/src/_locales/id/messages.json @@ -391,6 +391,58 @@ "message": "Batal dan hapus", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Mode anotasi", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Hapus catatan kosong secara otomatis", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Tutup tab setelah menyalin", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Warna default", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Selesai", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Ukuran font", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Gaya sorotan", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Opasitas", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Atur ulang ke default", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Latar belakang", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Bingkai", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Garis bawah", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Bergelombang", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Ubah warna", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Klik untuk mengedit", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Pilih teks untuk menambahkan catatan", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Pilih teks untuk menambahkan catatan", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Mode catatan", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Simpan", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Pengaturan", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Catatan", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/it/messages.json b/src/_locales/it/messages.json index b677d13b..2fea5b13 100644 --- a/src/_locales/it/messages.json +++ b/src/_locales/it/messages.json @@ -391,6 +391,58 @@ "message": "Annulla e rimuovi", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Modalità annotazione", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Elimina automaticamente le note vuote", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Chiudi scheda dopo la copia", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Colore predefinito", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Fatto", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Dimensione carattere", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Stile evidenziazione", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Opacità", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Ripristina impostazioni", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Sfondo", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Bordo", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Sottolineato", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Ondulato", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Cambia colore", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Fai clic per modificare", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Seleziona testo per aggiungere note", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Seleziona il testo per aggiungere annotazioni", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Modalità annotazioni", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Salva", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Impostazioni", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Annotazioni", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/ja/messages.json b/src/_locales/ja/messages.json index 1748e921..114a1fe7 100644 --- a/src/_locales/ja/messages.json +++ b/src/_locales/ja/messages.json @@ -391,6 +391,58 @@ "message": "取り消して削除", "description": "Cancel button in the annotation popup" }, + "remark_cfg_title": { + "message": "注釈モード", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "空のメモを自動削除", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "コピー後にタブを閉じる", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "デフォルトカラー", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "完了", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "フォントサイズ", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "ハイライトスタイル", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "不透明度", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "デフォルトに戻す", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "背景", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "枠線", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "下線", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "波線", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "色を変更", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "クリックして編集", "description": "Tooltip on a remark note in the sidebar indicating it is editable" }, + "remark_empty": { + "message": "テキストを選択してメモを追加", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "テキストを選択して注釈を追加", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "注釈モード", "description": "Toolbar button tooltip to enter Remark Mode" }, + "remark_save": { + "message": "保存", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "設定", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "注釈", "description": "Title of the Remark Mode sidebar panel" diff --git a/src/_locales/ko/messages.json b/src/_locales/ko/messages.json index 082a2848..0b2b38d9 100644 --- a/src/_locales/ko/messages.json +++ b/src/_locales/ko/messages.json @@ -391,6 +391,58 @@ "message": "취소하고 제거", "description": "Cancel button in the annotation popup" }, + "remark_cfg_title": { + "message": "주석 모드", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "빈 메모 자동 삭제", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "복사 후 탭 닫기", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "기본 색상", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "완료", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "글꼴 크기", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "강조 스타일", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "불투명도", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "기본값으로 재설정", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "배경", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "테두리", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "밑줄", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "물결", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "색상 변경", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "클릭하여 편집", "description": "Tooltip on a remark note in the sidebar indicating it is editable" }, + "remark_empty": { + "message": "텍스트를 선택하여 메모 추가", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "텍스트를 선택해 주석 추가", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "주석 모드", "description": "Toolbar button tooltip to enter Remark Mode" }, + "remark_save": { + "message": "저장", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "설정", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "주석", "description": "Title of the Remark Mode sidebar panel" diff --git a/src/_locales/lt/messages.json b/src/_locales/lt/messages.json index dd4cb56d..7a87c848 100644 --- a/src/_locales/lt/messages.json +++ b/src/_locales/lt/messages.json @@ -391,6 +391,58 @@ "message": "Atšaukti ir pašalinti", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Anotacijų režimas", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Automatiškai trinti tuščias pastabas", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Uždaryti skirtuką po kopijavimo", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Numatytoji spalva", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Atlikta", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Šrifto dydis", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Paryškinimo stilius", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Nepermatomumas", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Atkurti numatytuosius", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Fonas", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Rėmelis", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Pabraukimas", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Banguotas", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Keisti spalvą", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Spustelėkite, kad redaguotumėte", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Pažymėkite tekstą pastaboms pridėti", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Pasirinkite tekstą pastaboms pridėti", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Pastabų režimas", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Išsaugoti", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Nustatymai", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Pastabos", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/ms/messages.json b/src/_locales/ms/messages.json index 41d29a8c..b003d18e 100644 --- a/src/_locales/ms/messages.json +++ b/src/_locales/ms/messages.json @@ -391,6 +391,58 @@ "message": "Batal dan buang", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Mod anotasi", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Padam nota kosong secara automatik", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Tutup tab selepas menyalin", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Warna lalai", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Selesai", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Saiz fon", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Gaya serlahan", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Kelegapan", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Tetapkan semula ke lalai", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Latar belakang", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Bingkai", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Garis bawah", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Berombak", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Tukar warna", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Klik untuk mengedit", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Pilih teks untuk menambah nota", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Pilih teks untuk menambah catatan", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Mod catatan", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Simpan", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Tetapan", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Catatan", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/nl/messages.json b/src/_locales/nl/messages.json index eb774cdc..68ace256 100644 --- a/src/_locales/nl/messages.json +++ b/src/_locales/nl/messages.json @@ -391,6 +391,58 @@ "message": "Annuleren en verwijderen", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Annotatiemodus", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Lege opmerkingen automatisch verwijderen", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Tabblad sluiten na kopiëren", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Standaardkleur", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Klaar", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Lettergrootte", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Markeerstijl", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Dekking", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Standaardwaarden herstellen", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Achtergrond", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Kader", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Onderstreept", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Golvend", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Kleur wijzigen", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Klik om te bewerken", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Selecteer tekst om opmerkingen toe te voegen", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Selecteer tekst om opmerkingen toe te voegen", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Opmerkingsmodus", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Opslaan", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Instellingen", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Opmerkingen", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/no/messages.json b/src/_locales/no/messages.json index 2f7f2ed1..7b58310d 100644 --- a/src/_locales/no/messages.json +++ b/src/_locales/no/messages.json @@ -391,6 +391,58 @@ "message": "Avbryt og fjern", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Merknadsmodus", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Slett tomme notater automatisk", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Lukk fane etter kopiering", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Standardfarge", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Ferdig", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Skriftstørrelse", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Uthevingsstil", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Gjennomsiktighet", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Tilbakestill til standard", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Bakgrunn", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Ramme", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Understreket", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Bølget", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Endre farge", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Klikk for å redigere", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Velg tekst for å legge til notater", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Velg tekst for å legge til merknader", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Merknadsmodus", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Lagre", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Innstillinger", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Merknader", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/pl/messages.json b/src/_locales/pl/messages.json index 41c3d57b..df947960 100644 --- a/src/_locales/pl/messages.json +++ b/src/_locales/pl/messages.json @@ -391,6 +391,58 @@ "message": "Anuluj i usuń", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Tryb adnotacji", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Automatycznie usuwaj puste notatki", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Zamknij kartę po skopiowaniu", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Domyślny kolor", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Gotowe", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Rozmiar czcionki", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Styl wyróżnienia", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Krycie", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Przywróć domyślne", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Tło", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Ramka", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Podkreślenie", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Falisty", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Zmień kolor", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Kliknij, aby edytować", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Zaznacz tekst, aby dodać notatkę", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Zaznacz tekst, aby dodać uwagi", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Tryb uwag", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Zapisz", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Ustawienia", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Uwagi", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/pt_BR/messages.json b/src/_locales/pt_BR/messages.json index fa958ba4..be623d64 100644 --- a/src/_locales/pt_BR/messages.json +++ b/src/_locales/pt_BR/messages.json @@ -391,6 +391,58 @@ "message": "Cancelar e remover", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Modo de anotação", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Excluir notas vazias automaticamente", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Fechar aba após copiar", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Cor padrão", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Concluído", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Tamanho da fonte", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Estilo de destaque", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Opacidade", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Restaurar padrões", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Fundo", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Borda", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Sublinhado", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Ondulado", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Alterar cor", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Clique para editar", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Selecione texto para adicionar notas", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Selecione um texto para adicionar observações", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Modo de observações", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Salvar", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Configurações", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Observações", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/pt_PT/messages.json b/src/_locales/pt_PT/messages.json index 77049b7c..cfce6346 100644 --- a/src/_locales/pt_PT/messages.json +++ b/src/_locales/pt_PT/messages.json @@ -391,6 +391,58 @@ "message": "Cancelar e remover", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Modo de anotação", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Eliminar notas vazias automaticamente", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Fechar separador após copiar", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Cor predefinida", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Concluído", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Tamanho da letra", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Estilo de destaque", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Opacidade", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Repor predefinições", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Fundo", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Moldura", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Sublinhado", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Ondulado", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Alterar cor", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Clique para editar", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Selecione texto para adicionar notas", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Selecione texto para adicionar observações", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Modo de observações", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Guardar", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Definições", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Observações", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/ru/messages.json b/src/_locales/ru/messages.json index 9fb2a64c..f212458e 100644 --- a/src/_locales/ru/messages.json +++ b/src/_locales/ru/messages.json @@ -391,6 +391,58 @@ "message": "Отменить и удалить", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Режим аннотаций", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Автоудаление пустых заметок", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Закрыть вкладку после копирования", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Цвет по умолчанию", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Готово", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Размер шрифта", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Стиль выделения", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Непрозрачность", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Сбросить настройки", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Фон", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Рамка", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Подчёркивание", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Волнистый", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Изменить цвет", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Нажмите для редактирования", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Выделите текст для добавления заметок", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Выделите текст, чтобы добавить заметки", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Режим заметок", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Сохранить", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Настройки", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Заметки", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/sv/messages.json b/src/_locales/sv/messages.json index 5c3ba5c9..21f0aee3 100644 --- a/src/_locales/sv/messages.json +++ b/src/_locales/sv/messages.json @@ -391,6 +391,58 @@ "message": "Avbryt och ta bort", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Anteckningsläge", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Ta bort tomma anteckningar automatiskt", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Stäng flik efter kopiering", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Standardfärg", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Klar", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Teckenstorlek", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Markeringsstil", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Opacitet", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Återställ standardvärden", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Bakgrund", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Ram", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Understruken", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Vågig", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Ändra färg", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Klicka för att redigera", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Markera text för att lägga till anteckningar", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Markera text för att lägga till kommentarer", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Kommentarsläge", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Spara", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Inställningar", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Kommentarer", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/th/messages.json b/src/_locales/th/messages.json index 7a88e657..206142e8 100644 --- a/src/_locales/th/messages.json +++ b/src/_locales/th/messages.json @@ -391,6 +391,58 @@ "message": "ยกเลิกและลบ", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "โหมดคำอธิบาย", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "ลบโน้ตว่างโดยอัตโนมัติ", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "ปิดแท็บหลังจากคัดลอก", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "สีเริ่มต้น", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "เสร็จสิ้น", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "ขนาดตัวอักษร", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "รูปแบบไฮไลต์", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "ความทึบ", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "รีเซ็ตเป็นค่าเริ่มต้น", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "พื้นหลัง", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "กรอบ", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "ขีดเส้นใต้", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "หยัก", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "เปลี่ยนสี", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "คลิกเพื่อแก้ไข", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "เลือกข้อความเพื่อเพิ่มโน้ต", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "เลือกข้อความเพื่อเพิ่มหมายเหตุ", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "โหมดหมายเหตุ", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "บันทึก", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "การตั้งค่า", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "หมายเหตุ", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/tr/messages.json b/src/_locales/tr/messages.json index 56127410..8c290f77 100644 --- a/src/_locales/tr/messages.json +++ b/src/_locales/tr/messages.json @@ -391,6 +391,58 @@ "message": "İptal et ve kaldır", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Not modu", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Boş notları otomatik sil", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Kopyaladıktan sonra sekmeyi kapat", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Varsayılan renk", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Tamam", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Yazı tipi boyutu", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Vurgulama stili", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Saydamlık", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Varsayılanlara sıfırla", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Arka plan", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Kenarlık", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Alt çizgi", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Dalgalı", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Rengi değiştir", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Düzenlemek için tıkla", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Not eklemek için metin seçin", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Not eklemek için metin seç", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Not modu", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Kaydet", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Ayarlar", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Notlar", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/uk/messages.json b/src/_locales/uk/messages.json index 0fbc56a6..22181d8d 100644 --- a/src/_locales/uk/messages.json +++ b/src/_locales/uk/messages.json @@ -391,6 +391,58 @@ "message": "Скасувати й видалити", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Режим анотацій", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Автовидалення порожніх нотаток", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Закрити вкладку після копіювання", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Колір за замовчуванням", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Готово", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Розмір шрифту", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Стиль виділення", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Непрозорість", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Скинути налаштування", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Фон", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Рамка", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Підкреслення", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Хвилястий", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Змінити колір", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Натисніть, щоб редагувати", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Виділіть текст, щоб додати нотатку", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Виділіть текст, щоб додати примітки", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Режим приміток", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Зберегти", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Налаштування", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Примітки", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/vi/messages.json b/src/_locales/vi/messages.json index 16a1cade..755b66ca 100644 --- a/src/_locales/vi/messages.json +++ b/src/_locales/vi/messages.json @@ -391,6 +391,58 @@ "message": "Hủy và xóa", "description": "Button text for cancelling a new remark and removing the temporary mark" }, + "remark_cfg_title": { + "message": "Chế độ chú thích", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "Tự động xóa ghi chú trống", + "description": "Config: auto-delete annotations with no note" + }, + "remark_cfg_close_after_copy": { + "message": "Đóng tab sau khi sao chép", + "description": "Config: close the file tab after copying remarks" + }, + "remark_cfg_default_color": { + "message": "Màu mặc định", + "description": "Config: default annotation color" + }, + "remark_cfg_done": { + "message": "Xong", + "description": "Button to close config panel" + }, + "remark_cfg_font_size": { + "message": "Cỡ chữ", + "description": "Config: sidebar font size" + }, + "remark_cfg_highlight_style": { + "message": "Kiểu đánh dấu", + "description": "Config: how text is visually marked" + }, + "remark_cfg_opacity": { + "message": "Độ mờ", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "Đặt lại mặc định", + "description": "Button to reset all config to defaults" + }, + "remark_cfg_style_bg": { + "message": "Nền", + "description": "Highlight style: colored background" + }, + "remark_cfg_style_border": { + "message": "Viền", + "description": "Highlight style: bottom border" + }, + "remark_cfg_style_underline": { + "message": "Gạch chân", + "description": "Highlight style: solid underline" + }, + "remark_cfg_style_wavy": { + "message": "Lượn sóng", + "description": "Highlight style: wavy underline" + }, "remark_change_color": { "message": "Đổi màu", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "Nhấp để chỉnh sửa", "description": "Tooltip for editing an existing remark note" }, + "remark_empty": { + "message": "Chọn văn bản để thêm ghi chú", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "Chọn văn bản để thêm ghi chú", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "Chế độ ghi chú", "description": "Toolbar button title for entering remark mode" }, + "remark_save": { + "message": "Lưu", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "Cài đặt", + "description": "Settings button tooltip" + }, "remark_sidebar_title": { "message": "Ghi chú", "description": "Sidebar title for the remarks panel" diff --git a/src/_locales/zh_CN/messages.json b/src/_locales/zh_CN/messages.json index 2dc1ea30..c48d8203 100644 --- a/src/_locales/zh_CN/messages.json +++ b/src/_locales/zh_CN/messages.json @@ -388,9 +388,61 @@ "description": "Placeholder for the note input in the annotation popup and sidebar" }, "remark_cancel": { - "message": "取消并移除", + "message": "取消", "description": "Cancel button in the annotation popup" }, + "remark_cfg_title": { + "message": "批注模式", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "自动删除空批注", + "description": "" + }, + "remark_cfg_close_after_copy": { + "message": "复制后关闭标签页", + "description": "" + }, + "remark_cfg_default_color": { + "message": "默认颜色", + "description": "" + }, + "remark_cfg_done": { + "message": "完成", + "description": "" + }, + "remark_cfg_font_size": { + "message": "字体大小", + "description": "" + }, + "remark_cfg_highlight_style": { + "message": "高亮样式", + "description": "" + }, + "remark_cfg_opacity": { + "message": "Opacity", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "恢复默认设置", + "description": "" + }, + "remark_cfg_style_bg": { + "message": "背景色", + "description": "" + }, + "remark_cfg_style_border": { + "message": "底部边框", + "description": "" + }, + "remark_cfg_style_underline": { + "message": "下划线", + "description": "" + }, + "remark_cfg_style_wavy": { + "message": "波浪线", + "description": "" + }, "remark_change_color": { "message": "更改颜色", "description": "Tooltip for changing the remark color" @@ -443,6 +495,10 @@ "message": "点击编辑", "description": "Tooltip on a remark note in the sidebar indicating it is editable" }, + "remark_empty": { + "message": "选中文字以添加批注", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "选择文本以添加批注", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "批注模式", "description": "Toolbar button tooltip to enter Remark Mode" }, + "remark_save": { + "message": "保存", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "设置", + "description": "" + }, "remark_sidebar_title": { "message": "批注", "description": "Title of the Remark Mode sidebar panel" diff --git a/src/_locales/zh_TW/messages.json b/src/_locales/zh_TW/messages.json index f23eca9d..304042bd 100644 --- a/src/_locales/zh_TW/messages.json +++ b/src/_locales/zh_TW/messages.json @@ -388,9 +388,61 @@ "description": "Placeholder for the note input in the annotation popup and sidebar" }, "remark_cancel": { - "message": "取消並移除", + "message": "取消", "description": "Cancel button in the annotation popup" }, + "remark_cfg_title": { + "message": "批注模式", + "description": "Title for remark mode settings card" + }, + "remark_cfg_auto_delete": { + "message": "自動刪除空批註", + "description": "" + }, + "remark_cfg_close_after_copy": { + "message": "複製後關閉標籤頁", + "description": "" + }, + "remark_cfg_default_color": { + "message": "預設顏色", + "description": "" + }, + "remark_cfg_done": { + "message": "完成", + "description": "" + }, + "remark_cfg_font_size": { + "message": "字體大小", + "description": "" + }, + "remark_cfg_highlight_style": { + "message": "標記樣式", + "description": "" + }, + "remark_cfg_opacity": { + "message": "Opacity", + "description": "Config: highlight opacity" + }, + "remark_cfg_reset": { + "message": "恢復預設設定", + "description": "" + }, + "remark_cfg_style_bg": { + "message": "背景色", + "description": "" + }, + "remark_cfg_style_border": { + "message": "底部邊框", + "description": "" + }, + "remark_cfg_style_underline": { + "message": "底線", + "description": "" + }, + "remark_cfg_style_wavy": { + "message": "波浪線", + "description": "" + }, "remark_change_color": { "message": "更改顏色", "description": "Tooltip for changing the remark color" @@ -420,7 +472,7 @@ "description": "Feedback shown after remarks are successfully copied" }, "remark_copy_btn": { - "message": "複製批註", + "message": "複製批注", "description": "Button label to copy all remarks to clipboard and exit Remark Mode" }, "remark_copy_failed": { @@ -443,6 +495,10 @@ "message": "點擊以編輯", "description": "Tooltip on a remark note in the sidebar indicating it is editable" }, + "remark_empty": { + "message": "選取文字以新增批注", + "description": "Placeholder shown in sidebar when no remarks exist" + }, "remark_empty_hint": { "message": "選取文字以新增批註", "description": "Empty state hint in the remarks sidebar" @@ -463,6 +519,14 @@ "message": "批註模式", "description": "Toolbar button tooltip to enter Remark Mode" }, + "remark_save": { + "message": "儲存", + "description": "Save button in the annotation popup" + }, + "remark_settings": { + "message": "設定", + "description": "" + }, "remark_sidebar_title": { "message": "批註", "description": "Title of the Remark Mode sidebar panel" diff --git a/src/ui/popup/settings-tab.ts b/src/ui/popup/settings-tab.ts index 80570bcf..15d1bccb 100644 --- a/src/ui/popup/settings-tab.ts +++ b/src/ui/popup/settings-tab.ts @@ -342,6 +342,9 @@ export function createSettingsTabManager({ // Auto Refresh settings (Chrome only) loadAutoRefreshSettingsUI(); + // Remark Mode settings + loadRemarkSettingsUI(); + } async function loadLocalesIntoSelect(localeSelect: HTMLSelectElement): Promise { @@ -598,6 +601,57 @@ export function createSettingsTabManager({ } } + /** + * Remark Mode settings card + */ + function loadRemarkSettingsUI(): void { + const REMARK_STORAGE_KEY = 'remarkConfig'; + const autoDeleteEl = document.getElementById('remark-auto-delete-empty') as HTMLInputElement | null; + const closeAfterCopyEl = document.getElementById('remark-close-after-copy') as HTMLInputElement | null; + const highlightStyleEl = document.getElementById('remark-highlight-style') as HTMLSelectElement | null; + const defaultColorEl = document.getElementById('remark-default-color') as HTMLSelectElement | null; + const fontSizeEl = document.getElementById('remark-font-size') as HTMLSelectElement | null; + + if (!autoDeleteEl || !closeAfterCopyEl || !highlightStyleEl || !defaultColorEl || !fontSizeEl) { + return; + } + + // Load current remark config from storage + storageGet([REMARK_STORAGE_KEY]).then((result) => { + const cfg = (result[REMARK_STORAGE_KEY] || {}) as Record; + autoDeleteEl.checked = cfg.autoDeleteEmpty !== false; + closeAfterCopyEl.checked = cfg.closeAfterCopy === true; + highlightStyleEl.value = (cfg.highlightStyle as string) || 'background'; + defaultColorEl.value = (cfg.defaultColor as string) || 'yellow'; + fontSizeEl.value = String(cfg.fontSize || 13); + }).catch(() => { /* use defaults already in HTML */ }); + + function saveRemarkConfig(): void { + const cfg = { + autoDeleteEmpty: autoDeleteEl!.checked, + autoDeleteDelay: 3000, + closeAfterCopy: closeAfterCopyEl!.checked, + highlightStyle: highlightStyleEl!.value, + defaultColor: defaultColorEl!.value, + fontSize: parseInt(fontSizeEl!.value, 10), + }; + storageSet({ [REMARK_STORAGE_KEY]: cfg }).then(() => { + showMessage(translate('settings_save_success'), 'success'); + }).catch(() => { + showMessage(translate('settings_save_failed'), 'error'); + }); + } + + // Wire change listeners + const elements = [autoDeleteEl, closeAfterCopyEl, highlightStyleEl, defaultColorEl, fontSizeEl]; + elements.forEach((el) => { + if (!el.dataset.listenerAdded) { + el.dataset.listenerAdded = 'true'; + el.addEventListener('change', saveRemarkConfig); + } + }); + } + /** * Save settings to storage (internal helper) */ diff --git a/src/ui/remark-mode.ts b/src/ui/remark-mode.ts index 472ee0ab..057beaee 100644 --- a/src/ui/remark-mode.ts +++ b/src/ui/remark-mode.ts @@ -5,8 +5,9 @@ import { truncate, formatLineRef, getBlockRange, rangesOverlap, isMediaBlock, formatExportText, findTrLineInBlock, findLiLineInBlock, findCodeLineInBlock, narrowLineInBlock, + generateHighlightCSS, findSentenceBounds, SENTENCE_END_RE, COLOR_MAP, COLOR_LABELS, SKIP_ANNOTATION_TAGS, - type RemarkColor, type RemarkAnnotation, + type RemarkColor, type RemarkAnnotation, type HighlightStyle, } from './remark-utils'; /** @@ -87,16 +88,84 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll return translated; } + // ─── Config (persisted via chrome.storage.local) ──────────────────────────── + const CONFIG_STORAGE_KEY = 'remarkConfig'; + const CONFIG_DEFAULTS = { + autoDeleteEmpty: true, + autoDeleteDelay: 3000, // ms wait after blur before auto-delete sequence starts + closeAfterCopy: false, // close file/tab after export + highlightStyle: 'background' as 'background' | 'underline' | 'wavy' | 'border', + defaultColor: 'yellow' as RemarkColor, + fontSize: 13, // sidebar font size 12-16 + }; + type RemarkConfig = typeof CONFIG_DEFAULTS; + const config: RemarkConfig = { ...CONFIG_DEFAULTS }; + + async function loadConfig(): Promise { + try { + if (typeof chrome !== 'undefined' && chrome.storage?.local) { + const data = await chrome.storage.local.get(CONFIG_STORAGE_KEY); + if (data[CONFIG_STORAGE_KEY]) { + Object.assign(config, data[CONFIG_STORAGE_KEY]); + } + } else { + const stored = localStorage.getItem(CONFIG_STORAGE_KEY); + if (stored) Object.assign(config, JSON.parse(stored)); + } + } catch { /* storage unavailable — use defaults */ } + } + + function saveConfig(): void { + try { + if (typeof chrome !== 'undefined' && chrome.storage?.local) { + void chrome.storage.local.set({ [CONFIG_STORAGE_KEY]: { ...config } }); + } else { + localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(config)); + } + } catch { /* ignore */ } + } + + function resetConfig(): void { + Object.assign(config, CONFIG_DEFAULTS); + saveConfig(); + } + + // Listen for config changes from popup settings page + if (typeof chrome !== 'undefined' && chrome.storage?.onChanged) { + chrome.storage.onChanged.addListener((changes, area) => { + if (area === 'local' && changes[CONFIG_STORAGE_KEY]?.newValue) { + Object.assign(config, changes[CONFIG_STORAGE_KEY].newValue); + applyConfigStyles(); + if (active) renderHighlights(); + } + }); + } + + /** Apply config-driven CSS variables to sidebar + highlights */ + function applyConfigStyles(): void { + if (sidebarEl) { + sidebarEl.style.setProperty('--remark-font-size', `${config.fontSize}px`); + } + // Dynamic highlight style sheet + let dynStyle = document.getElementById('remark-dynamic-styles') as HTMLStyleElement; + if (!dynStyle) { + dynStyle = document.createElement('style'); + dynStyle.id = 'remark-dynamic-styles'; + document.head.appendChild(dynStyle); + } + dynStyle.textContent = generateHighlightCSS(config.highlightStyle as HighlightStyle); + } + let active = false; let annotations: RemarkAnnotation[] = []; let softDeletedIds: Set = new Set(); // IDs in 5s undo window — excluded from export let abortController: AbortController | null = null; - let popupEl: HTMLElement | null = null; let sidebarEl: HTMLElement | null = null; let tooltipEl: HTMLElement | null = null; let pendingFocusId: string | null = null; // for focus chain across re-renders let sidebarHideCleanupTimer: ReturnType | null = null; let sidebarHideCleanupToken = 0; + const autoDeleteTimers = new Map>(); function cancelPendingSidebarCleanup(): void { sidebarHideCleanupToken += 1; @@ -137,11 +206,19 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll container.addEventListener('mouseout', handleHoverOut, { signal }); } + // Clamp table selection to single cell — prevents cross-cell selection visually + document.addEventListener('selectionchange', clampTableSelection, { signal }); + document.body.classList.add('remark-panel-open'); injectStyles(); - renderHighlights(); - showSidebar(); + + // Load config before rendering so sidebar reflects persisted values + void loadConfig().then(() => { + if (!active) return; // exited during async load + renderHighlights(); + showSidebar(); + }); // Always schedule to catch streamed/async blocks that appear after enter(). // Handles: container not yet in DOM, [data-line] not yet rendered, and @@ -161,19 +238,18 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll if (undoTimer) { clearTimeout(undoTimer); undoTimer = null; } if (undoQueue.length) { commitPendingDeletes(); } - hidePopup(); hideTooltip(); onModeChange?.(false); // toolbar state changes immediately - // Choreographed exit: fade highlights first, then slide sidebar + // Choreographed exit: fade marks, then clear const container = getContainer(); if (container) { container.classList.remove('remark-mode-active'); container.classList.add('remark-exiting'); - // Highlights fade via CSS transition (120ms) + // Marks fade via CSS transition (120ms) setTimeout(() => { container.classList.remove('remark-exiting'); - clearHighlights(); + clearMarks(); }, 160); } hideSidebar(); @@ -199,10 +275,12 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll async function saveAnnotations(): Promise { try { const key = storageKey(); + // Exclude soft-deleted annotations (in undo window) from persistence + const toSave = annotations.filter(a => !softDeletedIds.has(a.id)); if (typeof chrome !== 'undefined' && chrome.storage?.local) { - await chrome.storage.local.set({ [key]: annotations }); + await chrome.storage.local.set({ [key]: toSave }); } else { - localStorage.setItem(key, JSON.stringify(annotations)); + localStorage.setItem(key, JSON.stringify(toSave)); } } catch { // Silently fail — annotations remain in-memory @@ -304,18 +382,54 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll // ─── Selection Handling ────────────────────────────────────────────────── + /** Clamp selection to stay within a single table cell */ + function clampTableSelection(): void { + const sel = window.getSelection(); + if (!sel || sel.isCollapsed || !sel.rangeCount) return; + const range = sel.getRangeAt(0); + const startEl = range.startContainer instanceof HTMLElement + ? range.startContainer : range.startContainer.parentElement; + const startCell = startEl?.closest('td, th'); + if (!startCell) return; // Not in a table — no clamping needed + const endEl = range.endContainer instanceof HTMLElement + ? range.endContainer : range.endContainer.parentElement; + const endCell = endEl?.closest('td, th'); + if (endCell === startCell) return; // Within same cell — OK + // Selection crossed cell boundary — clamp to starting cell + const clamped = document.createRange(); + clamped.setStart(range.startContainer, range.startOffset); + clamped.setEnd(startCell, startCell.childNodes.length); + sel.removeAllRanges(); + sel.addRange(clamped); + } + function handleSelection(): void { const sel = window.getSelection(); - if (!sel || sel.isCollapsed || !sel.rangeCount) { - return; - } + if (!sel || !sel.rangeCount) return; - const range = sel.getRangeAt(0); const container = getContainer(); - if (!container || !container.contains(range.commonAncestorContainer)) { + if (!container) return; + + // Click-to-annotate: collapsed selection = click without drag + if (sel.isCollapsed) { + handleClickToAnnotate(sel, container); return; } + let range = sel.getRangeAt(0); + if (!container.contains(range.commonAncestorContainer)) return; + + // Table guard: if selection somehow crosses cell boundary, ignore + const startNode = range.startContainer; + const startEl = startNode instanceof HTMLElement ? startNode : startNode.parentElement; + if (startEl?.closest('td, th')) { + const endNode = range.endContainer; + const endEl = endNode instanceof HTMLElement ? endNode : endNode.parentElement; + const startCell = startEl.closest('td, th'); + const endCell = endEl?.closest('td, th'); + if (startCell !== endCell) return; // Cross-cell — should not happen due to clamp + } + const selectedText = sel.toString().trim(); if (!selectedText) return; @@ -325,7 +439,78 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll // Skip media blocks (images, charts, diagrams) if (startBlock && isMediaBlock(startBlock)) return; - showPopup(range, selectedText, startLine, endLine, blockId, startBlock ?? undefined); + // Deduplicate: if same text already annotated in same block, scroll to it + const existing = annotations.find(a => + a.selectedText === selectedText && a.startLine === startLine && !softDeletedIds.has(a.id) + ); + if (existing) { + sel.removeAllRanges(); + onMarkClick(existing.id); + return; + } + + // Create annotation with precise mark from live Range + createAndFocusSidebar(selectedText, startLine, endLine, range, blockId); + sel.removeAllRanges(); + } + + function handleClickToAnnotate(sel: Selection, container: HTMLElement): void { + const anchor = sel.anchorNode; + if (!anchor || !container.contains(anchor)) return; + + // Don't trigger on mark clicks (handled by onMarkClick) + const el = anchor instanceof HTMLElement ? anchor : anchor.parentElement; + if (el?.closest('mark.remark-ann')) return; + + // Find block + const block = findBlockAncestor(anchor, container); + if (!block || isMediaBlock(block)) return; + + // For table clicks, narrow to the clicked cell + const cell = el?.closest('td, th') as HTMLElement | null; + const scope = cell || block; + + const fullText = scope.textContent || ''; + if (!fullText.trim()) return; + + // Find the sentence around the click offset + const offset = sel.anchorOffset; + // Walk text nodes to find global offset in scope + let globalOffset = 0; + const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT); + let found = false; + while (walker.nextNode()) { + if (walker.currentNode === anchor) { + globalOffset += offset; + found = true; + break; + } + globalOffset += (walker.currentNode as Text).textContent!.length; + } + if (!found) globalOffset = 0; + + // Find sentence boundaries + const bounds = findSentenceBounds(fullText, globalOffset); + + const sentenceText = fullText.slice(bounds.start, bounds.end).trim(); + if (!sentenceText) return; + + const startLine = Number(block.getAttribute('data-line')) || 0; + const lineCount = Number(block.getAttribute('data-line-count')) || 1; + const endLine = startLine + lineCount - 1; + + // Deduplicate + const existing = annotations.find(a => + a.selectedText === sentenceText && a.startLine === startLine && !softDeletedIds.has(a.id) + ); + if (existing) { onMarkClick(existing.id); return; } + + // Create range for the sentence text + const range = findTextRange(scope, sentenceText); + if (!range) return; + + createAndFocusSidebar(sentenceText, startLine, endLine, range, + block.getAttribute('data-block-id') || undefined); } @@ -383,25 +568,27 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll // ─── Hover Tooltip ───────────────────────────────────────────────────────── function handleHover(e: Event): void { - const target = (e.target as HTMLElement).closest?.('[data-line][data-block-id]') as HTMLElement | null; - if (!target || !target.classList.contains('remark-highlighted')) return; - if (isMediaBlock(target)) return; - - const { start: blockLine, end: blockEnd } = getBlockRange(target); + // Show tooltip when hovering over a marked annotation + const mark = (e.target as HTMLElement).closest?.('mark.remark-ann') as HTMLElement | null; + if (!mark) return; + const annId = mark.dataset.annId; + if (!annId) return; - // Find annotations for this block - const blockAnns = annotations.filter(a => rangesOverlap(a.startLine, a.endLine, blockLine, blockEnd)); - if (blockAnns.length === 0) return; + const ann = annotations.find(a => a.id === annId); + if (!ann || !ann.note) return; // Only show tooltip if there's a note - showTooltip(target, blockAnns); + showTooltip(mark, [ann]); } function handleHoverOut(e: Event): void { - const target = (e.target as HTMLElement).closest?.('[data-line][data-block-id]') as HTMLElement | null; - if (!target) return; - // Only hide if moving away from a highlighted block + const mark = (e.target as HTMLElement).closest?.('mark.remark-ann') as HTMLElement | null; + if (!mark) { + // Also handle moving away from tooltip itself + const tooltip = (e.target as HTMLElement).closest?.('.remark-tooltip'); + if (!tooltip) return; + } const related = (e as MouseEvent).relatedTarget as HTMLElement | null; - if (related && (related.closest?.('.remark-tooltip') || related.closest?.('[data-line][data-block-id].remark-highlighted'))) { + if (related && (related.closest?.('.remark-tooltip') || related.closest?.('mark.remark-ann'))) { return; } hideTooltip(); @@ -442,162 +629,235 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll } } - // ─── Popup ───────────────────────────────────────────────────────────────── + // ─── Precise Mark Highlighting ─────────────────────────────────────────────── + + /** + * Find text within a DOM subtree and return a Range spanning it. + * Skips existing elements to avoid double-wrapping. + */ + function findTextRange(root: Element, text: string): Range | null { + const full = root.textContent || ''; + const idx = full.indexOf(text); + if (idx === -1) return null; + + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + let charCount = 0; + let startNode: Text | null = null; + let startOffset = 0; + let endNode: Text | null = null; + let endOffset = 0; + let node: Node | null; + + while ((node = walker.nextNode())) { + // Skip text inside existing marks + if ((node as Text).parentElement?.closest('mark.remark-ann')) continue; + const len = (node as Text).textContent!.length; + if (!startNode && charCount + len > idx) { + startNode = node as Text; + startOffset = idx - charCount; + } + if (charCount + len >= idx + text.length) { + endNode = node as Text; + endOffset = idx + text.length - charCount; + break; + } + charCount += len; + } - function showPopup(range: Range, selectedText: string, startLine: number, endLine: number, blockId?: string, targetBlock?: HTMLElement): void { - hidePopup(); + if (!startNode || !endNode) return null; + try { + const range = document.createRange(); + range.setStart(startNode, startOffset); + range.setEnd(endNode, endOffset); + return range; + } catch { return null; } + } - // Highlight the block being annotated - if (targetBlock) { - targetBlock.classList.add('remark-popup-target'); + /** + * Collect text nodes within a Range, splitting at boundaries. + * Skips table-structural whitespace nodes (children of tr/tbody/thead/table). + */ + function getTextNodesInRange(range: Range): Array<{ node: Text; start: number; end: number }> { + const nodes: Array<{ node: Text; start: number; end: number }> = []; + const root = range.commonAncestorContainer; + const walker = document.createTreeWalker( + root.nodeType === Node.TEXT_NODE ? root.parentNode! : root, + NodeFilter.SHOW_TEXT + ); + const TABLE_STRUCTURAL = new Set(['TR', 'TBODY', 'THEAD', 'TFOOT', 'TABLE']); + let node: Node | null; + while ((node = walker.nextNode())) { + if ((node as Text).parentElement?.closest('mark.remark-ann')) continue; + if (!range.intersectsNode(node)) continue; + // Skip whitespace-only text nodes that are direct children of table structural elements + const parentTag = (node as Text).parentElement?.tagName; + if (parentTag && TABLE_STRUCTURAL.has(parentTag) && !(node as Text).textContent?.trim()) continue; + const start = node === range.startContainer ? range.startOffset : 0; + const end = node === range.endContainer ? range.endOffset : (node as Text).textContent!.length; + if (start < end) nodes.push({ node: node as Text, start, end }); } + return nodes; + } - // Create annotation immediately with default color - const annId = generateId(); - const ann: RemarkAnnotation = { - id: annId, startLine, endLine, selectedText, - note: '', color: 'yellow', timestamp: Date.now(), blockId, - }; - annotations.push(ann); - renderHighlights(); - renderSidebarContent(); - notifyCount(); - void saveAnnotations(); + const BLOCK_TAGS = new Set(['LI', 'TD', 'TH', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DIV', 'BLOCKQUOTE', 'PRE']); - const rect = range.getBoundingClientRect(); - popupEl = document.createElement('div'); - popupEl.className = 'remark-popup'; - popupEl.innerHTML = buildPopupHTML(selectedText); + /** Check if a Range spans across block-level element boundaries (e.g., multiple
  • ) */ + function rangeSpansBlockElements(range: Range): boolean { + const startEl = range.startContainer instanceof HTMLElement ? range.startContainer : range.startContainer.parentElement; + const endEl = range.endContainer instanceof HTMLElement ? range.endContainer : range.endContainer.parentElement; + if (!startEl || !endEl) return false; + const startBlock = startEl.closest('li, td, th, p, pre'); + const endBlock = endEl.closest('li, td, th, p, pre'); + return !!(startBlock && endBlock && startBlock !== endBlock); + } - document.body.appendChild(popupEl); + /** + * Wrap a Range with elements. Returns the first mark, or null on failure. + * Handles both simple (single text node) and complex (multi-node) cases. + */ + function applyMark(range: Range, id: string, color: RemarkColor): HTMLElement | null { + const cls = `remark-ann remark-ann-${color}`; - // Position below selection - const popupRect = popupEl.getBoundingClientRect(); - let top = rect.bottom + 8; - let left = rect.left + (rect.width / 2) - (popupRect.width / 2); + // Detect if range spans block-level elements (li, td, p, etc.) + // In that case, extractContents would rip block structure → go straight to per-node wrap + const spansBlocks = rangeSpansBlockElements(range); - if (left < 8) left = 8; - if (left + popupRect.width > window.innerWidth - 8) { - left = window.innerWidth - popupRect.width - 8; - } - if (top + popupRect.height > window.innerHeight - 8) { - top = rect.top - popupRect.height - 8; + // Strategy 1: extractContents (works when range is within a single inline context) + if (!spansBlocks) { + try { + const contents = range.extractContents(); + const mark = document.createElement('mark'); + mark.className = cls; + mark.dataset.annId = id; + mark.addEventListener('click', (e) => { e.stopPropagation(); onMarkClick(id); }); + mark.appendChild(contents); + range.insertNode(mark); + return mark; + } catch { + // Falls through to multi-node wrap + } } - popupEl.style.top = `${top}px`; - popupEl.style.left = `${left}px`; - - // Wire color buttons — change existing annotation's color - let interacted = false; - const colorsDiv = popupEl.querySelector('.remark-popup-colors'); - const colorToggleBtn = popupEl.querySelector('.remark-color-toggle'); - const colorBtns = popupEl.querySelectorAll('.remark-color-btn'); - - // Toggle color picker on dot click - colorToggleBtn?.addEventListener('click', () => { - const isOpen = colorsDiv?.style.display !== 'none'; - if (colorsDiv) colorsDiv.style.display = isOpen ? 'none' : 'flex'; - }); + // Strategy 2: Wrap each text node independently + const textNodes = getTextNodesInRange(range); + let first: HTMLElement | null = null; + for (const { node, start, end } of textNodes) { + try { + const r = document.createRange(); + r.setStart(node, start); + r.setEnd(node, end); + const mark = document.createElement('mark'); + mark.className = cls; + mark.dataset.annId = id; + mark.dataset.annSeq = first ? 'cont' : 'first'; + mark.addEventListener('click', (e) => { e.stopPropagation(); onMarkClick(id); }); + r.surroundContents(mark); + if (!first) first = mark; + } catch { + // Skip nodes that can't be wrapped + } + } + return first; + } - colorBtns.forEach(btn => { - btn.addEventListener('click', () => { - interacted = true; - colorBtns.forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - ann.color = btn.dataset.color as RemarkColor; - // Update toggle dot to reflect selected color - if (colorToggleBtn) colorToggleBtn.textContent = COLOR_MAP[ann.color].emoji; - // Collapse the picker after selection - if (colorsDiv) colorsDiv.style.display = 'none'; - renderHighlights(); - renderSidebarContent(); - void saveAnnotations(); - }); + /** + * Remove all elements, preserving their content. + */ + function clearMarks(): void { + const container = getContainer(); + if (!container) return; + container.querySelectorAll('mark.remark-ann').forEach(mark => { + const parent = mark.parentNode!; + while (mark.firstChild) parent.insertBefore(mark.firstChild, mark); + parent.removeChild(mark); }); + // Normalize text nodes that were split by mark insertion + container.normalize(); + } - const cancelBtn = popupEl.querySelector('.remark-cancel-btn'); - const noteInput = popupEl.querySelector('.remark-note-input'); - - // Cancel → delete the just-created annotation - cancelBtn?.addEventListener('click', () => { - annotations = annotations.filter(a => a.id !== annId); - renderHighlights(); - renderSidebarContent(); - notifyCount(); - void saveAnnotations(); - hidePopup(); - window.getSelection()?.removeAllRanges(); - }); + /** + * Restore marks for all annotations by searching their selectedText in the DOM. + */ + function restoreMarks(): void { + const container = getContainer(); + if (!container) return; - // Save note on Enter (without shift) - noteInput?.addEventListener('keydown', (ke) => { - if (ke.key === 'Enter' && !ke.shiftKey) { - ke.preventDefault(); - ann.note = noteInput.value.trim(); - renderSidebarContent(); - void saveAnnotations(); - hidePopup(); - window.getSelection()?.removeAllRanges(); - } - }); + const visibleAnnotations = annotations.filter(a => !softDeletedIds.has(a.id)); + for (const ann of visibleAnnotations) { + // Find the block this annotation belongs to + const blocks = container.querySelectorAll('[data-line]'); + let marked = false; + for (const block of blocks) { + const { start: blockLine, end: blockEnd } = getBlockRange(block); + if (!rangesOverlap(blockLine, blockEnd, ann.startLine, ann.endLine)) continue; - setTimeout(() => noteInput?.focus(), 50); - - - // Click outside → cancel (remove annotation) if user never interacted; otherwise save - const outsideHandler = (e: MouseEvent) => { - if (popupEl && !popupEl.contains(e.target as Node)) { - const note = noteInput?.value.trim() ?? ''; - if (!interacted && note === '') { - // No interaction — silently discard the annotation - annotations = annotations.filter(a => a.id !== annId); - renderHighlights(); - renderSidebarContent(); - notifyCount(); - void saveAnnotations(); - window.getSelection()?.removeAllRanges(); - } else if (noteInput) { - ann.note = note; - renderSidebarContent(); - void saveAnnotations(); + const range = findTextRange(block, ann.selectedText); + if (range) { + applyMark(range, ann.id, ann.color); + marked = true; + break; } - hidePopup(); - document.removeEventListener('mousedown', outsideHandler); } - }; - setTimeout(() => document.addEventListener('mousedown', outsideHandler), 100); + // If text not found (document changed), annotation is "orphaned" + // It still appears in sidebar with ⚠️ but no inline mark + if (!marked) { + // Tag for sidebar display + (ann as RemarkAnnotation & { _orphaned?: boolean })._orphaned = true; + } + } } - function buildPopupHTML(selectedText: string): string { - const preview = escapeHtml(truncate(selectedText, 80)); + /** + * Click on an inline → scroll sidebar to that annotation + focus textarea + */ + function onMarkClick(annId: string): void { + if (!sidebarEl) return; + const item = sidebarEl.querySelector(`.remark-sidebar-item[data-ann-id="${annId}"]`); + const ta = item?.querySelector('.remark-sidebar-note-editor'); + if (item) { + item.scrollIntoView({ behavior: 'auto', block: 'nearest' }); + // Landing pulse on sidebar item + item.classList.add('remark-item-landing'); + setTimeout(() => item.classList.remove('remark-item-landing'), 800); + } + if (ta) ta.focus(); - return ` -
    - "${preview}" -
    - - -
    - - -
    - `; + // Landing pulse on marks + const container = getContainer(); + if (container) { + container.querySelectorAll(`mark[data-ann-id="${annId}"]`).forEach(m => { + m.classList.add('remark-landing'); + setTimeout(() => m.classList.remove('remark-landing'), 800); + }); + } } - function hidePopup(): void { - if (popupEl) { - popupEl.remove(); - popupEl = null; - } - // Remove any temporary block highlight - document.querySelectorAll('.remark-popup-target') - .forEach(el => el.classList.remove('remark-popup-target')); + // ─── Sidebar-first creation ──────────────────────────────────────────────── + + function createAndFocusSidebar(selectedText: string, startLine: number, endLine: number, range: Range, blockId?: string): void { + const annId = generateId(); + const ann: RemarkAnnotation = { + id: annId, startLine, endLine, selectedText, + note: '', color: config.defaultColor, timestamp: Date.now(), blockId, + }; + annotations.push(ann); + + // Apply inline mark directly from the live Range (most reliable) + applyMark(range, annId, ann.color); + + renderSidebarContent(); + notifyCount(); + void saveAnnotations(); + + // Focus the new item's textarea in sidebar + requestAnimationFrame(() => { + const item = sidebarEl?.querySelector(`.remark-sidebar-item[data-ann-id="${annId}"]`); + const ta = item?.querySelector('.remark-sidebar-note-editor'); + if (ta) { + item?.scrollIntoView({ behavior: 'auto', block: 'nearest' }); + ta.focus(); + } + }); } // ─── Annotations ─────────────────────────────────────────────────────────── @@ -709,10 +969,42 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll softDeletedIds.add(id); undoQueue.push({ id, ann: { ...ann } }); updateExportBtnState(); - renderHighlights(); - renderSidebarContent(); + // Remove marks for this annotation + removeMarksForAnnotation(id); + // Animate sidebar item collapse instead of full re-render + const sidebarItem = sidebarEl?.querySelector(`.remark-sidebar-item[data-ann-id="${id}"]`); + if (sidebarItem) { + sidebarItem.classList.add('remark-item-collapsing'); + setTimeout(() => sidebarItem.remove(), 500); + } notifyCount(); showUndoToast(); + void saveAnnotations(); // Persist immediately so refresh reflects deletion + } + + /** Remove annotation silently (no undo toast) — used for auto-delete empty */ + function silentRemoveAnnotation(id: string): void { + const idx = annotations.findIndex(a => a.id === id); + if (idx === -1) return; + annotations.splice(idx, 1); + removeMarksForAnnotation(id); + // Remove sidebar item directly (already collapsed by CSS animation) + const sidebarItem = sidebarEl?.querySelector(`.remark-sidebar-item[data-ann-id="${id}"]`); + if (sidebarItem) sidebarItem.remove(); + notifyCount(); + void saveAnnotations(); + } + + /** Remove inline elements for a specific annotation */ + function removeMarksForAnnotation(id: string): void { + const container = getContainer(); + if (!container) return; + container.querySelectorAll(`mark[data-ann-id="${id}"]`).forEach(mark => { + const parent = mark.parentNode!; + while (mark.firstChild) parent.insertBefore(mark.firstChild, mark); + parent.removeChild(mark); + }); + container.normalize(); } function updateAnnotationNote(id: string, note: string): void { @@ -740,7 +1032,7 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll
    ${t('remark_sidebar_title', 'Remarks')}
    - +
    @@ -748,6 +1040,7 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll `; el.classList.remove('remark-sidebar-closed'); + applyConfigStyles(); // Wire export button: copy and reset (no auto-exit, allows repeated copy) const exportBtn = el.querySelector('.remark-sidebar-export'); @@ -755,15 +1048,28 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll const result = await exportToClipboard(); if (exportBtn) { if (result.ok) { + if (config.closeAfterCopy) { + exportBtn.textContent = `✅ ${t('remark_copied', 'Copied!')}`; + exportBtn.disabled = true; + let countdown = 3; + const tick = (): void => { + if (countdown <= 0) { window.close(); return; } + exportBtn.textContent = `🔄 closing ${countdown}`; + countdown--; + setTimeout(tick, 1000); + }; + setTimeout(tick, 1000); // 1s showing "Copied!" then start countdown + return; + } exportBtn.textContent = `✅ ${t('remark_copied', 'Copied!')}`; exportBtn.disabled = true; setTimeout(() => { - exportBtn.textContent = `📋 ${t('remark_copy_btn', 'Copy remarks')}`; + exportBtn.textContent = `📋 ${t('remark_copy_btn', 'Copy')}`; exportBtn.disabled = false; }, 2000); } else { exportBtn.textContent = `⚠️ ${t('remark_copy_failed', 'Failed')}`; - setTimeout(() => { exportBtn.textContent = `📋 ${t('remark_copy_btn', 'Copy remarks')}`; exportBtn.disabled = false; }, 2000); + setTimeout(() => { exportBtn.textContent = `📋 ${t('remark_copy_btn', 'Copy')}`; exportBtn.disabled = false; }, 2000); } } }); @@ -787,6 +1093,7 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll renderSidebarContent(); notifyCount(); showUndoToast(); + void saveAnnotations(); // Persist immediately so refresh reflects deletion }); } @@ -839,21 +1146,28 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll } const sorted = [...visibleAnnotations].sort((a, b) => a.startLine - b.startLine); - list.innerHTML = sorted.map(ann => { + list.innerHTML = sorted.map((ann, idx) => { const lineRef = formatLineRef(ann.startLine, ann.endLine); const quote = escapeHtml(truncate(ann.selectedText, 50)); - const noteHtml = ann.note - ? `
    ${escapeHtml(ann.note)}
    ` - : `
    ${t('remark_add_note', 'Add a note…')}
    `; + const noteEscaped = escapeHtml(ann.note || ''); + const orphaned = (ann as RemarkAnnotation & { _orphaned?: boolean })._orphaned; + const colorOptions = (['yellow', 'green', 'blue', 'pink'] as RemarkColor[]).map(c => + `${COLOR_MAP[c].emoji}` + ).join(''); return `
    - ${COLOR_MAP[ann.color].emoji} ${lineRef} + + ${COLOR_MAP[ann.color].emoji} + ${orphaned ? '⚠️ ' : ''}${lineRef} + #${idx + 1} +
    +
    "${quote}"
    - ${noteHtml} +
    `; }).join(''); @@ -864,48 +1178,35 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll const container = getContainer(); if (container) { - // Use range-overlap so row-level annotations (whose startLine may differ - // from the block's data-line) still scroll to the correct block. - const block = Array.from( - container.querySelectorAll('[data-line]') - ).find(el => { - const { start, end } = getBlockRange(el); - return rangesOverlap(start, end, ann.startLine, ann.endLine); - }) ?? null; - if (block) { - const rect = block.getBoundingClientRect(); + // Find the inline for this annotation + const mark = container.querySelector(`mark[data-ann-id="${id}"]`); + if (mark) { + const rect = mark.getBoundingClientRect(); const inViewport = rect.top >= 0 && rect.bottom <= window.innerHeight; if (!inViewport) { - block.scrollIntoView({ behavior: 'auto', block: 'center' }); - // Landing pulse after scroll settles - setTimeout(() => { - block.classList.add('remark-landing'); - setTimeout(() => block.classList.remove('remark-landing'), 1000); - }, 300); - } else { - // Already visible — pulse immediately - block.classList.add('remark-landing'); - setTimeout(() => block.classList.remove('remark-landing'), 1000); + mark.scrollIntoView({ behavior: 'auto', block: 'center' }); } + // Landing pulse on the mark + mark.classList.add('remark-landing'); + setTimeout(() => mark.classList.remove('remark-landing'), 800); + } else { + // Fallback: scroll to block if mark not rendered (orphaned) + const block = Array.from( + container.querySelectorAll('[data-line]') + ).find(el => { + const { start, end } = getBlockRange(el); + return rangesOverlap(start, end, ann.startLine, ann.endLine); + }); + if (block) block.scrollIntoView({ behavior: 'auto', block: 'center' }); } } const targetItem = list.querySelector(`.remark-sidebar-item[data-ann-id="${id}"]`); - const noteEl = targetItem?.querySelector('[data-editable]') as HTMLElement | null; - if (noteEl) noteEl.click(); + const ta = targetItem?.querySelector('.remark-sidebar-note-editor'); + if (ta) ta.focus(); }; - const requestAnnotationFocus = (id: string, event?: MouseEvent): void => { - const activeEditor = sidebarEl?.querySelector('.remark-sidebar-note-editor') as HTMLTextAreaElement | null; - const activeItemId = activeEditor?.closest('.remark-sidebar-item')?.getAttribute('data-ann-id') || null; - - if (activeEditor && activeItemId !== id) { - pendingFocusId = id; - event?.preventDefault(); - activeEditor.blur(); - return; - } - + const requestAnnotationFocus = (id: string, _event?: MouseEvent): void => { focusAnnotationFromSidebar(id); }; @@ -923,85 +1224,152 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll }); }); - // Wire click-to-scroll (on header/quote area, not note) + // Wire color dot → toggle picker + list.querySelectorAll('.remark-color-dot').forEach(dot => { + dot.addEventListener('click', (e) => { + e.stopPropagation(); + const item = (dot as HTMLElement).closest('.remark-sidebar-item'); + const picker = item?.querySelector('.remark-color-picker'); + if (picker) picker.style.display = picker.style.display === 'none' ? 'flex' : 'none'; + }); + }); + + // Wire color picker options + list.querySelectorAll('.remark-color-opt').forEach(opt => { + opt.addEventListener('click', (e) => { + e.stopPropagation(); + const color = (opt as HTMLElement).dataset.color as RemarkColor; + const item = (opt as HTMLElement).closest('.remark-sidebar-item') as HTMLElement | null; + const id = item?.dataset.annId; + if (!id || !color) return; + const ann = annotations.find(a => a.id === id); + if (!ann) return; + ann.color = color; + // Update marks in content + removeMarksForAnnotation(id); + const container = getContainer(); + if (container) { + const blocks = container.querySelectorAll('[data-line]'); + for (const block of blocks) { + const { start, end } = getBlockRange(block); + if (rangesOverlap(start, end, ann.startLine, ann.endLine)) { + const range = findTextRange(block, ann.selectedText); + if (range) applyMark(range, id, color); + break; + } + } + } + // Update sidebar item in-place (no full re-render) + const dot = item?.querySelector('.remark-color-dot'); + if (dot) dot.textContent = COLOR_MAP[color].emoji; + const picker = item?.querySelector('.remark-color-picker'); + if (picker) picker.style.display = 'none'; + void saveAnnotations(); + }); + }); + + // Wire click-to-scroll (on header/quote area) list.querySelectorAll('.remark-sidebar-item').forEach(item => { const header = item.querySelector('.remark-sidebar-item-header'); const quote = item.querySelector('.remark-sidebar-quote'); [header, quote].forEach(el => { el?.addEventListener('mousedown', (e) => { - if ((e.target as HTMLElement | null)?.closest?.('.remark-sidebar-delete')) return; + if ((e.target as HTMLElement | null)?.closest?.('.remark-sidebar-delete, .remark-color-dot')) return; const id = (item as HTMLElement).dataset.annId; if (!id) return; requestAnnotationFocus(id, e as MouseEvent); }); }); - // Wire inline note editing - const noteEl = item.querySelector('[data-editable]') as HTMLElement | null; - noteEl?.addEventListener('mousedown', (e) => { - const id = (item as HTMLElement).dataset.annId; - if (!id) return; - const activeEditor = sidebarEl?.querySelector('.remark-sidebar-note-editor') as HTMLTextAreaElement | null; - const activeItemId = activeEditor?.closest('.remark-sidebar-item')?.getAttribute('data-ann-id') || null; - if (activeEditor && activeItemId !== id) { - pendingFocusId = id; - e.preventDefault(); - activeEditor.blur(); - } + // Wire always-visible textarea (auto-grow + save on input) + const ta = item.querySelector('.remark-sidebar-note-editor'); + if (!ta) return; + const id = (item as HTMLElement).dataset.annId; + + const autoResize = (): void => { + ta.style.height = 'auto'; + const lineHeight = parseInt(getComputedStyle(ta).lineHeight) || 18; + const maxH = lineHeight * 5 + 12; + ta.style.height = `${Math.min(ta.scrollHeight, maxH)}px`; + ta.style.overflow = ta.scrollHeight > maxH ? 'auto' : 'hidden'; + }; + + // Auto-resize on content + requestAnimationFrame(autoResize); + + let saveTimer: ReturnType | null = null; + ta.addEventListener('input', () => { + autoResize(); + // Debounced save (300ms) + if (saveTimer) clearTimeout(saveTimer); + saveTimer = setTimeout(() => { + if (!id) return; + const ann = annotations.find(a => a.id === id); + if (ann) { + ann.note = ta.value.trim(); + void saveAnnotations(); + } + }, 300); }); - noteEl?.addEventListener('click', (e) => { - e.stopPropagation(); - const id = (item as HTMLElement).dataset.annId; - if (!id) return; + + // Stop keyboard events from bubbling (prevents ext shortcuts) + ta.addEventListener('keydown', (e) => { e.stopPropagation(); }); + ta.addEventListener('keyup', (e) => { e.stopPropagation(); }); + + // Auto-delete empty: double-fade sidebar note only, then remove (3s total) + ta.addEventListener('blur', () => { + if (!id || !config.autoDeleteEmpty) return; const ann = annotations.find(a => a.id === id); - if (!ann) return; + if (!ann || ann.note.trim()) return; // Has note → keep - // Replace with textarea - const ta = document.createElement('textarea'); - ta.className = 'remark-sidebar-note-editor'; - ta.value = ann.note; - ta.placeholder = t('remark_add_note', 'Add a note…'); - ta.rows = 1; - noteEl.replaceWith(ta); - - // Auto-expand textarea up to 5 lines - const autoResize = (): void => { - ta.style.height = 'auto'; - const lineHeight = parseInt(getComputedStyle(ta).lineHeight) || 18; - const maxH = lineHeight * 5 + 12; // 5 lines + padding - ta.style.height = `${Math.min(ta.scrollHeight, maxH)}px`; - ta.style.overflow = ta.scrollHeight > maxH ? 'auto' : 'hidden'; - }; - ta.addEventListener('input', autoResize); - // Initial auto-resize after DOM insertion - requestAnimationFrame(autoResize); + // Cancel any existing timer for this id + if (autoDeleteTimers.has(id)) clearTimeout(autoDeleteTimers.get(id)!); - ta.focus(); - // Place cursor at end without selecting text - const len = ta.value.length; - ta.setSelectionRange(len, len); - - const saveEdit = (): void => { - const newNote = ta.value.trim(); - updateAnnotationNote(id, newNote); - // renderSidebarContent is called inside updateAnnotationNote - }; - - ta.addEventListener('blur', saveEdit); - ta.addEventListener('keydown', (ke) => { - if (ke.key === 'Enter' && !ke.shiftKey) { - ke.preventDefault(); - ta.blur(); - } - if (ke.key === 'Escape') { - ta.removeEventListener('blur', saveEdit); - renderSidebarContent(); - } - }); + const timer = setTimeout(() => { + autoDeleteTimers.delete(id); + // Re-check: user may have re-focused or typed + if (document.activeElement === ta) return; + const annCheck = annotations.find(a => a.id === id); + if (!annCheck || annCheck.note.trim()) return; + + const sidebarItem = sidebarEl?.querySelector(`.remark-sidebar-item[data-ann-id="${id}"]`); + + // Phase 1: fade sidebar note (0→800ms) + if (sidebarItem) sidebarItem.classList.add('remark-item-fading'); + + setTimeout(() => { + // Phase 2: fade back (800→1600ms) + if (sidebarItem) sidebarItem.classList.remove('remark-item-fading'); + + setTimeout(() => { + // Re-check before final removal + if (document.activeElement === ta) return; + const annFinal = annotations.find(a => a.id === id); + if (!annFinal || annFinal.note.trim()) return; + + // Phase 3: final fade + collapse (1600→3000ms) + if (sidebarItem) sidebarItem.classList.add('remark-item-collapsing'); + + setTimeout(() => { + silentRemoveAnnotation(id); + }, 1400); + }, 800); + }, 800); + }, config.autoDeleteDelay); + + autoDeleteTimers.set(id, timer); + }); + + // Cancel auto-delete on re-focus + ta.addEventListener('focus', () => { + if (id && autoDeleteTimers.has(id)) { + clearTimeout(autoDeleteTimers.get(id)!); + autoDeleteTimers.delete(id); + } }); }); - // Handle pending focus from click chain (when clicking note B while A was editing) + // Handle pending focus from click chain if (pendingFocusId) { const focusId = pendingFocusId; pendingFocusId = null; @@ -1012,90 +1380,16 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll updateExportBtnState(); } - // ─── Highlights ──────────────────────────────────────────────────────────── + // ─── Highlights (mark-based) ───────────────────────────────────────────────── function renderHighlights(): void { - clearHighlights(); - const container = getContainer(); - if (!container) return; - - // Only render active (non-deleted) annotations - const visibleAnnotations = annotations.filter(a => !softDeletedIds.has(a.id)); - - for (const ann of visibleAnnotations) { - const blocks = container.querySelectorAll('[data-line]'); - for (const block of blocks) { - const { start: blockLine, end: blockEnd } = getBlockRange(block); - - if (rangesOverlap(blockLine, blockEnd, ann.startLine, ann.endLine)) { - block.classList.add('remark-highlighted'); - block.style.setProperty('--remark-bg', COLOR_MAP[ann.color].bg); - block.style.setProperty('--remark-border', COLOR_MAP[ann.color].border); - - // For narrowed (sub-block) annotations, highlight the specific element - const isNarrowed = ann.startLine > blockLine || ann.endLine < blockEnd; - if (isNarrowed) { - // Table rows - const tbody = block.querySelector('tbody'); - if (tbody) { - Array.from(tbody.querySelectorAll('tr')).forEach((row, idx) => { - const rowLine = blockLine + 2 + idx; - if (rangesOverlap(ann.startLine, ann.endLine, rowLine, rowLine)) { - row.classList.add('remark-row-highlighted'); - row.dataset.remarkColor = ann.color; - } - }); - } - // List items - const lis = block.querySelectorAll('li'); - if (lis.length > 0) { - Array.from(lis).forEach((li, idx) => { - const liLine = blockLine + idx; - if (rangesOverlap(ann.startLine, ann.endLine, liLine, liLine)) { - li.classList.add('remark-li-highlighted'); - li.dataset.remarkColor = ann.color; - } - }); - } - } - - if (!block.querySelector(`.remark-badge[data-ann-id="${ann.id}"]`)) { - const badge = document.createElement('span'); - badge.className = 'remark-badge'; - badge.dataset.annId = ann.id; - badge.textContent = '✕'; - badge.title = `${t('remark_delete', 'Delete')}: ${ann.note || getColorLabel(ann.color)}`; - badge.style.color = COLOR_MAP[ann.color].border; - block.style.position = 'relative'; - badge.addEventListener('click', (e) => { - e.stopPropagation(); - removeAnnotation(ann.id); - }); - block.appendChild(badge); - } - } - } - } + // Clear existing marks and re-apply from annotations + clearMarks(); + restoreMarks(); } function clearHighlights(): void { - const container = getContainer(); - if (!container) return; - - container.querySelectorAll('.remark-highlighted').forEach(el => { - el.classList.remove('remark-highlighted'); - (el as HTMLElement).style.removeProperty('--remark-bg'); - (el as HTMLElement).style.removeProperty('--remark-border'); - }); - container.querySelectorAll('.remark-row-highlighted').forEach(el => { - el.classList.remove('remark-row-highlighted'); - delete (el as HTMLElement).dataset.remarkColor; - }); - container.querySelectorAll('.remark-li-highlighted').forEach(el => { - el.classList.remove('remark-li-highlighted'); - delete (el as HTMLElement).dataset.remarkColor; - }); - container.querySelectorAll('.remark-badge').forEach(el => el.remove()); + clearMarks(); } // ─── Export ──────────────────────────────────────────────────────────────── @@ -1181,77 +1475,54 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll .remark-mode-active { cursor: text; } - /* Choreographed exit: fade highlights before removing them */ - .remark-exiting .remark-highlighted, - .remark-exiting .remark-badge, - .remark-exiting .remark-row-highlighted, - .remark-exiting .remark-li-highlighted { + /* Choreographed exit: fade marks before removing them */ + .remark-exiting mark.remark-ann { opacity: 0; transition: opacity 120ms ease-out; } - /* Landing pulse — stronger ring+glow for sidebar navigation */ - @keyframes remark-landing-anim { - 0% { box-shadow: 0 0 0 3px rgba(250, 204, 21, 0.6); background-color: rgba(250, 204, 21, 0.25); } - 100% { box-shadow: 0 0 0 0 transparent; background-color: transparent; } - } - .remark-landing { - animation: remark-landing-anim 1s ease-out; - border-radius: 3px; + + /* ── Inline highlights ─────────────────────────────── */ + mark.remark-ann { + background-color: rgba(250, 204, 21, 0.25) !important; + border-radius: 2px; + cursor: pointer; + padding: 1px 0; + transition: opacity 0.3s ease-out, background-color 0.2s; } - /* Row-level highlight for table annotations with narrowed line ranges */ - tr.remark-row-highlighted { background: rgba(250, 204, 21, 0.35) !important; } - tr.remark-row-highlighted[data-remark-color="green"] { background: rgba(74, 222, 128, 0.35) !important; } - tr.remark-row-highlighted[data-remark-color="blue"] { background: rgba(96, 165, 250, 0.35) !important; } - tr.remark-row-highlighted[data-remark-color="pink"] { background: rgba(244, 114, 182, 0.35) !important; } - li.remark-li-highlighted { background: rgba(250, 204, 21, 0.3) !important; border-radius: 3px; } - li.remark-li-highlighted[data-remark-color="green"] { background: rgba(74, 222, 128, 0.3) !important; } - li.remark-li-highlighted[data-remark-color="blue"] { background: rgba(96, 165, 250, 0.3) !important; } - li.remark-li-highlighted[data-remark-color="pink"] { background: rgba(244, 114, 182, 0.3) !important; } - .remark-mode-active [data-line][data-block-id]:not(:has(img, svg, canvas, figure, video)):hover { - outline: 1px dashed var(--color-nav-active-border, var(--color-theme-accent, var(--color-primary, #2563eb))); - outline-offset: 2px; - border-radius: 3px; + /* Hide structural whitespace marks between block elements */ + ul > mark.remark-ann, ol > mark.remark-ann, + tr > mark.remark-ann, tbody > mark.remark-ann, + thead > mark.remark-ann, table > mark.remark-ann { + display: none !important; } - /* Temporary highlight on the block being annotated */ - .remark-popup-target { - outline: 2px dashed var(--color-nav-active-border, var(--color-theme-accent, var(--color-primary, #2563eb))) !important; - outline-offset: 3px; - border-radius: 3px; - background: var(--color-nav-active-bg, var(--color-theme-accent-subtle, var(--color-primary-subtle, rgba(37, 99, 235, 0.06)))); + mark.remark-ann-yellow { background-color: rgba(255, 212, 0, 0.25) !important; } + mark.remark-ann-green { background-color: rgba(46, 160, 67, 0.18) !important; } + mark.remark-ann-blue { background-color: rgba(9, 105, 218, 0.15) !important; } + mark.remark-ann-pink { background-color: rgba(219, 97, 162, 0.18) !important; } + + /* Auto-delete animation: double-fade then collapse */ + /* Landing pulse on mark */ + @keyframes remark-mark-landing { + 0% { box-shadow: 0 0 0 3px rgba(250, 204, 21, 0.5); } + 100% { box-shadow: 0 0 0 0 transparent; } } - .remark-highlighted { - background: var(--remark-bg, rgba(250, 204, 21, 0.15)); - border-left: 3px solid var(--remark-border, rgba(250, 204, 21, 0.6)); - padding-left: 8px; - border-radius: 3px; - transition: background 0.2s; - cursor: pointer; + mark.remark-ann.remark-landing { + animation: remark-mark-landing 0.8s ease-out; + border-radius: 2px; } - .remark-badge { - position: absolute; - top: 2px; - right: -24px; - font-size: 11px; - font-weight: 700; - cursor: pointer; - user-select: none; - opacity: 0; - transition: opacity 0.15s, background 0.15s; - width: 16px; - height: 16px; - line-height: 16px; - text-align: center; - border-radius: 50%; - background: var(--gray-100, #f3f4f6); + @keyframes remark-item-landing { + 0% { background: rgba(250, 204, 21, 0.12); } + 100% { background: transparent; } } - .remark-highlighted:hover .remark-badge, - .remark-badge:hover { - opacity: 1; + .remark-sidebar-item.remark-item-landing { + animation: remark-item-landing 0.8s ease-out; } - .remark-badge:hover { - background: var(--color-danger-bg, rgba(239, 68, 68, 0.15)); - color: var(--color-danger, #ef4444) !important; - transform: scale(1.1); + + /* Hover outline for selectable blocks */ + .remark-mode-active [data-line][data-block-id]:not(:has(img, svg, canvas, figure, video)):hover { + outline: 1px dashed var(--color-nav-active-border, var(--color-theme-accent, var(--color-primary, #2563eb))); + outline-offset: 2px; + border-radius: 3px; } /* Tooltip */ @@ -1335,6 +1606,7 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll flex: 1; overflow-y: auto; padding: 8px; + font-size: var(--remark-font-size, 13px); } .remark-sidebar-empty { color: var(--gray-400, #9ca3af); @@ -1399,7 +1671,7 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll .remark-sidebar-quote { font-style: italic; color: var(--gray-500, #6b7280); - font-size: 12px; + font-size: calc(var(--remark-font-size, 13px) - 1px); line-height: 1.4; overflow: hidden; text-overflow: ellipsis; @@ -1407,128 +1679,83 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll -webkit-line-clamp: 2; -webkit-box-orient: vertical; } - .remark-sidebar-note { - margin-top: 4px; - font-size: 12px; - color: var(--color-text-primary, #1a1a1a); - background: var(--gray-50, #f9fafb); - padding: 4px 8px; + .remark-sidebar-note-editor { + width: 100%; + box-sizing: border-box; + border: 1px solid var(--color-border, #e2e8f0); border-radius: 4px; + padding: 4px 6px; + font-size: var(--remark-font-size, 13px); + font-family: inherit; + resize: none; overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 5; - -webkit-box-orient: vertical; - line-height: 18px; - } - - /* Popup */ - .remark-popup { - position: fixed; - z-index: 10001; - background: var(--color-bg-surface, #fff); - border: 1px solid var(--color-border, #e2e8f0); - border-radius: 8px; - box-shadow: var(--shadow-floating, 0 4px 16px rgba(0,0,0,0.15)); - padding: 12px; - width: 320px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 13px; + margin-top: 4px; + background: var(--gray-50, #f9fafb); color: var(--color-text-primary, #1a1a1a); + line-height: 1.4; + transition: border-color 0.15s, box-shadow 0.15s; } - .remark-popup-header { - margin-bottom: 8px; + .remark-sidebar-note-editor:focus { + outline: none; + border-color: var(--color-nav-active-border, var(--color-theme-accent, var(--color-primary, #2563eb))); + background: var(--color-bg-surface, #fff); + box-shadow: 0 0 0 2px var(--color-theme-accent-subtle, var(--color-primary-subtle, #dbeafe)); } - .remark-popup-quote { + .remark-sidebar-note-editor::placeholder { + color: var(--gray-400, #9ca3af); font-style: italic; - color: var(--gray-500, #6b7280); - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } - .remark-popup-colors { + + /* ── Sidebar polish: line ref pill, color picker, seq badge ── */ + .remark-sidebar-ref { display: flex; + align-items: center; gap: 6px; - margin-bottom: 8px; } - .remark-color-btn { - border: 2px solid transparent; - border-radius: 6px; - background: var(--gray-100, #f3f4f6); - padding: 4px 8px; + .remark-color-dot { cursor: pointer; - font-size: 16px; - color: var(--color-text-primary, #1a1a1a); - transition: border-color 0.15s; - } - .remark-color-btn:hover { - background: var(--gray-200, #e5e7eb); + font-size: 14px; + transition: transform 0.1s; } - .remark-color-btn.active { - border-color: var(--color-nav-active-border, var(--color-theme-accent, var(--color-primary, #2563eb))); - background: var(--color-nav-active-bg, var(--color-theme-accent-bg, var(--color-primary-light, #eff6ff))); - color: var(--color-nav-active-text, var(--color-theme-accent, var(--color-primary, #2563eb))); + .remark-color-dot:hover { + transform: scale(1.2); } - .remark-note-input { - width: 100%; - box-sizing: border-box; - border: 1px solid var(--color-border, #e2e8f0); - border-radius: 6px; - padding: 8px; - font-size: 13px; - font-family: inherit; - resize: vertical; - min-height: 48px; - margin-bottom: 8px; - color: inherit; - background: var(--gray-50, #f9fafb); + .remark-lineref-pill { + font-family: 'SF Mono', Consolas, 'Liberation Mono', monospace; + font-size: 10px; + font-weight: 600; + background: var(--gray-100, #f3f4f6); + color: var(--gray-600, #4b5563); + padding: 1px 6px; + border-radius: 8px; + letter-spacing: 0.3px; } - .remark-note-input:focus { - outline: none; - border-color: var(--color-nav-active-border, var(--color-theme-accent, var(--color-primary, #2563eb))); - box-shadow: 0 0 0 2px var(--color-theme-accent-subtle, var(--color-primary-subtle, #dbeafe)); + .remark-ann-seq { + font-size: 10px; + color: var(--gray-400, #9ca3af); } - .remark-popup-actions { + .remark-color-picker { display: flex; - justify-content: flex-end; - gap: 8px; - align-items: center; + gap: 4px; + padding: 4px 0; + margin-bottom: 2px; } - .remark-color-toggle { - font-size: 18px; - line-height: 1; - padding: 2px 6px; - border: 1px solid var(--color-border, #e2e8f0); - border-radius: 6px; - background: var(--gray-50, #f9fafb); + .remark-color-opt { cursor: pointer; - margin-right: auto; - transition: background 0.15s; - } - .remark-color-toggle:hover { - background: var(--gray-200, #e5e7eb); + font-size: 16px; + padding: 2px 4px; + border-radius: 4px; + transition: background 0.1s; + opacity: 0.6; } - .remark-popup-actions button { - padding: 6px 14px; - border-radius: 6px; - font-size: 13px; - cursor: pointer; - border: 1px solid var(--color-border, #e2e8f0); - background: var(--gray-50, #f9fafb); - color: inherit; - transition: background 0.15s; + .remark-color-opt:hover { + background: var(--gray-100, #f3f4f6); + opacity: 1; } - .remark-popup-actions button:hover { + .remark-color-opt.active { + opacity: 1; background: var(--gray-200, #e5e7eb); } - .remark-save-btn { - background: var(--color-theme-accent, var(--color-primary, #2563eb)) !important; - color: var(--color-text-on-primary, #fff) !important; - border-color: var(--color-theme-accent, var(--color-primary, #2563eb)) !important; - } - .remark-save-btn:hover { - background: var(--color-theme-accent-hover, var(--color-primary-hover, #1d4ed8)) !important; - } /* Toolbar button active state */ .toolbar-btn.remark-active { @@ -1556,30 +1783,6 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll pointer-events: none; } - /* Sidebar note editable */ - .remark-sidebar-note[data-editable] { - cursor: pointer; - } - .remark-note-placeholder { - color: var(--gray-400, #9ca3af) !important; - font-style: italic; - } - .remark-sidebar-note-editor { - width: 100%; - box-sizing: border-box; - border: 1px solid var(--color-nav-active-border, var(--color-theme-accent, var(--color-primary, #2563eb))); - border-radius: 4px; - padding: 4px 6px; - font-size: 12px; - font-family: inherit; - resize: none; - overflow: hidden; - margin-top: 4px; - background: var(--color-bg-surface, #fff); - color: var(--color-text-primary, #1a1a1a); - box-shadow: 0 0 0 2px var(--color-theme-accent-subtle, var(--color-primary-subtle, #dbeafe)); - line-height: 18px; - } .remark-sidebar-count { color: var(--gray-500, #6b7280); font-weight: normal; @@ -1588,35 +1791,30 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll /* ── UX Delight: Animations ─────────────────────────────── */ @media (prefers-reduced-motion: no-preference) { - /* Popup scale-in */ - .remark-popup { - animation: remark-popup-in 0.15s cubic-bezier(0.34, 1.56, 0.64, 1); - transform-origin: top center; + /* Sidebar item fade-in */ + .remark-sidebar-item { + animation: remark-item-in 0.15s ease-out; + transition: opacity 0.5s ease, max-height 0.5s ease, margin 0.5s ease, padding 0.5s ease; + max-height: 400px; + overflow: hidden; } - @keyframes remark-popup-in { - from { opacity: 0; transform: scale(0.9) translateY(-4px); } - to { opacity: 1; transform: scale(1) translateY(0); } + @keyframes remark-item-in { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } } - - /* Color button ink splash on selection */ - .remark-color-btn.active { - animation: remark-ink 0.3s ease-out; + .remark-sidebar-item.remark-item-fading { + opacity: 0.3; } - @keyframes remark-ink { - 0% { box-shadow: 0 0 0 0 var(--color-theme-accent-subtle, var(--color-primary-subtle, #dbeafe)); } - 70% { box-shadow: 0 0 0 6px transparent; } - 100% { box-shadow: none; } + .remark-sidebar-item.remark-item-collapsing { + opacity: 0; + max-height: 0; + margin-top: 0 !important; + margin-bottom: 0 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; } } - /* Color button labels */ - .remark-color-label { - font-size: 11px; - vertical-align: middle; - color: inherit; - opacity: 0.8; - } - `; document.head.appendChild(style); } diff --git a/src/ui/remark-utils.ts b/src/ui/remark-utils.ts index 8681a385..7bb7e402 100644 --- a/src/ui/remark-utils.ts +++ b/src/ui/remark-utils.ts @@ -267,3 +267,110 @@ export function formatExportText( return lines.join('\n'); } + +// ─── Config Style Generation ───────────────────────────────────────────────── + +export type HighlightStyle = 'background' | 'underline' | 'wavy' | 'border'; + +export const BG_COLORS: Record = { + yellow: 'rgba(255, 212, 0, 0.25)', + green: 'rgba(46, 160, 67, 0.18)', + blue: 'rgba(9, 105, 218, 0.15)', + pink: 'rgba(219, 97, 162, 0.18)', +}; + +export const LINE_COLORS: Record = { + yellow: 'rgba(202, 138, 4, 0.8)', + green: 'rgba(22, 128, 50, 0.75)', + blue: 'rgba(9, 80, 180, 0.7)', + pink: 'rgba(190, 60, 130, 0.75)', +}; + +/** Generate CSS rules for mark elements based on highlight style config */ +export function generateHighlightCSS(style: HighlightStyle): string { + if (style === 'background') { + return Object.entries(BG_COLORS).map(([c, rgba]) => + `mark.remark-ann-${c} { background-color: ${rgba} !important; text-decoration: none !important; border: none !important; }` + ).join('\n'); + } else if (style === 'underline') { + return Object.entries(LINE_COLORS).map(([c, rgba]) => + `mark.remark-ann-${c} { background-color: transparent !important; text-decoration: underline 2px ${rgba} !important; text-underline-offset: 3px; border: none !important; }` + ).join('\n'); + } else if (style === 'wavy') { + return Object.entries(LINE_COLORS).map(([c, rgba]) => + `mark.remark-ann-${c} { background-color: transparent !important; text-decoration: wavy underline ${rgba} !important; text-underline-offset: 2px; border: none !important; }` + ).join('\n'); + } else if (style === 'border') { + return Object.entries(LINE_COLORS).map(([c, rgba]) => + `mark.remark-ann-${c} { background-color: transparent !important; text-decoration: none !important; border: 1.5px solid ${rgba} !important; border-radius: 3px; padding: 0 2px; }` + ).join('\n'); + } + return ''; +} + +// ─── Sentence Boundary Detection ───────────────────────────────────────────── + +/** Regex for sentence-ending punctuation */ +export const SENTENCE_END_RE = /[。.?!?!\n]/; + +/** + * Find sentence boundaries around a given offset in text. + * Returns [start, end) indices of the sentence containing the offset. + */ +export function findSentenceBounds(text: string, offset: number): { start: number; end: number } { + let start = 0; + for (let i = offset - 1; i >= 0; i--) { + if (SENTENCE_END_RE.test(text[i])) { start = i + 1; break; } + } + let end = text.length; + for (let i = offset; i < text.length; i++) { + if (SENTENCE_END_RE.test(text[i])) { end = i + 1; break; } + } + return { start, end }; +} + +// ─── Text Node Offset Calculation ──────────────────────────────────────────── + +export interface TextNodeOffset { + nodeIndex: number; + localOffset: number; +} + +/** + * Given an array of text node lengths, find which node contains a given + * global character offset. Returns nodeIndex and localOffset within that node. + * Returns null if offset is beyond total length. + */ +export function locateOffsetInNodes( + nodeLengths: number[], + globalOffset: number +): TextNodeOffset | null { + let charCount = 0; + for (let i = 0; i < nodeLengths.length; i++) { + if (charCount + nodeLengths[i] > globalOffset) { + return { nodeIndex: i, localOffset: globalOffset - charCount }; + } + charCount += nodeLengths[i]; + } + // Exact end of last node + if (charCount === globalOffset && nodeLengths.length > 0) { + const last = nodeLengths.length - 1; + return { nodeIndex: last, localOffset: nodeLengths[last] }; + } + return null; +} + +/** + * Find start and end positions for a substring within concatenated text nodes. + * Used by findTextRange to map text.indexOf() result to node-level offsets. + */ +export function locateSubstringInNodes( + nodeLengths: number[], + substringStart: number, + substringLength: number +): { start: TextNodeOffset; end: TextNodeOffset } | null { + const startPos = locateOffsetInNodes(nodeLengths, substringStart); + const endPos = locateOffsetInNodes(nodeLengths, substringStart + substringLength); + if (!startPos || !endPos) return null; + return { start: startPos, end: endPos }; +} diff --git a/test/remark-mode.test.ts b/test/remark-mode.test.ts index 298483d8..f2a8e564 100644 --- a/test/remark-mode.test.ts +++ b/test/remark-mode.test.ts @@ -19,6 +19,13 @@ import { findLiLineInBlock, findCodeLineInBlock, narrowLineInBlock, + generateHighlightCSS, + findSentenceBounds, + locateOffsetInNodes, + locateSubstringInNodes, + BG_COLORS, + LINE_COLORS, + SENTENCE_END_RE, } from '../src/ui/remark-utils.ts'; // ─── truncate ──────────────────────────────────────────────────────────────── @@ -678,3 +685,233 @@ describe('narrowLineInBlock', () => { // Caller uses: startLine + lineCount - 1 = 34 + 1 - 1 = 34 (L34 only, not L34-L36) }); }); + +// ─── generateHighlightCSS ──────────────────────────────────────────────────── + +describe('generateHighlightCSS', () => { + it('background: generates background-color rules for all 4 colors', () => { + const css = generateHighlightCSS('background'); + assert.ok(css.includes('mark.remark-ann-yellow')); + assert.ok(css.includes('mark.remark-ann-green')); + assert.ok(css.includes('mark.remark-ann-blue')); + assert.ok(css.includes('mark.remark-ann-pink')); + assert.ok(css.includes('background-color: rgba(255, 212, 0, 0.25)')); + assert.ok(css.includes('text-decoration: none')); + assert.ok(css.includes('border: none')); + }); + + it('underline: uses line colors with solid underline', () => { + const css = generateHighlightCSS('underline'); + assert.ok(css.includes('background-color: transparent')); + assert.ok(css.includes('text-decoration: underline 2px')); + assert.ok(css.includes('text-underline-offset: 3px')); + assert.ok(css.includes(LINE_COLORS.yellow)); + }); + + it('wavy: uses wavy underline decoration', () => { + const css = generateHighlightCSS('wavy'); + assert.ok(css.includes('text-decoration: wavy underline')); + assert.ok(css.includes('text-underline-offset: 2px')); + }); + + it('border: uses solid border with border-radius', () => { + const css = generateHighlightCSS('border'); + assert.ok(css.includes('border: 1.5px solid')); + assert.ok(css.includes('border-radius: 3px')); + assert.ok(css.includes('padding: 0 2px')); + assert.ok(css.includes('text-decoration: none')); + }); + + it('unknown style: returns empty string', () => { + const css = generateHighlightCSS('invalid' as any); + assert.strictEqual(css, ''); + }); + + it('each style generates exactly 4 rules (one per color)', () => { + for (const style of ['background', 'underline', 'wavy', 'border'] as const) { + const css = generateHighlightCSS(style); + const ruleCount = css.split('mark.remark-ann-').length - 1; + assert.strictEqual(ruleCount, 4, `${style} should have 4 rules`); + } + }); +}); + +// ─── findSentenceBounds ────────────────────────────────────────────────────── + +describe('findSentenceBounds', () => { + it('single sentence: returns full text range', () => { + const text = 'Hello world'; + const { start, end } = findSentenceBounds(text, 5); + assert.strictEqual(start, 0); + assert.strictEqual(end, text.length); + }); + + it('period-separated: finds correct sentence', () => { + const text = 'First sentence.Second sentence.Third.'; + const { start, end } = findSentenceBounds(text, 16); + assert.strictEqual(start, 15); + assert.strictEqual(end, 31); + }); + + it('Chinese punctuation: 。as sentence boundary', () => { + const text = '第一句话。第二句话。第三句话。'; + const { start, end } = findSentenceBounds(text, 6); + assert.strictEqual(start, 5); + assert.strictEqual(end, 10); + }); + + it('question mark: acts as boundary', () => { + const text = 'Really?Yes!'; + const { start, end } = findSentenceBounds(text, 8); + assert.strictEqual(start, 7); + assert.strictEqual(end, 11); + }); + + it('newline: acts as sentence boundary', () => { + const text = 'Line one\nLine two\nLine three'; + const { start, end } = findSentenceBounds(text, 12); + assert.strictEqual(start, 9); + assert.strictEqual(end, 18); + }); + + it('offset at start of text: start is 0', () => { + const text = 'First.Second.'; + const { start, end } = findSentenceBounds(text, 0); + assert.strictEqual(start, 0); + assert.strictEqual(end, 6); + }); + + it('offset at end of text: end is text.length', () => { + const text = 'Only sentence'; + const { start, end } = findSentenceBounds(text, 12); + assert.strictEqual(start, 0); + assert.strictEqual(end, text.length); + }); + + it('mixed delimiters: !as Chinese exclamation', () => { + const text = '好的!那就这样吧。'; + const { start, end } = findSentenceBounds(text, 5); + assert.strictEqual(start, 3); + assert.strictEqual(end, 9); + }); + + it('offset exactly on delimiter: includes it in previous sentence', () => { + const text = 'Hello.World'; + const { start, end } = findSentenceBounds(text, 5); + assert.strictEqual(start, 0); + assert.strictEqual(end, 6); + }); +}); + +// ─── SENTENCE_END_RE ───────────────────────────────────────────────────────── + +describe('SENTENCE_END_RE', () => { + it('matches period', () => assert.ok(SENTENCE_END_RE.test('.'))); + it('matches 。', () => assert.ok(SENTENCE_END_RE.test('。'))); + it('matches ?', () => assert.ok(SENTENCE_END_RE.test('?'))); + it('matches ?', () => assert.ok(SENTENCE_END_RE.test('?'))); + it('matches !', () => assert.ok(SENTENCE_END_RE.test('!'))); + it('matches !', () => assert.ok(SENTENCE_END_RE.test('!'))); + it('matches newline', () => assert.ok(SENTENCE_END_RE.test('\n'))); + it('does not match comma', () => assert.ok(!SENTENCE_END_RE.test(','))); + it('does not match semicolon', () => assert.ok(!SENTENCE_END_RE.test(';'))); + it('does not match space', () => assert.ok(!SENTENCE_END_RE.test(' '))); +}); + +// ─── locateOffsetInNodes ───────────────────────────────────────────────────── + +describe('locateOffsetInNodes', () => { + it('single node: offset 0 → node 0, local 0', () => { + const result = locateOffsetInNodes([10], 0); + assert.deepStrictEqual(result, { nodeIndex: 0, localOffset: 0 }); + }); + + it('single node: offset in middle', () => { + const result = locateOffsetInNodes([10], 5); + assert.deepStrictEqual(result, { nodeIndex: 0, localOffset: 5 }); + }); + + it('single node: offset at exact end', () => { + const result = locateOffsetInNodes([10], 10); + assert.deepStrictEqual(result, { nodeIndex: 0, localOffset: 10 }); + }); + + it('single node: offset beyond → null', () => { + const result = locateOffsetInNodes([10], 11); + assert.strictEqual(result, null); + }); + + it('multiple nodes: offset in first node', () => { + const result = locateOffsetInNodes([5, 3, 7], 3); + assert.deepStrictEqual(result, { nodeIndex: 0, localOffset: 3 }); + }); + + it('multiple nodes: offset at boundary → starts next node', () => { + const result = locateOffsetInNodes([5, 3, 7], 5); + assert.deepStrictEqual(result, { nodeIndex: 1, localOffset: 0 }); + }); + + it('multiple nodes: offset in second node', () => { + const result = locateOffsetInNodes([5, 3, 7], 7); + assert.deepStrictEqual(result, { nodeIndex: 1, localOffset: 2 }); + }); + + it('multiple nodes: offset in last node', () => { + const result = locateOffsetInNodes([5, 3, 7], 10); + assert.deepStrictEqual(result, { nodeIndex: 2, localOffset: 2 }); + }); + + it('empty nodes array: returns null', () => { + const result = locateOffsetInNodes([], 0); + assert.strictEqual(result, null); + }); + + it('zero-length node: skips to next', () => { + const result = locateOffsetInNodes([0, 5], 2); + assert.deepStrictEqual(result, { nodeIndex: 1, localOffset: 2 }); + }); +}); + +// ─── locateSubstringInNodes ────────────────────────────────────────────────── + +describe('locateSubstringInNodes', () => { + it('substring in single node: correct start and end', () => { + // "Hello World" in one node, find "World" at index 6, len 5 + const result = locateSubstringInNodes([11], 6, 5); + assert.deepStrictEqual(result, { + start: { nodeIndex: 0, localOffset: 6 }, + end: { nodeIndex: 0, localOffset: 11 }, + }); + }); + + it('substring spanning two nodes', () => { + // nodes: "Hello " (6) + "World" (5) = "Hello World" + // find "o W" at index 4, len 3 + const result = locateSubstringInNodes([6, 5], 4, 3); + assert.deepStrictEqual(result, { + start: { nodeIndex: 0, localOffset: 4 }, + end: { nodeIndex: 1, localOffset: 1 }, + }); + }); + + it('substring entirely in second node', () => { + const result = locateSubstringInNodes([5, 10], 7, 3); + assert.deepStrictEqual(result, { + start: { nodeIndex: 1, localOffset: 2 }, + end: { nodeIndex: 1, localOffset: 5 }, + }); + }); + + it('out of bounds: returns null', () => { + const result = locateSubstringInNodes([5, 5], 8, 5); + assert.strictEqual(result, null); + }); + + it('zero-length substring: start equals end', () => { + const result = locateSubstringInNodes([10], 5, 0); + assert.deepStrictEqual(result, { + start: { nodeIndex: 0, localOffset: 5 }, + end: { nodeIndex: 0, localOffset: 5 }, + }); + }); +});