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 @@
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 `
-
-
-
-
- `;
+ // 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
@@ -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
- ? ``
- : ``;
+ const noteEscaped = escapeHtml(ann.note || '');
+ const orphaned = (ann as RemarkAnnotation & { _orphaned?: boolean })._orphaned;
+ const colorOptions = (['yellow', 'green', 'blue', 'pink'] as RemarkColor[]).map(c =>
+ ``
+ ).join('');
return `
`;
}).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 },
+ });
+ });
+});