From f9bdfc042da2f9dd1ea73d49fb51e5ccceac6cc6 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Date: Sat, 18 Jul 2026 10:54:43 -0300 Subject: [PATCH] fix(release): address automated review findings --- README.md | 13 ++++++++++ src/ui/i18n.ts | 2 +- src/ui/view/LeifView.ts | 4 ++-- src/ui/view/components/SessionsTab.ts | 16 ++++++------- src/ui/view/registerLeifView.ts | 8 ++++--- src/ui/view/shared/DomHelpers.ts | 34 +++++++++++++-------------- styles.css | 2 -- tests/ReleaseReadiness.test.ts | 10 ++++++++ tests/mocks/obsidian.ts | 13 +++++++++- tests/ui/LeifPlugin.test.ts | 2 +- tests/ui/LeifView.test.ts | 6 ++--- tests/ui/styles.test.ts | 1 + 12 files changed, 73 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index eccba18..2353ea4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,19 @@ Leif é um plugin para [Obsidian](https://obsidian.md/) que organiza estudos par Ele reúne concursos, matérias, tópicos do edital, materiais e sessões de estudo para mostrar o que estudar agora e acompanhar o progresso sem sair do Obsidian. +## Instalação + +### Pelo Obsidian + +Quando o Leif estiver disponível no diretório oficial, abra **Configurações → Plugins da comunidade → Explorar**, procure por **Leif**, selecione **Instalar** e depois **Ativar**. + +### Instalação manual + +1. Baixe `main.js`, `manifest.json` e `styles.css` da mesma versão na página de releases. +2. Crie a pasta `/.obsidian/plugins/leif` e coloque os três arquivos nela. +3. Reinicie o Obsidian ou recarregue os plugins em **Configurações → Plugins da comunidade**. +4. Ative o **Leif**. + ## Como usar Ative o Leif em **Plugins da comunidade** e abra o painel pelo ícone da faixa lateral ou pelo comando **Abrir painel**. Crie um concurso, organize suas matérias e recursos, registre cada sessão e consulte **Hoje** para seguir o ciclo. diff --git a/src/ui/i18n.ts b/src/ui/i18n.ts index 1e34270..0701605 100644 --- a/src/ui/i18n.ts +++ b/src/ui/i18n.ts @@ -29,7 +29,7 @@ const ptBR = { export type TranslationKey = keyof typeof ptBR; -const bundle: Record = ptBR as unknown as Record; +const bundle: Record = ptBR; export function t(key: TranslationKey): string { return bundle[key] ?? key; diff --git a/src/ui/view/LeifView.ts b/src/ui/view/LeifView.ts index 4b4330b..213f1fc 100644 --- a/src/ui/view/LeifView.ts +++ b/src/ui/view/LeifView.ts @@ -133,8 +133,8 @@ export class LeifView extends ItemView { label.textContent = tab.label; button.append(icon, label); button.dataset.tab = tab.id; - button.addEventListener("click", async () => { - await this.selectTab(tab.id); + button.addEventListener("click", () => { + void this.selectTab(tab.id); }); button.setAttribute("role", "tab"); button.id = `leif-tab-${tab.id}`; diff --git a/src/ui/view/components/SessionsTab.ts b/src/ui/view/components/SessionsTab.ts index 19be4b3..3260fd5 100644 --- a/src/ui/view/components/SessionsTab.ts +++ b/src/ui/view/components/SessionsTab.ts @@ -209,21 +209,21 @@ export class SessionsTab { fromInput.dataset.sessionFilter = "from"; toInput.dataset.sessionFilter = "to"; - subjectSelect.addEventListener("change", async () => { + subjectSelect.addEventListener("change", () => { this.historySubjectFilter = subjectSelect.value; - await this.onUpdate(); + void this.onUpdate(); }); - typeSelect.addEventListener("change", async () => { + typeSelect.addEventListener("change", () => { this.historyTypeFilter = typeSelect.value; - await this.onUpdate(); + void this.onUpdate(); }); - fromInput.addEventListener("change", async () => { + fromInput.addEventListener("change", () => { this.historyFromFilter = fromInput.value; - await this.onUpdate(); + void this.onUpdate(); }); - toInput.addEventListener("change", async () => { + toInput.addEventListener("change", () => { this.historyToFilter = toInput.value; - await this.onUpdate(); + void this.onUpdate(); }); const filters = DomHelpers.createElement("div", "leif-session-filters"); diff --git a/src/ui/view/registerLeifView.ts b/src/ui/view/registerLeifView.ts index 8f00499..2d40a1a 100644 --- a/src/ui/view/registerLeifView.ts +++ b/src/ui/view/registerLeifView.ts @@ -10,9 +10,11 @@ export const LEIF_ICON = "compass"; export function registerLeifView(plugin: Obsidian.Plugin, dataStore: PluginDataStore): void { plugin.registerView(LEIF_VIEW_TYPE, (leaf) => new LeifView(leaf, dataStore)); - plugin.addRibbonIcon(LEIF_ICON, "Abrir Leif", () => openLeifView(plugin)); + plugin.addRibbonIcon(LEIF_ICON, "Abrir Leif", () => { + void openLeifView(plugin); + }); plugin.addCommand({ - id: "leif-open-view", + id: "open-view", name: t("command.openView"), callback: async () => { await openLeifView(plugin); @@ -29,5 +31,5 @@ export async function openLeifView(plugin: Obsidian.Plugin): Promise { active: true }); - await plugin.app.workspace.revealLeaf(leaf); + plugin.app.workspace.setActiveLeaf(leaf, { focus: true }); } diff --git a/src/ui/view/shared/DomHelpers.ts b/src/ui/view/shared/DomHelpers.ts index a8eafc5..51f35ce 100644 --- a/src/ui/view/shared/DomHelpers.ts +++ b/src/ui/view/shared/DomHelpers.ts @@ -15,11 +15,7 @@ export class DomHelpers { tagName: K, className?: string ): HTMLElementTagNameMap[K] { - const element = document.createElement(tagName); - if (className) { - element.className = className; - } - return element; + return createEl(tagName, className ? { cls: className } : undefined); } /** @@ -98,7 +94,7 @@ export class DomHelpers { * Creates a strong (bold) text element. */ static createStrong(text: string): HTMLElement { - const strong = document.createElement("strong"); + const strong = this.createElement("strong"); strong.textContent = text; return strong; } @@ -134,7 +130,7 @@ export class DomHelpers { * Creates an input element. */ static createInput(type: string, placeholder: string, value = ""): HTMLInputElement { - const input = document.createElement("input"); + const input = this.createElement("input"); input.type = type; input.placeholder = placeholder; input.value = value; @@ -147,10 +143,10 @@ export class DomHelpers { * @param selectedValue - Optional value to pre-select */ static createSelect(options: Array<[string, string]>, selectedValue?: string): HTMLSelectElement { - const select = document.createElement("select"); + const select = this.createElement("select"); options.forEach(([value, label]) => { - const option = document.createElement("option"); + const option = this.createElement("option"); option.value = value; option.textContent = label; if (value === selectedValue) { @@ -166,7 +162,7 @@ export class DomHelpers { * Creates a textarea element. */ static createTextarea(placeholder: string, value = ""): HTMLTextAreaElement { - const textarea = document.createElement("textarea"); + const textarea = this.createElement("textarea"); textarea.placeholder = placeholder; textarea.value = value; return textarea; @@ -302,7 +298,7 @@ export class DomHelpers { type?: "button" | "submit" | "reset"; } = {} ): HTMLButtonElement { - const button = document.createElement("button"); + const button = this.createElement("button"); button.type = options.type || "button"; button.className = options.className || ""; @@ -319,7 +315,9 @@ export class DomHelpers { } if (options.onClick) { - button.addEventListener("click", options.onClick); + button.addEventListener("click", (event) => { + void options.onClick?.(event); + }); } return button; @@ -341,7 +339,7 @@ export class DomHelpers { onClick?: (event: MouseEvent) => void | Promise; } = {} ): HTMLButtonElement { - const button = document.createElement("button"); + const button = this.createElement("button"); button.type = "button"; button.className = options.className || "clickable-icon"; button.setAttribute("aria-label", title); @@ -354,7 +352,9 @@ export class DomHelpers { } if (options.onClick) { - button.addEventListener("click", options.onClick); + button.addEventListener("click", (event) => { + void options.onClick?.(event); + }); } if (typeof setTooltip === "function") { @@ -370,11 +370,11 @@ export class DomHelpers { * Creates a form element. */ static createForm(onSubmit?: (event: Event) => void | Promise): HTMLFormElement { - const form = document.createElement("form"); + const form = this.createElement("form"); if (onSubmit) { form.addEventListener("submit", (event) => { event.preventDefault(); - onSubmit(event); + void onSubmit(event); }); } return form; @@ -393,7 +393,7 @@ export class DomHelpers { ): void { select.innerHTML = ""; options.forEach(([value, label]) => { - const option = document.createElement("option"); + const option = this.createElement("option"); option.value = value; option.textContent = label; if (selectedValue !== undefined && value === selectedValue) { diff --git a/styles.css b/styles.css index 8884c42..1d35992 100644 --- a/styles.css +++ b/styles.css @@ -1123,7 +1123,6 @@ form.leif-card, gap: 4px; overflow-x: auto; padding-bottom: 3px; - scrollbar-width: thin; } .leif-view.is-narrow .leif-tab-button { @@ -1277,7 +1276,6 @@ form.leif-card, gap: 4px; overflow-x: auto; padding-bottom: 3px; - scrollbar-width: thin; } .leif-tab-button { diff --git a/tests/ReleaseReadiness.test.ts b/tests/ReleaseReadiness.test.ts index a9a7cf6..2756e53 100644 --- a/tests/ReleaseReadiness.test.ts +++ b/tests/ReleaseReadiness.test.ts @@ -16,9 +16,19 @@ describe("community release readiness", () => { expect(versions[version]).toBe(manifest.minAppVersion); expect(manifest.description).not.toBe("A bússola do seu estudo."); expect(manifest.authorUrl).toBe("https://github.com/ttusk"); + expect(read("README.md")).toContain("## Instalação"); expect(read("README.md")).toContain("## Como usar"); }); + it("uses APIs and DOM patterns supported by the declared Obsidian version", () => { + const viewRegistration = read("src/ui/view/registerLeifView.ts"); + const domHelpers = read("src/ui/view/shared/DomHelpers.ts"); + + expect(viewRegistration).not.toContain("workspace.revealLeaf"); + expect(domHelpers).not.toContain("document.createElement"); + expect(domHelpers).not.toMatch(/addEventListener\([^,]+,\s*options\.onClick\)/); + }); + it("keeps demo and compiled artifacts out of the source repository", () => { expect(existsSync(resolve(root, "sample-vault/.obsidian/plugins/leif/main.js"))).toBe(false); expect(existsSync(resolve(root, "docs/improvements-audit.md"))).toBe(false); diff --git a/tests/mocks/obsidian.ts b/tests/mocks/obsidian.ts index 99d6c73..d5e9f97 100644 --- a/tests/mocks/obsidian.ts +++ b/tests/mocks/obsidian.ts @@ -1,6 +1,17 @@ const notices: string[] = []; const registeredIcons = new Map(); +globalThis.createEl = ((tagName, options) => { + const element = document.createElement(tagName); + const className = typeof options === "string" ? options : options?.cls; + + if (className) { + element.className = Array.isArray(className) ? className.join(" ") : className; + } + + return element; +}) as typeof createEl; + type ViewCreator = (leaf: WorkspaceLeaf) => ItemView; export class Notice { @@ -72,7 +83,7 @@ export class Workspace { return this.getLeaf(); } - async revealLeaf(_leaf: WorkspaceLeaf): Promise { + setActiveLeaf(_leaf: WorkspaceLeaf, _params?: { focus?: boolean }): void { // Test stub. } diff --git a/tests/ui/LeifPlugin.test.ts b/tests/ui/LeifPlugin.test.ts index 108e065..67b4fb6 100644 --- a/tests/ui/LeifPlugin.test.ts +++ b/tests/ui/LeifPlugin.test.ts @@ -14,7 +14,7 @@ describe("LeifPlugin", () => { const registeredPlugin = plugin as unknown as Plugin; expect(registeredPlugin.settingTabs).toHaveLength(0); - expect(registeredPlugin.commands.map((command) => command.id)).toEqual(["leif-open-view"]); + expect(registeredPlugin.commands.map((command) => command.id)).toEqual(["open-view"]); expect(registeredPlugin.commands[0]?.name).toBe("Abrir painel"); }); }); diff --git a/tests/ui/LeifView.test.ts b/tests/ui/LeifView.test.ts index 17d20b5..3d2ce0f 100644 --- a/tests/ui/LeifView.test.ts +++ b/tests/ui/LeifView.test.ts @@ -80,7 +80,7 @@ async function openLeifView(dataStore: PluginDataStore): Promise<{ const plugin = new Plugin(app); registerLeifView(plugin as never, dataStore); - const openCommand = plugin.commands.find((command) => command.id === "leif-open-view"); + const openCommand = plugin.commands.find((command) => command.id === "open-view"); if (!openCommand) { throw new Error("Open view command was not registered."); @@ -155,8 +155,8 @@ describe("LeifView", () => { expect(plugin.ribbonIcons).toHaveLength(1); expect(plugin.ribbonIcons[0]?.title).toBe("Abrir Leif"); - expect(plugin.commands.map((command) => command.id)).toContain("leif-open-view"); - expect(plugin.commands.find((command) => command.id === "leif-open-view")?.name).toBe( + expect(plugin.commands.map((command) => command.id)).toContain("open-view"); + expect(plugin.commands.find((command) => command.id === "open-view")?.name).toBe( "Abrir painel" ); expect(leaf.containerEl.textContent).toContain("Hoje"); diff --git a/tests/ui/styles.test.ts b/tests/ui/styles.test.ts index d7232b5..8e4247f 100644 --- a/tests/ui/styles.test.ts +++ b/tests/ui/styles.test.ts @@ -43,6 +43,7 @@ describe("Leif visual system", () => { expect(styles).toMatch( /\.leif-view\.is-narrow\s+\.leif-tab-bar\s*{[^}]*flex-direction:\s*row;[^}]*overflow-x:\s*auto;/s ); + expect(styles).not.toContain("scrollbar-width"); }); it("gives semantic tabs a native, visible active state", () => {