From 5cf2ccc23a0d1cd5263290f413deb9580a9db1b9 Mon Sep 17 00:00:00 2001 From: Gustavo Carreiro Date: Wed, 25 Feb 2026 20:12:01 -0300 Subject: [PATCH] feat: Implement visual conflict resolution with CodeMirror 6 --- package-lock.json | 101 ++++++++++++ package.json | 5 + scripts/create-conflict.mjs | 85 ++++++---- src/lang/locales/en.ts | 4 + src/lang/locales/pt.ts | 4 + src/lang/locales/ru.ts | 4 + src/ui/conflict-modal.ts | 302 ++++++++++++++++++++++++------------ styles.css | 160 +++++++++++++++---- 8 files changed, 505 insertions(+), 160 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e3e447..f76214d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,11 @@ "name": "syncthing-manager", "version": "1.3.3", "license": "MIT", + "dependencies": { + "@codemirror/merge": "^6.12.0", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.15" + }, "devDependencies": { "@types/node": "^16.11.6", "builtin-modules": "3.3.0", @@ -77,6 +82,54 @@ "specificity": "bin/cli.js" } }, + "node_modules/@codemirror/language": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", + "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/merge": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@codemirror/merge/-/merge-6.12.0.tgz", + "integrity": "sha512-o+36bbapcEHf4Ux75pZ4CKjMBUd14parA0uozvWVlacaT+uxaA3DDefEvWYjngsKU+qsrDe/HOOfsw0Q72pLjA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/highlight": "^1.0.0", + "style-mod": "^4.1.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.39.15", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz", + "integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", @@ -885,6 +938,36 @@ "dev": true, "license": "MIT" }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@microsoft/eslint-plugin-sdl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@microsoft/eslint-plugin-sdl/-/eslint-plugin-sdl-1.1.0.tgz", @@ -1912,6 +1995,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5281,6 +5370,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5808,6 +5903,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index 9c5c216..727575a 100644 --- a/package.json +++ b/package.json @@ -37,5 +37,10 @@ "tslib": "2.4.0", "typescript": "^4.8.4", "vitest": "^4.0.18" + }, + "dependencies": { + "@codemirror/merge": "^6.12.0", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.15" } } diff --git a/scripts/create-conflict.mjs b/scripts/create-conflict.mjs index 239fbbb..a09de43 100644 --- a/scripts/create-conflict.mjs +++ b/scripts/create-conflict.mjs @@ -1,43 +1,68 @@ -import fs from 'fs'; -import path from 'path'; +import fs from "fs"; +import path from "path"; // --- CONFIGURAÇÃO --- -const VAULT_PATH = 'C:/Users/Gustavo/Android/dev-obsidian'; +const VAULT_PATH = "C:/Users/Gustavo/Android/dev-obsidian"; -const BASE_FILENAME = 'TesteConflito.md'; +const BASE_FILENAME = "TesteConflito.md"; function generateTimestamp() { - const now = new Date(); - // Formata para YYYYMMDD-HHMMSS (Padrão do Syncthing) - const date = now.toISOString().slice(0, 10).replace(/-/g, ''); - const time = now.toTimeString().slice(0, 8).replace(/:/g, ''); - return `${date}-${time}`; + const now = new Date(); + // Formata para YYYYMMDD-HHMMSS (Padrão do Syncthing) + const date = now.toISOString().slice(0, 10).replace(/-/g, ""); + const time = now.toTimeString().slice(0, 8).replace(/:/g, ""); + return `${date}-${time}`; } function createConflict() { - if (!fs.existsSync(VAULT_PATH)) { - console.error(`❌ Erro: O caminho do cofre não existe: ${VAULT_PATH}`); - process.exit(1); - } + if (!fs.existsSync(VAULT_PATH)) { + console.error(`❌ Erro: O caminho do cofre não existe: ${VAULT_PATH}`); + process.exit(1); + } - // 1. Cria o arquivo Original (se não existir ou sobrescreve) - const originalPath = path.join(VAULT_PATH, BASE_FILENAME); - const originalContent = `# Nota Original\n\nEsta é a versão que estava no disco.\n\n- Item A\n- Item B\n- Item C`; - - fs.writeFileSync(originalPath, originalContent); - console.log(`✅ Arquivo original criado: ${BASE_FILENAME}`); + // 1. Cria o arquivo Original (se não existir ou sobrescreve) + const originalPath = path.join(VAULT_PATH, BASE_FILENAME); + const originalContent = `# Planejamento de Reunião Diária - // 2. Cria o arquivo de Conflito - const timestamp = generateTimestamp(); - const conflictName = `TesteConflito.sync-conflict-${timestamp}-ScriptDev.md`; - const conflictPath = path.join(VAULT_PATH, conflictName); - - const conflictContent = `# Nota em Conflito\n\nEsta é a versão que veio de outro dispositivo (Conflito).\n\n- Item A\n- Item B (Modificado)\n- Item D (Novo)`; +## Tópicos do Dia (Versão Computador) +1. Discutir as metas da Sprint 42. +2. Analisar os bugs críticos relatados pelos clientes no final de semana. +3. Revisar a arquitetura do novo módulo de autenticação. - fs.writeFileSync(conflictPath, conflictContent); - console.log(`⚠️ Arquivo de conflito criado: ${conflictName}`); - - console.log('\n👉 Agora abra o Obsidian e verifique o painel lateral do plugin!'); +## Observações Gerais +Hoje o dia está bastante chuvoso. Recomendo que a equipe faça home office se possível. Não esqueçam de bater o ponto no sistema novo. + +--- +*Gerado via Syncthing Manager Tests*`; + + fs.writeFileSync(originalPath, originalContent); + console.log(`✅ Arquivo original criado: ${BASE_FILENAME}`); + + // 2. Cria o arquivo de Conflito + const timestamp = generateTimestamp(); + const conflictName = `TesteConflito.sync-conflict-${timestamp}-ScriptDev.md`; + const conflictPath = path.join(VAULT_PATH, conflictName); + + const conflictContent = `# Planejamento de Reunião Diária + +## Tópicos do Dia (Versão Celular) +1. Discutir as metas da Sprint 42. +2. URGE: Adicionar pauta sobre a queda do servidor de banco de dados hoje cedo! +3. Revisar a arquitetura do novo módulo de autenticação. +4. Alinhar folgas do mês que vem. + +## Observações Gerais +O Sol está raiando lá fora! Lembrem-se de passar protetor solar se vierem de bicicleta. Não esqueçam de bater o ponto no sistema novo (Aquele lá, o portal azul). + +--- +*Gerado via Celular (Conflito)*`; + + fs.writeFileSync(conflictPath, conflictContent); + console.log(`⚠️ Arquivo de conflito criado: ${conflictName}`); + + console.log( + "\n👉 Agora abra o Obsidian e verifique o painel lateral do plugin!", + ); } -createConflict(); \ No newline at end of file +createConflict(); diff --git a/src/lang/locales/en.ts b/src/lang/locales/en.ts index 87b7f85..3309a04 100644 --- a/src/lang/locales/en.ts +++ b/src/lang/locales/en.ts @@ -122,6 +122,10 @@ export default { btn_keep_conflict: "Use conflict version", tooltip_keep_conflict: "Overwrites your local file with the conflict version (right side).", + btn_use_this_version: "Use this version", + diff_instructions: + "Use the arrows in the middle to transfer changes to the original file. To keep one version entirely, click 'Use this version' above it.", + btn_save_merge: "Save merge", // Diff View diff_original_header: "Current file (original)", diff --git a/src/lang/locales/pt.ts b/src/lang/locales/pt.ts index bebe159..c0ed33f 100644 --- a/src/lang/locales/pt.ts +++ b/src/lang/locales/pt.ts @@ -124,6 +124,10 @@ export default { btn_keep_conflict: "Usar Versão do Conflito", tooltip_keep_conflict: "Substitui o seu arquivo local pela versão do conflito (da direita).", + btn_use_this_version: "Usar esta versão", + diff_instructions: + "Use as setas no meio para transferir alterações para o arquivo original. Para manter uma das versões integralmente, clique em 'Usar esta versão' acima dela.", + btn_save_merge: "Salvar Merge", // Visualização de Diferença diff_original_header: "Arquivo Atual (Original)", diff --git a/src/lang/locales/ru.ts b/src/lang/locales/ru.ts index 4f9e4be..a1f9c29 100644 --- a/src/lang/locales/ru.ts +++ b/src/lang/locales/ru.ts @@ -124,6 +124,10 @@ export default { btn_keep_conflict: "Использовать конфликтную", tooltip_keep_conflict: "Заменяет Ваши локальные файлы на конфликтные (правая сторона).", + btn_use_this_version: "Использовать эту версию", + diff_instructions: + "Используйте стрелки посередине для переноса изменений в исходный файл. Чтобы полностью сохранить одну из версий, нажмите 'Использовать эту версию' над ней.", + btn_save_merge: "Сохранить слияние", // Diff View diff_original_header: "Текущий файл (Оригинал)", diff --git a/src/ui/conflict-modal.ts b/src/ui/conflict-modal.ts index 68ad684..3813b51 100644 --- a/src/ui/conflict-modal.ts +++ b/src/ui/conflict-modal.ts @@ -1,125 +1,221 @@ -import { App, Modal, TFile, ButtonComponent } from 'obsidian'; -import { ConflictManager, ConflictFile } from '../services/conflict-manager'; -import { t } from '../lang/lang'; +import { App, Modal, TFile, ButtonComponent, Notice } from "obsidian"; +import { ConflictManager, ConflictFile } from "../services/conflict-manager"; +import { t } from "../lang/lang"; +import { MergeView } from "@codemirror/merge"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; export class ConflictModal extends Modal { - manager: ConflictManager; - conflicts: ConflictFile[]; - onCloseCallback: () => void; + manager: ConflictManager; + conflicts: ConflictFile[]; + onCloseCallback: () => void; - constructor(app: App, manager: ConflictManager, onCloseCallback: () => void) { - super(app); - this.manager = manager; - this.onCloseCallback = onCloseCallback; - this.conflicts = manager.getConflicts(); - } + constructor( + app: App, + manager: ConflictManager, + onCloseCallback: () => void, + ) { + super(app); + this.manager = manager; + this.onCloseCallback = onCloseCallback; + this.conflicts = manager.getConflicts(); + } - onOpen() { - const { contentEl } = this; - contentEl.empty(); - - this.modalEl.addClass('st-modal-wide'); + onOpen() { + const { contentEl } = this; + contentEl.empty(); - contentEl.createEl('h2', { text: `${t('modal_conflict_title')} (${this.conflicts.length})` }); + this.modalEl.addClass("st-modal-wide"); - if (this.conflicts.length === 0) { - contentEl.createEl('p', { text: t('modal_conflict_empty') }); - return; - } + contentEl.createEl("h2", { + text: `${t("modal_conflict_title")} (${this.conflicts.length})`, + }); - contentEl.createEl('p', { text: t('modal_conflict_desc') }); + if (this.conflicts.length === 0) { + contentEl.createEl("p", { text: t("modal_conflict_empty") }); + return; + } - this.conflicts.forEach(conflict => { - const container = contentEl.createDiv({ cls: 'conflict-item-box' }); - - container.createEl('h4', { - text: conflict.baseName, - cls: 'st-conflict-title' - }); - - container.createEl('div', { text: `Data: ${conflict.date}`, cls: 'conflict-meta' }); - container.createEl('div', { text: conflict.path, cls: 'conflict-meta-path' }); + contentEl.createEl("p", { text: t("modal_conflict_desc") }); - const previewContainer = container.createDiv(); - const actionsContainer = container.createDiv({ cls: 'conflict-actions' }); + this.conflicts.forEach((conflict) => { + const container = contentEl.createDiv({ cls: "conflict-item-box" }); - new ButtonComponent(actionsContainer) - .setButtonText(t('btn_compare')) - .setIcon('eye') - .onClick(async () => { - if (previewContainer.hasChildNodes()) { - previewContainer.empty(); - return; - } - await this.renderContentPreview(previewContainer, conflict); - }); + container.createEl("h4", { + text: conflict.baseName, + cls: "st-conflict-title", + }); - actionsContainer.createSpan({ cls: 'st-flex-grow' }); + container.createEl("div", { + text: `Data: ${conflict.date}`, + cls: "conflict-meta", + }); + container.createEl("div", { + text: conflict.path, + cls: "conflict-meta-path", + }); - new ButtonComponent(actionsContainer) - .setButtonText(t('btn_keep_original')) - .setTooltip(t('tooltip_keep_original')) - .onClick(() => { - this.manager.deleteConflict(conflict) - .then(() => this.refresh()) - .catch(console.error); - }); + const previewContainer = container.createDiv(); + const actionsContainer = container.createDiv({ + cls: "conflict-actions", + }); - new ButtonComponent(actionsContainer) - .setButtonText(t('btn_keep_conflict')) - .setCta() - .setTooltip(t('tooltip_keep_conflict')) - .onClick(() => { - this.manager.acceptConflict(conflict) - .then(() => this.refresh()) - .catch(console.error); - }); - }); - } + new ButtonComponent(actionsContainer) + .setButtonText(t("btn_compare")) + .setIcon("eye") + .onClick(async () => { + if (previewContainer.hasChildNodes()) { + previewContainer.empty(); + return; + } + await this.renderContentPreview(previewContainer, conflict); + }); + }); + } - async renderContentPreview(container: HTMLElement, conflict: ConflictFile) { - container.empty(); - const diffWrapper = container.createDiv({ cls: 'st-diff-container' }); + async renderContentPreview(container: HTMLElement, conflict: ConflictFile) { + container.empty(); - const leftBox = diffWrapper.createDiv({ cls: 'st-diff-box' }); - leftBox.createDiv({ cls: 'st-diff-header', text: t('diff_original_header') }); - const leftContent = leftBox.createDiv({ cls: 'st-diff-content st-diff-original' }); - leftContent.setText(t('diff_loading')); + const loadingEl = container.createEl("div", { + text: t("diff_loading"), + cls: "st-diff-loading", + }); + let originalText = ""; + let conflictText = ""; - const rightBox = diffWrapper.createDiv({ cls: 'st-diff-box' }); - rightBox.createDiv({ cls: 'st-diff-header', text: `${t('diff_conflict_header')} (${conflict.date})` }); - const rightContent = rightBox.createDiv({ cls: 'st-diff-content st-diff-conflict' }); - rightContent.setText(t('diff_loading')); + try { + conflictText = await this.app.vault.read(conflict.file); - try { - const conflictText = await this.app.vault.read(conflict.file); - rightContent.setText(conflictText); + const originalPath = conflict.path.replace( + conflict.file.name, + conflict.baseName, + ); + const originalFile = + this.app.vault.getAbstractFileByPath(originalPath); - const originalPath = conflict.path.replace(conflict.file.name, conflict.baseName); - const originalFile = this.app.vault.getAbstractFileByPath(originalPath); + if (originalFile instanceof TFile) { + originalText = await this.app.vault.read(originalFile); + } else { + originalText = t("diff_original_missing"); + } + } catch (error) { + console.error(error); + loadingEl.setText(t("diff_read_error")); + return; + } - if (originalFile instanceof TFile) { - const originalText = await this.app.vault.read(originalFile); - leftContent.setText(originalText); - } else { - leftContent.setText(t('diff_original_missing')); - } + loadingEl.remove(); - } catch (error) { - console.error(error); - leftContent.setText(t('diff_read_error')); - rightContent.setText(t('diff_read_error')); - } - } + // 1. Instructions + container.createEl("p", { + text: t("diff_instructions"), + cls: "st-diff-instructions", + }); - refresh() { - this.conflicts = this.manager.getConflicts(); - this.onOpen(); - } + // 2. Headers and Keep Buttons + const headersContainer = container.createDiv({ + cls: "st-diff-headers", + }); - onClose() { - const { contentEl } = this; - contentEl.empty(); - this.onCloseCallback(); - } -} \ No newline at end of file + const leftHeader = headersContainer.createDiv({ + cls: "st-diff-header-box", + }); + const leftBtn = new ButtonComponent(leftHeader) + .setButtonText(t("btn_use_this_version")) + .setTooltip(t("tooltip_keep_original")) + .onClick(() => { + this.manager + .deleteConflict(conflict) + .then(() => this.refresh()) + .catch(console.error); + }); + leftBtn.buttonEl.addClass("st-btn-accent"); + + const rightHeader = headersContainer.createDiv({ + cls: "st-diff-header-box", + }); + const rightBtn = new ButtonComponent(rightHeader) + .setButtonText(t("btn_use_this_version")) + .setTooltip(t("tooltip_keep_conflict")) + .onClick(() => { + this.manager + .acceptConflict(conflict) + .then(() => this.refresh()) + .catch(console.error); + }); + rightBtn.buttonEl.addClass("st-btn-accent"); + + // 3. Diff View Wrapper + const diffWrapper = container.createDiv({ + cls: "st-diff-view-wrapper", + }); + + const mergeView = new MergeView({ + a: { + doc: originalText, + extensions: [EditorView.lineWrapping], + }, + b: { + doc: conflictText, + extensions: [ + EditorView.lineWrapping, + EditorState.readOnly.of(true), + ], + }, + parent: diffWrapper, + revertControls: "b-to-a", // Permite puxar alterações do Conflito para o Original + }); + + // 4. Save Merge Control + const diffControlsContainer = container.createDiv({ + cls: "st-diff-controls", + }); + + diffControlsContainer.createEl("span", { + text: "Original (A) ↔ Conflito (B)", + cls: "st-diff-legend", + }); + + diffControlsContainer.createSpan({ cls: "st-flex-grow" }); + + const saveMergeBtn = new ButtonComponent(diffControlsContainer) + .setButtonText(t("btn_save_merge")) + .setTooltip( + "Sobrescrever original com estado atual da esquerda e remover o conflito", + ) + .setCta(); + + saveMergeBtn.onClick(async () => { + const mergedText = mergeView.a.state.doc.toString(); + const originalPath = conflict.path.replace( + conflict.file.name, + conflict.baseName, + ); + const originalFile = + this.app.vault.getAbstractFileByPath(originalPath); + + try { + if (originalFile instanceof TFile) { + await this.app.vault.modify(originalFile, mergedText); + await this.manager.deleteConflict(conflict); + new Notice(`Merge salvo: ${conflict.baseName}`); + this.refresh(); + } + } catch (e) { + console.error(e); + new Notice("Erro ao salvar arquivo merged."); + } + }); + } + + refresh() { + this.conflicts = this.manager.getConflicts(); + this.onOpen(); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + this.onCloseCallback(); + } +} diff --git a/styles.css b/styles.css index e64a29b..0672614 100644 --- a/styles.css +++ b/styles.css @@ -255,45 +255,151 @@ flex-grow: 1; } -/* Diff View */ -.st-diff-container { - display: flex; - gap: 10px; +/* Diff View & Merge View */ +.st-diff-instructions { + font-size: 0.85em; + color: var(--text-muted); margin-top: 15px; margin-bottom: 15px; - border-top: 1px solid var(--background-modifier-border); - padding-top: 10px; } -.st-diff-box { +.st-diff-headers { + display: flex; + gap: 10px; + margin-bottom: 5px; +} +.st-diff-header-box { flex: 1; display: flex; - flex-direction: column; - width: 50%; + justify-content: center; + align-items: center; + padding: 5px 0; } -.st-diff-header { - font-weight: bold; - margin-bottom: 5px; +.st-btn-accent { + color: var(--text-on-accent) !important; +} +.st-diff-controls { + display: flex; + align-items: center; + margin-top: 15px; + margin-bottom: 10px; + border-top: 1px solid var(--background-modifier-border); + padding-top: 15px; +} + +.cm-changeGutter { + background-color: var(--background-modifier-border) !important; +} + +.st-diff-legend { + font-size: 0.85em; color: var(--text-muted); - font-size: 0.8em; - text-transform: uppercase; + font-weight: bold; } -.st-diff-content { - background-color: var(--background-primary-alt); +.st-diff-loading { + margin: 15px 0; + color: var(--text-muted); + font-style: italic; +} +.st-diff-view-wrapper { border: 1px solid var(--background-modifier-border); - border-radius: 4px; - padding: 10px; - height: 200px; + border-radius: 6px; + background-color: var(--background-primary-alt); + overflow: hidden; +} + +/* CodeMirror Merge Styles overrides to match Obsidian Theme */ +.cm-mergeView { + max-height: 400px; overflow-y: auto; - font-family: var(--font-monospace); - font-size: 0.8em; - white-space: pre-wrap; - user-select: text; } -.st-diff-original { - border-left: 3px solid var(--text-success); +.cm-mergeView .cm-editor { + background-color: var(--background-primary) !important; + color: var(--text-normal) !important; + font-family: var(--font-monospace) !important; + font-size: 0.9em; } -.st-diff-conflict { - border-left: 3px solid var(--text-warning); + +.cm-merge-a .cm-content { + border-right: 1px solid var(--background-modifier-border); +} + +/* Highlight lines */ +.cm-merge-deleted { + background-color: rgba(var(--color-red-rgb), 0.15) !important; + text-decoration: line-through; +} +.cm-merge-inserted { + background-color: rgba(var(--color-green-rgb), 0.15) !important; +} + +/* Background chunks */ +.cm-mergeView .cm-deletedChunk { + background-color: rgba(var(--color-red-rgb), 0.08) !important; +} +.cm-mergeView .cm-insertedChunk { + background-color: rgba(var(--color-green-rgb), 0.08) !important; +} + +/* Revert buttons spacer */ +.cm-merge-spacer { + background-color: var(--background-secondary) !important; +} + +.cm-merge-revert { + background-color: var(--background-modifier-form-field) !important; + border: 1px solid var(--background-modifier-border) !important; + color: var(--text-muted) !important; + border-radius: 4px; +} + +.cm-merge-revert button { + height: 2.8vh; + display: flex; + align-items: center; + justify-content: center; +} + +.cm-mergeView { + overflow-y: auto; +} + +/* Mobile Responsiveness (Celulares) */ +@media (max-width: 600px) { + .st-diff-instructions { + font-size: 0.8em; + } + + .st-diff-header-box { + padding: 4px 2px; + } + + .st-diff-header-box button { + font-size: 0.8em; + padding: 4px 8px; + height: auto; + white-space: normal; + text-align: center; + } + + .st-diff-controls { + flex-direction: column; + align-items: stretch; + gap: 10px; + } + + .st-diff-legend { + text-align: center; + margin-bottom: 5px; + } + + .st-diff-controls button { + width: 100%; + } + + /* Ajuste forçado para displays muito pequenos do CodeMirror MergeView */ + .cm-mergeView .cm-editor { + font-size: 0.8em; + } } .st-history-container {