diff --git a/main.js b/main.js index e73662f..b5216a2 100644 --- a/main.js +++ b/main.js @@ -46,7 +46,6 @@ var DEFAULT_SETTINGS = { demoMode: false, showLegend: true, tooltipStyle: "standard", - sidebarSide: "right", selectedYear: new Date().getFullYear(), sizePreset: "medium", defaultView: "year", @@ -61,7 +60,7 @@ var PRESET_SIZES = { "medium": { cell: 11, gap: 2 }, "large": { cell: 14, gap: 3 }, "fit": { cell: 0, gap: 2 } - // sentinel — calculated at render time + // sentinel - calculated at render time }; var PALETTES = { "classic": { @@ -86,10 +85,10 @@ var PALETTES = { } }; function debounce(fn, ms) { - let timer; + let timer = 0; return (...args) => { - clearTimeout(timer); - timer = setTimeout(() => fn(...args), ms); + window.clearTimeout(timer); + timer = window.setTimeout(() => fn(...args), ms); }; } async function requestDeviceCode() { @@ -522,7 +521,7 @@ var ContributionsView = class extends import_obsidian.ItemView { container.empty(); container.addClass("gh-contributions-view"); if (!this.tooltipEl) { - this.tooltipEl = document.body.createDiv({ cls: "gh-tooltip" }); + this.tooltipEl = activeDocument.body.createDiv({ cls: "gh-tooltip" }); } const s = this.plugin.settings; const needsGH = s.dataSource === "github" || s.dataSource === "both"; @@ -696,9 +695,9 @@ var ContributionsView = class extends import_obsidian.ItemView { renderYearGrid(container, weeks) { const { cell, gap } = this.getCellSize(); const graphWrap = container.createDiv({ cls: "gh-graph-wrap" }); - graphWrap.style.overflowX = "auto"; + graphWrap.setCssStyles({ overflowX: "auto" }); const monthRow = graphWrap.createDiv({ cls: "gh-month-row" }); - monthRow.style.cssText = `display:grid;grid-template-columns:repeat(${weeks.length},${cell}px);gap:${gap}px;margin-bottom:3px`; + monthRow.setCssStyles({ display: "grid", gridTemplateColumns: `repeat(${weeks.length},${cell}px)`, gap: gap + "px", marginBottom: "3px" }); let lastMonth = -1; weeks.forEach((week, wi) => { const first = week.find((d) => d.date); @@ -708,15 +707,14 @@ var ContributionsView = class extends import_obsidian.ItemView { if (m !== lastMonth) { lastMonth = m; const lbl = monthRow.createEl("span", { cls: "gh-month-lbl", text: MONTHS_SHORT[m] }); - lbl.style.gridColumn = String(wi + 1); - lbl.style.fontSize = Math.max(8, cell - 2) + "px"; + lbl.setCssStyles({ gridColumn: String(wi + 1), fontSize: Math.max(8, cell - 2) + "px" }); } }); const grid = graphWrap.createDiv({ cls: "gh-grid" }); - grid.style.cssText = `display:flex;gap:${gap}px`; + grid.setCssStyles({ display: "flex", gap: gap + "px" }); weeks.forEach((week) => { const col = grid.createDiv({ cls: "gh-col" }); - col.style.cssText = `display:flex;flex-direction:column;gap:${gap}px`; + col.setCssStyles({ display: "flex", flexDirection: "column", gap: gap + "px" }); week.forEach((day) => this.renderCell(col, day, cell)); }); } @@ -725,22 +723,22 @@ var ContributionsView = class extends import_obsidian.ItemView { const graphWrap = container.createDiv({ cls: "gh-graph-wrap" }); const colTemplate = `repeat(7,${cell}px)`; const dowRow = graphWrap.createDiv({ cls: "gh-dow-row" }); - dowRow.style.cssText = `display:grid;grid-template-columns:${colTemplate};gap:${gap}px;margin-bottom:4px`; + dowRow.setCssStyles({ display: "grid", gridTemplateColumns: colTemplate, gap: gap + "px", marginBottom: "4px" }); ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].forEach((d) => { const lbl = dowRow.createEl("span", { cls: "gh-dow-lbl", text: d }); - lbl.style.cssText = `font-size:10px;color:var(--text-faint);text-align:center;overflow:hidden`; + lbl.setCssStyles({ fontSize: "10px", color: "var(--text-faint)", textAlign: "center", overflow: "hidden" }); }); const grid = graphWrap.createDiv({ cls: "gh-month-grid" }); - grid.style.cssText = `display:flex;flex-direction:column;gap:${gap}px`; + grid.setCssStyles({ display: "flex", flexDirection: "column", gap: gap + "px" }); weeks.forEach((week) => { const row = grid.createDiv({ cls: "gh-month-row-cells" }); - row.style.cssText = `display:grid;grid-template-columns:${colTemplate};gap:${gap}px`; + row.setCssStyles({ display: "grid", gridTemplateColumns: colTemplate, gap: gap + "px" }); week.forEach((day) => this.renderCell(row, day, cell)); }); } renderCell(parent, day, size) { const cell = parent.createDiv({ cls: "gh-cell" }); - cell.style.cssText = `width:${size}px;height:${size}px`; + cell.setCssStyles({ width: size + "px", height: size + "px" }); if (!day.date) { cell.addClass("gh-cell--empty"); return; @@ -752,7 +750,7 @@ var ContributionsView = class extends import_obsidian.ItemView { cell.addEventListener("mouseenter", (e) => this.showTooltip(e, day)); cell.addEventListener("mouseleave", () => { if (this.tooltipEl) - this.tooltipEl.style.display = "none"; + this.tooltipEl.setCssStyles({ display: "none" }); }); cell.addEventListener("click", () => this.openOrCreateDailyNote(day.date)); } @@ -769,17 +767,14 @@ var ContributionsView = class extends import_obsidian.ItemView { } else { this.renderStandardTooltip(day, showSources, repoEntries); } - this.tooltipEl.style.display = "block"; - this.tooltipEl.style.visibility = "hidden"; + this.tooltipEl.setCssStyles({ display: "block", visibility: "hidden" }); const tooltipWidth = this.tooltipEl.offsetWidth || 180; const tooltipHeight = this.tooltipEl.offsetHeight || 80; const spaceOnRight = window.innerWidth - e.pageX; const left = spaceOnRight < tooltipWidth + 20 ? e.pageX - tooltipWidth - 12 : e.pageX + 12; const spaceBelow = window.innerHeight - e.pageY; const top = spaceBelow < tooltipHeight + 20 ? e.pageY - tooltipHeight - 8 : e.pageY - 34; - this.tooltipEl.style.left = left + "px"; - this.tooltipEl.style.top = top + "px"; - this.tooltipEl.style.visibility = "visible"; + this.tooltipEl.setCssStyles({ left: left + "px", top: top + "px", visibility: "visible" }); } renderSimpleTooltip(day, showSources) { const t = this.tooltipEl; @@ -797,19 +792,63 @@ var ContributionsView = class extends import_obsidian.ItemView { if (day.githubCount > 0) { const row = sources.createDiv({ cls: "gh-tip-source-row" }); const ghIco = row.createEl("span", { cls: "gh-tip-source-icon" }); - ghIco.innerHTML = ``; + ghIco.appendChild(this.makeGithubSvg()); row.createEl("span", { cls: "gh-tip-source-lbl", text: "GitHub" }); row.createEl("span", { cls: "gh-tip-source-val", text: String(day.githubCount) }); } if (day.localCount > 0) { const row = sources.createDiv({ cls: "gh-tip-source-row" }); const localIco = row.createEl("span", { cls: "gh-tip-source-icon" }); - localIco.innerHTML = ``; + localIco.appendChild(this.makeLocalSvg()); row.createEl("span", { cls: "gh-tip-source-lbl", text: "Local" }); row.createEl("span", { cls: "gh-tip-source-val", text: String(day.localCount) }); } } } + makeGithubSvg() { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", "13"); + svg.setAttribute("height", "13"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + const p1 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + p1.setAttribute("d", "M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"); + svg.appendChild(p1); + const p2 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + p2.setAttribute("d", "M9 18c-4.51 2-5-2-7-2"); + svg.appendChild(p2); + return svg; + } + makeLocalSvg() { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", "13"); + svg.setAttribute("height", "13"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + rect.setAttribute("width", "18"); + rect.setAttribute("height", "12"); + rect.setAttribute("x", "3"); + rect.setAttribute("y", "4"); + rect.setAttribute("rx", "2"); + rect.setAttribute("ry", "2"); + svg.appendChild(rect); + const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); + line.setAttribute("x1", "2"); + line.setAttribute("x2", "22"); + line.setAttribute("y1", "20"); + line.setAttribute("y2", "20"); + svg.appendChild(line); + return svg; + } renderStandardTooltip(day, showSources, repoEntries) { const t = this.tooltipEl; t.addClass("gh-tooltip--modern"); @@ -821,21 +860,19 @@ var ContributionsView = class extends import_obsidian.ItemView { const countRow = t.createDiv({ cls: "gh-tip-count-row" }); countRow.createEl("span", { cls: "gh-tip-count", text: String(day.count) }); countRow.createEl("span", { cls: "gh-tip-count-lbl", text: ` contribution${day.count !== 1 ? "s" : ""}` }); - const ghSvg = ``; - const localSvg = ``; if (showSources && (day.githubCount > 0 || day.localCount > 0)) { const sources = t.createDiv({ cls: "gh-tip-sources" }); if (day.githubCount > 0) { const row = sources.createDiv({ cls: "gh-tip-source-row" }); const ico = row.createEl("span", { cls: "gh-tip-source-icon" }); - ico.innerHTML = ghSvg; + ico.appendChild(this.makeGithubSvg()); row.createEl("span", { cls: "gh-tip-source-lbl", text: "GitHub" }); row.createEl("span", { cls: "gh-tip-source-val", text: String(day.githubCount) }); } if (day.localCount > 0) { const row = sources.createDiv({ cls: "gh-tip-source-row" }); const ico = row.createEl("span", { cls: "gh-tip-source-icon" }); - ico.innerHTML = localSvg; + ico.appendChild(this.makeLocalSvg()); row.createEl("span", { cls: "gh-tip-source-lbl", text: "Local" }); row.createEl("span", { cls: "gh-tip-source-val", text: String(day.localCount) }); } @@ -846,7 +883,7 @@ var ContributionsView = class extends import_obsidian.ItemView { repoEntries.forEach(([repo, count], i) => { const row = t.createDiv({ cls: "gh-tip-repo-row" }); const dot = row.createEl("span", { cls: "gh-tip-dot" }); - dot.style.background = repoColors[i % repoColors.length]; + dot.setCssStyles({ background: repoColors[i % repoColors.length] }); row.createEl("span", { cls: "gh-tip-repo-name", text: repo }); row.createEl("span", { cls: "gh-tip-repo-count", text: `(${count})` }); }); @@ -859,7 +896,7 @@ var ContributionsView = class extends import_obsidian.ItemView { for (let i = 0; i <= 4; i++) { const sq = legend.createDiv({ cls: "gh-cell gh-legend-cell" }); sq.dataset.level = String(i); - sq.style.cssText = `width:${cell}px;height:${cell}px`; + sq.setCssStyles({ width: cell + "px", height: cell + "px" }); } legend.createEl("span", { cls: "gh-legend-lbl", text: "More" }); } @@ -882,7 +919,7 @@ var ContributionsView = class extends import_obsidian.ItemView { const cols = this.viewMode === "year" ? 53 : 7; const rows = this.viewMode === "year" ? 7 : 6; const { cell, gap } = this.getCellSize(); - grid.style.cssText = `display:grid;grid-template-columns:repeat(${cols},${cell}px);grid-template-rows:repeat(${rows},${cell}px);gap:${gap}px`; + grid.setCssStyles({ display: "grid", gridTemplateColumns: `repeat(${cols},${cell}px)`, gridTemplateRows: `repeat(${rows},${cell}px)`, gap: gap + "px" }); for (let i = 0; i < cols * rows; i++) grid.createDiv({ cls: "gh-skeleton gh-skeleton-cell" }); } @@ -907,7 +944,8 @@ var ContributionsView = class extends import_obsidian.ItemView { return; } } - await this.app.workspace.getLeaf(false).openFile(file); + if (file instanceof import_obsidian.TFile) + await this.app.workspace.getLeaf(false).openFile(file); } }; function countToLevel(n) { @@ -929,8 +967,8 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT display() { const { containerEl } = this; containerEl.empty(); - containerEl.createEl("h2", { text: "GitHub Contributions" }); - containerEl.createEl("h3", { text: "Data Sources" }); + new import_obsidian.Setting(containerEl).setName("GitHub Contributions").setHeading(); + new import_obsidian.Setting(containerEl).setName("Data Sources").setHeading(); new import_obsidian.Setting(containerEl).setName("Source").setDesc("Which contributions to display").addDropdown( (d) => d.addOption("github", "GitHub only").addOption("local", "Local git only").addOption("both", "Both").setValue(this.plugin.settings.dataSource).onChange(async (v) => { this.plugin.settings.dataSource = v; @@ -1011,7 +1049,7 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT this.plugin.settings.githubUsername = v.trim(); await this.plugin.saveSettings(); })); - new import_obsidian.Setting(containerEl).setName("Personal Access Token").setDesc("PAT with read:user scope \u2014 github.com/settings/tokens").addText((t) => { + new import_obsidian.Setting(containerEl).setName("Personal Access Token").setDesc("PAT with read:user scope - github.com/settings/tokens").addText((t) => { t.inputEl.type = "password"; t.setPlaceholder("ghp_\u2026").setValue(this.plugin.settings.githubToken).onChange(async (v) => { this.plugin.settings.githubToken = v.trim(); @@ -1034,7 +1072,7 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT btn.setButtonText("Scanning\u2026").setDisabled(true); await this.plugin.saveSettings(); btn.setButtonText("Scan").setDisabled(false); - new import_obsidian.Notice("Repo scan complete \u2014 refresh the panel to see results"); + new import_obsidian.Notice("Repo scan complete - refresh the panel to see results"); }) ); new import_obsidian.Setting(containerEl).setName("Scan depth").setDesc("How many folder levels deep to search for git repos").addDropdown( @@ -1045,11 +1083,7 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT ); } } - containerEl.createEl("h3", { text: "Display" }); - new import_obsidian.Setting(containerEl).setName("Sidebar side").addDropdown((d) => d.addOption("left", "Left").addOption("right", "Right").setValue(this.plugin.settings.sidebarSide).onChange(async (v) => { - this.plugin.settings.sidebarSide = v; - await this.plugin.saveSettings(); - })); + new import_obsidian.Setting(containerEl).setName("Display").setHeading(); new import_obsidian.Setting(containerEl).setName("Stats display").setDesc("How the stats bar is shown").addDropdown( (d) => d.addOption("default", "Default (pills with labels)").addOption("compact", "Compact (icons + numbers)").addOption("grid", "Grid (2-column)").setValue(this.plugin.settings.statsStyle).onChange(async (v) => { this.plugin.settings.statsStyle = v; @@ -1077,13 +1111,13 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT await this.plugin.saveSettings(); } })); - new import_obsidian.Setting(containerEl).setName("Tooltip style").setDesc("Console \u2014 compact monospace. Modern \u2014 larger, with colored repo markers.").addDropdown( + new import_obsidian.Setting(containerEl).setName("Tooltip style").addDropdown( (d) => d.addOption("standard", "Standard (with repos)").addOption("simple", "Simple (no repos)").setValue(this.plugin.settings.tooltipStyle).onChange(async (v) => { this.plugin.settings.tooltipStyle = v; await this.plugin.saveSettings(); }) ); - new import_obsidian.Setting(containerEl).setName("Demo mode").setDesc("Show fake contribution data \u2014 useful for screenshots or testing").addToggle((t) => t.setValue(this.plugin.settings.demoMode).onChange(async (v) => { + new import_obsidian.Setting(containerEl).setName("Demo mode").setDesc("Show fake contribution data - useful for screenshots or testing").addToggle((t) => t.setValue(this.plugin.settings.demoMode).onChange(async (v) => { this.plugin.settings.demoMode = v; await this.plugin.saveSettings(); })); @@ -1091,7 +1125,7 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT this.plugin.settings.showLegend = v; await this.plugin.saveSettings(); })); - containerEl.createEl("h3", { text: "Daily Notes" }); + new import_obsidian.Setting(containerEl).setName("Daily Notes").setHeading(); new import_obsidian.Setting(containerEl).setName("Daily notes folder").setDesc("Leave blank for vault root").addText((t) => { t.setPlaceholder("Daily Notes/").setValue(this.plugin.settings.dailyNoteFolder); t.onChange(debounce(async (v) => { @@ -1118,16 +1152,13 @@ var GitHubContributionsPlugin = class extends import_obsidian.Plugin { this.addSettingTab(new GitHubContributionsSettingTab(this.app, this)); this.registerView(VIEW_TYPE, (leaf) => new ContributionsView(leaf, this)); this.addRibbonIcon("github", "GitHub Contributions", () => this.activateView()); - this.addCommand({ id: "open-github-contributions", name: "Open GitHub Contributions panel", callback: () => this.activateView() }); - this.injectStyles(); + this.addCommand({ id: "open-panel", name: "Open panel", callback: () => this.activateView() }); this.injectPaletteStyles(); } onunload() { - var _a, _b; - this.app.workspace.detachLeavesOfType(VIEW_TYPE); - (_a = document.getElementById("gh-contributions-styles")) == null ? void 0 : _a.remove(); - (_b = document.getElementById("gh-palette-styles")) == null ? void 0 : _b.remove(); - document.querySelectorAll(".gh-tooltip").forEach((el) => el.remove()); + var _a; + (_a = activeDocument.getElementById("gh-palette-styles")) == null ? void 0 : _a.remove(); + activeDocument.querySelectorAll(".gh-tooltip").forEach((el) => el.remove()); } async activateView() { const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE); @@ -1135,7 +1166,7 @@ var GitHubContributionsPlugin = class extends import_obsidian.Plugin { this.app.workspace.revealLeaf(existing[0]); return; } - const leaf = this.settings.sidebarSide === "left" ? this.app.workspace.getLeftLeaf(false) : this.app.workspace.getRightLeaf(false); + const leaf = this.app.workspace.getRightLeaf(false); if (leaf) { await leaf.setViewState({ type: VIEW_TYPE, active: true }); this.app.workspace.revealLeaf(leaf); @@ -1156,127 +1187,17 @@ var GitHubContributionsPlugin = class extends import_obsidian.Plugin { } injectPaletteStyles() { var _a; - const existing = document.getElementById("gh-palette-styles"); + const doc = activeDocument; + const existing = doc.getElementById("gh-palette-styles"); if (existing) existing.remove(); const p = (_a = PALETTES[this.settings.palette]) != null ? _a : PALETTES["classic"]; - const style = document.createElement("style"); + const style = doc.createElement("style"); style.id = "gh-palette-styles"; style.textContent = ` body{--gh-c0:${p.dark[0]};--gh-c1:${p.dark[1]};--gh-c2:${p.dark[2]};--gh-c3:${p.dark[3]};--gh-c4:${p.dark[4]}} body.theme-light{--gh-c0:${p.light[0]};--gh-c1:${p.light[1]};--gh-c2:${p.light[2]};--gh-c3:${p.light[3]};--gh-c4:${p.light[4]}} `; - document.head.appendChild(style); - } - injectStyles() { - if (document.getElementById("gh-contributions-styles")) - return; - const style = document.createElement("style"); - style.id = "gh-contributions-styles"; - style.textContent = ` -.gh-contributions-view{padding:12px 10px 16px;overflow-y:auto;overflow-x:hidden;user-select:none} -.gh-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;gap:6px} -.gh-username{font-size:13px;font-weight:600;color:var(--text-normal);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0} -.gh-nav{display:flex;align-items:center;gap:3px;flex-shrink:0} -.gh-nav-label{font-size:12px;color:var(--text-muted);min-width:52px;text-align:center;white-space:nowrap} -.gh-nav-btn{background:none;border:1px solid var(--background-modifier-border);border-radius:4px;color:var(--text-muted);cursor:pointer;font-size:14px;line-height:1;padding:1px 6px;transition:background .15s} -.gh-nav-btn:hover:not(:disabled){background:var(--background-modifier-hover);color:var(--text-normal)} -.gh-nav-btn:disabled{opacity:.3;cursor:default} -.gh-stats{display:flex;gap:5px;margin-bottom:10px;flex-wrap:wrap} -.gh-stats--grid{display:grid!important;grid-template-columns:1fr 1fr} -.gh-stat{display:flex;flex-direction:column;align-items:center;background:var(--background-secondary);border-radius:6px;padding:5px 7px;flex:1;min-width:0} -.gh-stats--grid .gh-stat{flex:unset} -.gh-stat--wide{grid-column:1 / -1} -.gh-stat-val{font-size:13px;font-weight:700;color:var(--interactive-accent);line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%} -.gh-stat-lbl{font-size:9px;color:var(--text-faint);text-transform:uppercase;letter-spacing:.04em;margin-top:2px;text-align:center} -.gh-stats-compact{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;align-items:center} -.gh-chip{display:inline-flex;align-items:center;gap:3px;background:var(--background-secondary);border-radius:4px;padding:3px 6px;font-size:11px;white-space:nowrap} -.gh-chip-icon{font-size:10px} -.gh-chip-val{font-weight:700;color:var(--interactive-accent)} -.gh-stats-list{display:flex;flex-direction:column;gap:1px;margin-bottom:8px} -.gh-stats-list-row{display:flex;align-items:baseline;gap:4px;font-size:11px;line-height:1.6} -.gh-stats-list-icon{font-size:10px;width:14px;text-align:center;flex-shrink:0} -.gh-stats-list-val{font-weight:700;color:var(--interactive-accent);font-size:12px} -.gh-stats-list-lbl{color:var(--text-muted);font-size:11px} -.gh-oauth-code{display:inline-block;font-size:22px;font-weight:700;letter-spacing:4px;color:var(--interactive-accent);font-family:var(--font-monospace);margin:6px 0;padding:4px 8px;background:var(--background-secondary);border-radius:6px} -.gh-repo-line{margin-bottom:6px;padding-bottom:5px;border-bottom:1px solid var(--background-modifier-border)} -.gh-repo-line-icon{font-size:10px;color:var(--text-faint)} -.gh-repo-line-name{font-size:11px;color:var(--text-muted)} -.gh-graph-wrap{overflow-x:auto;padding-bottom:4px} -.gh-month-lbl{font-size:9px;color:var(--text-faint)} -.gh-cell{border-radius:2px;cursor:pointer;transition:transform .1s;flex-shrink:0;box-sizing:border-box} -.gh-cell:hover{transform:scale(1.3);z-index:1;position:relative} -.gh-cell--empty{background:transparent!important;cursor:default!important} -.gh-cell--empty:hover{transform:none!important} -.gh-today{outline:2px solid var(--interactive-accent)!important;outline-offset:1px} -.gh-cell[data-level="0"]{background:var(--gh-c0)} -.gh-cell[data-level="1"]{background:var(--gh-c1)} -.gh-cell[data-level="2"]{background:var(--gh-c2)} -.gh-cell[data-level="3"]{background:var(--gh-c3)} -.gh-cell[data-level="4"]{background:var(--gh-c4)} -.gh-legend{display:flex;align-items:center;gap:3px;margin-top:8px;justify-content:flex-end} -.gh-legend-lbl{font-size:9px;color:var(--text-faint)} -.gh-legend-cell{cursor:default!important} -.gh-legend-cell:hover{transform:none!important} -.gh-refresh-icon{margin-left:2px;font-size:13px!important} -.gh-empty{display:flex;flex-direction:column;align-items:center;padding:24px 12px;text-align:center;gap:10px} -.gh-empty-icon{font-size:32px} -.gh-empty p{font-size:12px;color:var(--text-muted);line-height:1.5;margin:0} -.gh-btn{background:var(--interactive-accent);border:none;border-radius:6px;color:var(--text-on-accent);cursor:pointer;font-size:12px;padding:6px 14px} -.gh-btn:hover{filter:brightness(1.1)} -.gh-error{padding:10px 12px;background:var(--background-modifier-error);border-radius:6px;color:var(--text-error);font-size:12px;margin:8px 0} -.gh-skeleton-wrap{padding:4px 0} -.gh-skeleton-header{height:14px;width:55%;margin-bottom:10px;border-radius:4px} -.gh-skeleton-stats{height:44px;width:100%;margin-bottom:10px;border-radius:6px} -.gh-skeleton-cell{border-radius:2px} -.gh-skeleton{animation:gh-shimmer 1.4s infinite linear;background:linear-gradient(90deg,var(--background-modifier-border) 25%,var(--background-secondary) 50%,var(--background-modifier-border) 75%);background-size:200% 100%} -@keyframes gh-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} -.gh-tooltip{position:fixed;background:#1a1a1a;color:#eee;border-radius:6px;padding:7px 10px;font-size:11px;line-height:1.6;pointer-events:none;display:none;z-index:9999;white-space:pre;box-shadow:0 2px 10px rgba(0,0,0,.35);font-family:var(--font-monospace)} -.gh-tooltip strong{color:#fff;display:block;margin-bottom:2px;font-family:var(--font-interface)} -.theme-light .gh-tooltip{background:#222} -/* Console tooltip */ -.gh-tooltip--console{background:#161b22;border:1px solid rgba(255,255,255,0.18);border-radius:8px;padding:10px 13px;font-family:var(--font-monospace);min-width:180px;white-space:normal;box-shadow:0 0 0 1px rgba(255,255,255,0.06),0 8px 24px rgba(0,0,0,.4)} -.gh-con-date{font-size:11px;font-weight:600;color:#3fb950;margin-bottom:4px} -.gh-con-dash{border-top:1px dashed rgba(255,255,255,0.15);margin:6px 0} -.gh-con-count-row{display:flex;align-items:baseline;gap:4px;margin-bottom:5px} -.gh-con-count{font-size:15px;font-weight:700;color:#3fb950} -.gh-con-count-lbl{font-size:11px;color:#8b949e} -.gh-con-source-row{display:flex;align-items:center;gap:6px;font-size:12px;margin-bottom:3px} -.gh-con-source-icon{color:#8b949e;width:14px;display:flex;align-items:center;flex-shrink:0} -.gh-con-source-icon svg{display:block} -.gh-con-source-lbl{color:#cdd9e5;flex:1} -.gh-con-source-val{color:#fff;font-weight:700;min-width:16px;text-align:right} -.gh-con-repo-row{display:flex;align-items:center;gap:6px;font-size:11px;margin-bottom:3px} -.gh-con-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0} -.gh-con-repo-name{color:#8b949e;flex:1} -.gh-con-repo-count{color:#8b949e;font-size:10px} -.gh-con-hint{font-size:9px;color:#484f58;margin-top:6px;font-style:italic} -.gh-con-no-contrib{font-size:12px;color:#8b949e} -.gh-tooltip--modern{white-space:normal;padding:12px 14px;border-radius:10px;background:#161b22;border:1px solid rgba(255,255,255,0.12);box-shadow:0 8px 32px rgba(0,0,0,.6);min-width:190px;font-family:var(--font-interface)} -.theme-light .gh-tooltip--modern{background:#1c2128;border-color:rgba(255,255,255,0.15)} -/* Date */ -.gh-tip-date{font-size:13px;font-weight:600;color:#fff;margin-bottom:6px} -/* Count row */ -.gh-tip-count-row{display:flex;align-items:baseline;gap:4px;margin-bottom:10px} -.gh-tip-count{font-size:18px;font-weight:700;color:#3fb950} -.gh-tip-count-lbl{font-size:13px;color:#fff;margin-left:3px;font-weight:400} -/* Source section */ -.gh-tip-section{margin-bottom:8px} -.gh-tip-source-row{display:flex;align-items:center;gap:8px;font-size:13px} -.gh-tip-source-icon{color:#6e7681;width:14px;display:flex;align-items:center;flex-shrink:0} -.gh-tip-source-icon svg{display:block} -.gh-tip-source-lbl{flex:1;color:#fff;font-weight:400} -.gh-tip-source-val{color:#fff;font-weight:500;min-width:16px;text-align:right} -/* Repo rows \u2014 colored dot, monospace name, gray count */ -.gh-tip-repo-row{display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:5px} -.gh-tip-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0} -.gh-tip-repo-name{color:#fff;flex:1;font-family:var(--font-monospace);font-size:11px} -.gh-tip-repo-count{color:#6e7681;font-size:11px} -/* Divider */ -.gh-tip-divider{height:1px;background:rgba(255,255,255,0.1);margin:8px 0} -/* Hint */ -.gh-tip-no-contrib{font-size:12px;color:#6e7681} - `; - document.head.appendChild(style); + doc.head.appendChild(style); } }; diff --git a/manifest.json b/manifest.json index f82de98..f6e7228 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "github-contributions", "name": "GitHub Contributions", - "version": "1.0.0", + "version": "1.0.1", "minAppVersion": "0.15.0", "description": "View GitHub and local Git contributions directly in the sidebar with streak tracking, repository breakdowns, color palettes, and daily note integration.", "author": "gearheaded", diff --git a/package.json b/package.json index 5e802c0..9c2970a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-github-contributions", - "version": "1.0.0", + "version": "1.0.1", "description": "GitHub contribution graph for Obsidian", "main": "main.js", "scripts": { diff --git a/src/main.ts b/src/main.ts index db06644..53fbcd6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,7 @@ import { // ######################################################################## // Constants // ######################################################################## -// Unique identifier for the Obsidian sidebar view — must match registerView() call +// Unique identifier for the Obsidian sidebar view - must match registerView() call const VIEW_TYPE = "github-contributions"; // GitHub API endpoints @@ -23,11 +23,10 @@ const GITHUB_DEVICE_URL = "https://github.com/login/device/code"; const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"; const GITHUB_USER_URL = "https://api.github.com/user"; -// OAuth App Client ID — registered at github.com/settings/developers +// OAuth App Client ID - registered at github.com/settings/developers // Device flow does NOT require a client secret, only the Client ID const GITHUB_CLIENT_ID = "Ov23litfj5GbQ8mw81VV"; -const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"]; const MONTHS_SHORT = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; // ######################################################################## @@ -70,7 +69,7 @@ interface GitHubContributionsSettings { localRepoRoot: string; scanDepth: number; // 2-5, or 0 = unlimited // Display - sidebarSide: "left" | "right"; + selectedYear: number; sizePreset: SizePreset; defaultView: ViewMode; @@ -94,7 +93,7 @@ const DEFAULT_SETTINGS: GitHubContributionsSettings = { demoMode: false, showLegend: true, tooltipStyle: "standard", - sidebarSide: "right", + selectedYear: new Date().getFullYear(), sizePreset: "medium", defaultView: "year", @@ -105,14 +104,14 @@ const DEFAULT_SETTINGS: GitHubContributionsSettings = { }; // Pixel dimensions for each size preset. -// "fit" uses cell:0 as a sentinel — actual size is calculated at render time +// "fit" uses cell:0 as a sentinel - actual size is calculated at render time // based on the panel's current width via ResizeObserver. const PRESET_SIZES: Record = { "ultra-compact": { cell: 7, gap: 1 }, "compact": { cell: 9, gap: 1 }, "medium": { cell: 11, gap: 2 }, "large": { cell: 14, gap: 3 }, - "fit": { cell: 0, gap: 2 }, // sentinel — calculated at render time + "fit": { cell: 0, gap: 2 }, // sentinel - calculated at render time }; // ######################################################################## @@ -158,14 +157,14 @@ const PALETTES: Record = { // ######################################################################## // Helpers // ######################################################################## -// Generic debounce utility — delays fn execution until ms milliseconds after +// Generic debounce utility - delays fn execution until ms milliseconds after // the last call. Used on text input fields (repo root, daily notes folder, date // format) to avoid triggering expensive saves/scans on every keystroke. function debounce void>(fn: T, ms: number): T { - let timer: ReturnType; + let timer = 0; return ((...args: unknown[]) => { - clearTimeout(timer); - timer = setTimeout(() => fn(...args), ms); + window.clearTimeout(timer); + timer = window.setTimeout(() => fn(...args), ms); }) as T; } @@ -204,7 +203,7 @@ async function requestDeviceCode(): Promise { // Step 2 of OAuth device flow: poll GitHub until the user approves or the code expires. // GitHub's spec requires respecting the interval and backing off on slow_down responses. // onCancel is checked before each poll so the Cancel button works immediately. -// Network errors retry silently rather than failing — a brief connection hiccup +// Network errors retry silently rather than failing - a brief connection hiccup // shouldn't kill the whole auth flow. async function pollForToken( deviceCode: string, @@ -280,7 +279,7 @@ async function fetchGitHubUsername(token: string): Promise { // Fetches the full year's contribution calendar from GitHub's GraphQL API. // Returns a Map for easy merging with local git data. // Uses regular fetch (not requestUrl) because the GraphQL API has proper CORS headers. -// Only requests the minimum data needed — no PR/issue/review data, just the calendar. +// Only requests the minimum data needed - no PR/issue/review data, just the calendar. async function fetchGitHubContributions( username: string, token: string, @@ -315,7 +314,9 @@ async function fetchGitHubContributions( // window.require is available in Obsidian desktop (Electron) but not on mobile. // All local git functionality is gated behind this check so the plugin // loads safely on mobile without crashing. -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// window.require is only available in Obsidian desktop (Electron), not on mobile. +// This check gates all Node.js/Electron API usage safely. +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Electron exposes require on window; no typed alternative const isDesktop = !!(window as any).require; // Runs a git command in the given directory and returns stdout as a string. @@ -324,7 +325,7 @@ const isDesktop = !!(window as any).require; function runGit(args: string, cwd: string): string { if (!isDesktop) return ""; try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Electron require; no typed alternative exists const { execSync } = (window as any).require("child_process"); return execSync(`git ${args}`, { cwd, timeout: 8000, encoding: "utf8", stdio: ["pipe","pipe","pipe"] }); } catch { @@ -332,24 +333,19 @@ function runGit(args: string, cwd: string): string { } } -function isGitRepo(path: string): boolean { - const result = runGit("rev-parse --is-inside-work-tree", path); - return result.trim() === "true"; -} - // Recursively scans rootPath for git repositories up to maxDepth levels deep. // Stops recursing into a folder once a .git directory is found (repos don't nest). // Hidden folders (starting with ".") are skipped to avoid scanning .git internals. -// maxDepth === 0 means unlimited depth — warn users this can be slow on large drives. -// Uses Node's fs.readdirSync via Electron's require — desktop only. +// maxDepth === 0 means unlimited depth - warn users this can be slow on large drives. +// Uses Node's fs.readdirSync via Electron's require - desktop only. async function discoverRepos(rootPath: string, maxDepth = 2): Promise { if (!isDesktop) return []; const repos: LocalRepo[] = []; // maxDepth === 0 means unlimited try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Electron fs/path modules; no typed alternative const fs = (window as any).require("fs"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Electron fs/path modules; no typed alternative const path = (window as any).require("path"); const scanDir = (dir: string, depth: number) => { @@ -387,7 +383,7 @@ async function discoverRepos(rootPath: string, maxDepth = 2): Promise { const map = new Map(); - // Widen range by 1 day each side — git's --after/--before are exclusive + // Widen range by 1 day each side - git's --after/--before are exclusive const after = `${year - 1}-12-31`; const before = `${year + 1}-01-01`; const raw = runGit( @@ -459,7 +455,7 @@ async function buildDayMap( // Generate full year week structure // ######################################################################## // Builds the week-column structure for the year view grid. -// GitHub's contribution graph starts on Sunday — the grid is padded at the start +// GitHub's contribution graph starts on Sunday - the grid is padded at the start // with empty cells so the first real day lands in the correct column. // Returns an array of weeks, each week being 7 DayData items (Sun→Sat). // Empty padding cells have date:"" and all counts 0. @@ -517,7 +513,7 @@ function buildMonthDays(year: number, month: number, days: Map) // ######################################################################## // Calculates current and longest contribution streaks for the given year. // Current streak: consecutive active days ending today (or yesterday if today -// has no contributions yet — we don't penalise an in-progress day). +// has no contributions yet - we don't penalise an in-progress day). // Longest streak: the longest consecutive run of active days in the year. function calculateStreaks(days: Map, year: number): StreakInfo { const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; @@ -566,7 +562,7 @@ function daysSinceLastCommit(days: Map): number | null { // ######################################################################## // Generates realistic fake contribution data for demo mode / screenshots. // Uses a seeded LCG pseudo-random number generator so the output is always -// the same for a given year — the graph looks consistent across refreshes. +// the same for a given year - the graph looks consistent across refreshes. // Activity is weighted toward recent months (recencyBoost) to look natural. // Data mirrors the real DayData structure exactly: split between GitHub repos // and local repos, with repo counts guaranteed to sum to the day total. @@ -574,7 +570,7 @@ function buildDemoData(year: number): Map { const days = new Map(); const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; const daysInYear = isLeap ? 366 : 365; - // Seeded LCG — same seed = same graph every time + // Seeded LCG - same seed = same graph every time let seed = 42; const rand = () => { seed = (seed * 1664525 + 1013904223) & 0xffffffff; return (seed >>> 0) / 0xffffffff; }; @@ -593,7 +589,7 @@ function buildDemoData(year: number): Map { const ghCount = Math.floor(rand() * totalCount); const localCount = totalCount - ghCount; - // Distribute GitHub count across gh repos — always sums to ghCount + // Distribute GitHub count across gh repos - always sums to ghCount const repos: Record = {}; if (ghCount > 0) { let rem = ghCount; @@ -606,7 +602,7 @@ function buildDemoData(year: number): Map { if (rem > 0) repos[ghRepos[0]] = (repos[ghRepos[0]] ?? 0) + rem; } - // Distribute local count across local repos — always sums to localCount + // Distribute local count across local repos - always sums to localCount if (localCount > 0) { let rem = localCount; for (let ri = 0; ri < localRepos.length; ri++) { @@ -706,7 +702,7 @@ export class ContributionsView extends ItemView { container.addClass("gh-contributions-view"); if (!this.tooltipEl) { - this.tooltipEl = document.body.createDiv({ cls: "gh-tooltip" }); + this.tooltipEl = activeDocument.body.createDiv({ cls: "gh-tooltip" }); } const s = this.plugin.settings; @@ -749,7 +745,7 @@ export class ContributionsView extends ItemView { this.renderHeader(container); this.renderStats(container, { totalGH, totalLocal, streaks, sinceCommit, recentRepo }); - // Repo name line (default style only — compact shows it inline) + // Repo name line (default style only - compact shows it inline) const style = this.plugin.settings.statsStyle ?? "default"; if (recentRepo && style === "default") { const repoLine = container.createDiv({ cls: "gh-repo-line" }); @@ -889,11 +885,11 @@ export class ContributionsView extends ItemView { private renderYearGrid(container: HTMLElement, weeks: DayData[][]) { const { cell, gap } = this.getCellSize(); const graphWrap = container.createDiv({ cls: "gh-graph-wrap" }); - graphWrap.style.overflowX = "auto"; + graphWrap.setCssStyles({ overflowX: "auto" }); // Month labels const monthRow = graphWrap.createDiv({ cls: "gh-month-row" }); - monthRow.style.cssText = `display:grid;grid-template-columns:repeat(${weeks.length},${cell}px);gap:${gap}px;margin-bottom:3px`; + monthRow.setCssStyles({ display: "grid", gridTemplateColumns: `repeat(${weeks.length},${cell}px)`, gap: gap + "px", marginBottom: "3px" }); let lastMonth = -1; weeks.forEach((week, wi) => { @@ -903,17 +899,16 @@ export class ContributionsView extends ItemView { if (m !== lastMonth) { lastMonth = m; const lbl = monthRow.createEl("span", { cls: "gh-month-lbl", text: MONTHS_SHORT[m] }); - lbl.style.gridColumn = String(wi + 1); - lbl.style.fontSize = Math.max(8, cell - 2) + "px"; + lbl.setCssStyles({ gridColumn: String(wi + 1), fontSize: Math.max(8, cell - 2) + "px" }); } }); // Grid const grid = graphWrap.createDiv({ cls: "gh-grid" }); - grid.style.cssText = `display:flex;gap:${gap}px`; + grid.setCssStyles({ display: "flex", gap: gap + "px" }); weeks.forEach(week => { const col = grid.createDiv({ cls: "gh-col" }); - col.style.cssText = `display:flex;flex-direction:column;gap:${gap}px`; + col.setCssStyles({ display: "flex", flexDirection: "column", gap: gap + "px" }); week.forEach(day => this.renderCell(col, day, cell)); }); } @@ -925,25 +920,25 @@ export class ContributionsView extends ItemView { // Day-of-week headers - use same grid columns as cells so they align const dowRow = graphWrap.createDiv({ cls: "gh-dow-row" }); - dowRow.style.cssText = `display:grid;grid-template-columns:${colTemplate};gap:${gap}px;margin-bottom:4px`; + dowRow.setCssStyles({ display: "grid", gridTemplateColumns: colTemplate, gap: gap + "px", marginBottom: "4px" }); ["Su","Mo","Tu","We","Th","Fr","Sa"].forEach(d => { const lbl = dowRow.createEl("span", { cls: "gh-dow-lbl", text: d }); - lbl.style.cssText = `font-size:10px;color:var(--text-faint);text-align:center;overflow:hidden`; + lbl.setCssStyles({ fontSize: "10px", color: "var(--text-faint)", textAlign: "center", overflow: "hidden" }); }); // Grid - rows are weeks, also grid for perfect alignment const grid = graphWrap.createDiv({ cls: "gh-month-grid" }); - grid.style.cssText = `display:flex;flex-direction:column;gap:${gap}px`; + grid.setCssStyles({ display: "flex", flexDirection: "column", gap: gap + "px" }); weeks.forEach(week => { const row = grid.createDiv({ cls: "gh-month-row-cells" }); - row.style.cssText = `display:grid;grid-template-columns:${colTemplate};gap:${gap}px`; + row.setCssStyles({ display: "grid", gridTemplateColumns: colTemplate, gap: gap + "px" }); week.forEach(day => this.renderCell(row, day, cell)); }); } private renderCell(parent: HTMLElement, day: DayData, size: number) { const cell = parent.createDiv({ cls: "gh-cell" }); - cell.style.cssText = `width:${size}px;height:${size}px`; + cell.setCssStyles({ width: size + "px", height: size + "px" }); if (!day.date) { cell.addClass("gh-cell--empty"); @@ -955,7 +950,7 @@ export class ContributionsView extends ItemView { if (day.date === today) cell.addClass("gh-today"); cell.addEventListener("mouseenter", (e: MouseEvent) => this.showTooltip(e, day)); - cell.addEventListener("mouseleave", () => { if (this.tooltipEl) this.tooltipEl.style.display = "none"; }); + cell.addEventListener("mouseleave", () => { if (this.tooltipEl) this.tooltipEl.setCssStyles({ display: "none" }); }); cell.addEventListener("click", () => this.openOrCreateDailyNote(day.date)); } @@ -974,9 +969,8 @@ export class ContributionsView extends ItemView { this.renderStandardTooltip(day, showSources, repoEntries); } - // Measure then position — render hidden first to get real dimensions - this.tooltipEl.style.display = "block"; - this.tooltipEl.style.visibility = "hidden"; + // Measure then position - render hidden first to get real dimensions + this.tooltipEl.setCssStyles({ display: "block", visibility: "hidden" }); const tooltipWidth = this.tooltipEl.offsetWidth || 180; const tooltipHeight = this.tooltipEl.offsetHeight || 80; @@ -991,9 +985,7 @@ export class ContributionsView extends ItemView { ? e.pageY - tooltipHeight - 8 : e.pageY - 34; - this.tooltipEl.style.left = left + "px"; - this.tooltipEl.style.top = top + "px"; - this.tooltipEl.style.visibility = "visible"; + this.tooltipEl.setCssStyles({ left: left + "px", top: top + "px", visibility: "visible" }); } private renderSimpleTooltip(day: DayData, showSources: boolean) { @@ -1016,20 +1008,61 @@ export class ContributionsView extends ItemView { if (day.githubCount > 0) { const row = sources.createDiv({ cls: "gh-tip-source-row" }); const ghIco = row.createEl("span", { cls: "gh-tip-source-icon" }); - ghIco.innerHTML = ``; + ghIco.appendChild(this.makeGithubSvg()); row.createEl("span", { cls: "gh-tip-source-lbl", text: "GitHub" }); row.createEl("span", { cls: "gh-tip-source-val", text: String(day.githubCount) }); } if (day.localCount > 0) { const row = sources.createDiv({ cls: "gh-tip-source-row" }); const localIco = row.createEl("span", { cls: "gh-tip-source-icon" }); - localIco.innerHTML = ``; + localIco.appendChild(this.makeLocalSvg()); row.createEl("span", { cls: "gh-tip-source-lbl", text: "Local" }); row.createEl("span", { cls: "gh-tip-source-val", text: String(day.localCount) }); } } } + private makeGithubSvg(): SVGElement { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", "13"); + svg.setAttribute("height", "13"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + const p1 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + p1.setAttribute("d", "M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"); + svg.appendChild(p1); + const p2 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + p2.setAttribute("d", "M9 18c-4.51 2-5-2-7-2"); + svg.appendChild(p2); + return svg; + } + + private makeLocalSvg(): SVGElement { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", "13"); + svg.setAttribute("height", "13"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + rect.setAttribute("width", "18"); rect.setAttribute("height", "12"); + rect.setAttribute("x", "3"); rect.setAttribute("y", "4"); + rect.setAttribute("rx", "2"); rect.setAttribute("ry", "2"); + svg.appendChild(rect); + const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); + line.setAttribute("x1", "2"); line.setAttribute("x2", "22"); + line.setAttribute("y1", "20"); line.setAttribute("y2", "20"); + svg.appendChild(line); + return svg; + } + private renderStandardTooltip(day: DayData, showSources: boolean, repoEntries: [string, number][]) { const t = this.tooltipEl!; t.addClass("gh-tooltip--modern"); @@ -1045,22 +1078,19 @@ export class ContributionsView extends ItemView { countRow.createEl("span", { cls: "gh-tip-count", text: String(day.count) }); countRow.createEl("span", { cls: "gh-tip-count-lbl", text: ` contribution${day.count !== 1 ? "s" : ""}` }); - const ghSvg = ``; - const localSvg = ``; - if (showSources && (day.githubCount > 0 || day.localCount > 0)) { const sources = t.createDiv({ cls: "gh-tip-sources" }); if (day.githubCount > 0) { const row = sources.createDiv({ cls: "gh-tip-source-row" }); const ico = row.createEl("span", { cls: "gh-tip-source-icon" }); - ico.innerHTML = ghSvg; + ico.appendChild(this.makeGithubSvg()); row.createEl("span", { cls: "gh-tip-source-lbl", text: "GitHub" }); row.createEl("span", { cls: "gh-tip-source-val", text: String(day.githubCount) }); } if (day.localCount > 0) { const row = sources.createDiv({ cls: "gh-tip-source-row" }); const ico = row.createEl("span", { cls: "gh-tip-source-icon" }); - ico.innerHTML = localSvg; + ico.appendChild(this.makeLocalSvg()); row.createEl("span", { cls: "gh-tip-source-lbl", text: "Local" }); row.createEl("span", { cls: "gh-tip-source-val", text: String(day.localCount) }); } @@ -1072,7 +1102,7 @@ export class ContributionsView extends ItemView { repoEntries.forEach(([repo, count], i) => { const row = t.createDiv({ cls: "gh-tip-repo-row" }); const dot = row.createEl("span", { cls: "gh-tip-dot" }); - dot.style.background = repoColors[i % repoColors.length]; + dot.setCssStyles({ background: repoColors[i % repoColors.length] }); row.createEl("span", { cls: "gh-tip-repo-name", text: repo }); row.createEl("span", { cls: "gh-tip-repo-count", text: `(${count})` }); }); @@ -1087,7 +1117,7 @@ export class ContributionsView extends ItemView { for (let i = 0; i <= 4; i++) { const sq = legend.createDiv({ cls: "gh-cell gh-legend-cell" }); sq.dataset.level = String(i); - sq.style.cssText = `width:${cell}px;height:${cell}px`; + sq.setCssStyles({ width: cell + "px", height: cell + "px" }); } legend.createEl("span", { cls: "gh-legend-lbl", text: "More" }); } @@ -1116,7 +1146,7 @@ export class ContributionsView extends ItemView { const cols = this.viewMode === "year" ? 53 : 7; const rows = this.viewMode === "year" ? 7 : 6; const { cell, gap } = this.getCellSize(); - grid.style.cssText = `display:grid;grid-template-columns:repeat(${cols},${cell}px);grid-template-rows:repeat(${rows},${cell}px);gap:${gap}px`; + grid.setCssStyles({ display: "grid", gridTemplateColumns: `repeat(${cols},${cell}px)`, gridTemplateRows: `repeat(${rows},${cell}px)`, gap: gap + "px" }); for (let i = 0; i < cols * rows; i++) grid.createDiv({ cls: "gh-skeleton gh-skeleton-cell" }); } @@ -1141,7 +1171,7 @@ export class ContributionsView extends ItemView { return; } } - await this.app.workspace.getLeaf(false).openFile(file as TFile); + if (file instanceof TFile) await this.app.workspace.getLeaf(false).openFile(file); } } @@ -1151,7 +1181,7 @@ export class ContributionsView extends ItemView { // Maps a contribution count to a colour level 0–4. // These thresholds roughly mirror GitHub's own contribution graph levels. // Level 0 = empty, level 4 = most active. Adjust thresholds here to change -// how aggressively the graph colours — useful if your commit volume is very +// how aggressively the graph colours - useful if your commit volume is very // high or very low compared to the defaults. function countToLevel(n: number): number { if (n === 0) return 0; @@ -1174,10 +1204,10 @@ class GitHubContributionsSettingTab extends PluginSettingTab { display(): void { const { containerEl } = this; containerEl.empty(); - containerEl.createEl("h2", { text: "GitHub Contributions" }); + new Setting(containerEl).setName("GitHub Contributions").setHeading(); // ── Data Sources - containerEl.createEl("h3", { text: "Data Sources" }); + new Setting(containerEl).setName("Data Sources").setHeading(); new Setting(containerEl) .setName("Source") @@ -1294,7 +1324,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab { new Setting(containerEl) .setName("Personal Access Token") - .setDesc("PAT with read:user scope — github.com/settings/tokens") + .setDesc("PAT with read:user scope - github.com/settings/tokens") .addText(t => { t.inputEl.type = "password"; t.setPlaceholder("ghp_…").setValue(this.plugin.settings.githubToken) @@ -1322,7 +1352,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab { btn.setButtonText("Scanning…").setDisabled(true); await this.plugin.saveSettings(); btn.setButtonText("Scan").setDisabled(false); - new Notice("Repo scan complete — refresh the panel to see results"); + new Notice("Repo scan complete - refresh the panel to see results"); }) ); @@ -1342,13 +1372,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab { } // ── Display - containerEl.createEl("h3", { text: "Display" }); - - new Setting(containerEl) - .setName("Sidebar side") - .addDropdown(d => d.addOption("left","Left").addOption("right","Right") - .setValue(this.plugin.settings.sidebarSide) - .onChange(async v => { this.plugin.settings.sidebarSide = v as "left"|"right"; await this.plugin.saveSettings(); })); + new Setting(containerEl).setName("Display").setHeading(); new Setting(containerEl) .setName("Stats display") @@ -1406,7 +1430,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab { // ── Daily Notes new Setting(containerEl) .setName("Tooltip style") - .setDesc("Console — compact monospace. Modern — larger, with colored repo markers.") + .addDropdown(d => d .addOption("standard", "Standard (with repos)") .addOption("simple", "Simple (no repos)") @@ -1416,7 +1440,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab { new Setting(containerEl) .setName("Demo mode") - .setDesc("Show fake contribution data — useful for screenshots or testing") + .setDesc("Show fake contribution data - useful for screenshots or testing") .addToggle(t => t.setValue(this.plugin.settings.demoMode) .onChange(async v => { this.plugin.settings.demoMode = v; await this.plugin.saveSettings(); })); @@ -1426,7 +1450,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab { .addToggle(t => t.setValue(this.plugin.settings.showLegend) .onChange(async v => { this.plugin.settings.showLegend = v; await this.plugin.saveSettings(); })); - containerEl.createEl("h3", { text: "Daily Notes" }); + new Setting(containerEl).setName("Daily Notes").setHeading(); new Setting(containerEl) .setName("Daily notes folder") @@ -1457,24 +1481,19 @@ export default class GitHubContributionsPlugin extends Plugin { this.addSettingTab(new GitHubContributionsSettingTab(this.app, this)); this.registerView(VIEW_TYPE, (leaf: WorkspaceLeaf) => new ContributionsView(leaf, this)); this.addRibbonIcon("github", "GitHub Contributions", () => this.activateView()); - this.addCommand({ id: "open-github-contributions", name: "Open GitHub Contributions panel", callback: () => this.activateView() }); - this.injectStyles(); + this.addCommand({ id: "open-panel", name: "Open panel", callback: () => this.activateView() }); this.injectPaletteStyles(); } onunload() { - this.app.workspace.detachLeavesOfType(VIEW_TYPE); - document.getElementById("gh-contributions-styles")?.remove(); - document.getElementById("gh-palette-styles")?.remove(); - document.querySelectorAll(".gh-tooltip").forEach(el => el.remove()); + activeDocument.getElementById("gh-palette-styles")?.remove(); + activeDocument.querySelectorAll(".gh-tooltip").forEach(el => el.remove()); } async activateView() { const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE); if (existing.length > 0) { this.app.workspace.revealLeaf(existing[0]); return; } - const leaf = this.settings.sidebarSide === "left" - ? this.app.workspace.getLeftLeaf(false) - : this.app.workspace.getRightLeaf(false); + const leaf = this.app.workspace.getRightLeaf(false); if (leaf) { await leaf.setViewState({ type: VIEW_TYPE, active: true }); this.app.workspace.revealLeaf(leaf); } } @@ -1492,127 +1511,18 @@ export default class GitHubContributionsPlugin extends Plugin { } injectPaletteStyles() { - const existing = document.getElementById("gh-palette-styles"); + const doc = activeDocument; + const existing = doc.getElementById("gh-palette-styles"); if (existing) existing.remove(); const p = PALETTES[this.settings.palette] ?? PALETTES["classic"]; - const style = document.createElement("style"); + const style = doc.createElement("style"); style.id = "gh-palette-styles"; // Use body instead of :root for reliability in Obsidian's DOM style.textContent = ` body{--gh-c0:${p.dark[0]};--gh-c1:${p.dark[1]};--gh-c2:${p.dark[2]};--gh-c3:${p.dark[3]};--gh-c4:${p.dark[4]}} body.theme-light{--gh-c0:${p.light[0]};--gh-c1:${p.light[1]};--gh-c2:${p.light[2]};--gh-c3:${p.light[3]};--gh-c4:${p.light[4]}} `; - document.head.appendChild(style); + doc.head.appendChild(style); } - private injectStyles() { - if (document.getElementById("gh-contributions-styles")) return; - const style = document.createElement("style"); - style.id = "gh-contributions-styles"; - style.textContent = ` -.gh-contributions-view{padding:12px 10px 16px;overflow-y:auto;overflow-x:hidden;user-select:none} -.gh-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;gap:6px} -.gh-username{font-size:13px;font-weight:600;color:var(--text-normal);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0} -.gh-nav{display:flex;align-items:center;gap:3px;flex-shrink:0} -.gh-nav-label{font-size:12px;color:var(--text-muted);min-width:52px;text-align:center;white-space:nowrap} -.gh-nav-btn{background:none;border:1px solid var(--background-modifier-border);border-radius:4px;color:var(--text-muted);cursor:pointer;font-size:14px;line-height:1;padding:1px 6px;transition:background .15s} -.gh-nav-btn:hover:not(:disabled){background:var(--background-modifier-hover);color:var(--text-normal)} -.gh-nav-btn:disabled{opacity:.3;cursor:default} -.gh-stats{display:flex;gap:5px;margin-bottom:10px;flex-wrap:wrap} -.gh-stats--grid{display:grid!important;grid-template-columns:1fr 1fr} -.gh-stat{display:flex;flex-direction:column;align-items:center;background:var(--background-secondary);border-radius:6px;padding:5px 7px;flex:1;min-width:0} -.gh-stats--grid .gh-stat{flex:unset} -.gh-stat--wide{grid-column:1 / -1} -.gh-stat-val{font-size:13px;font-weight:700;color:var(--interactive-accent);line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%} -.gh-stat-lbl{font-size:9px;color:var(--text-faint);text-transform:uppercase;letter-spacing:.04em;margin-top:2px;text-align:center} -.gh-stats-compact{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;align-items:center} -.gh-chip{display:inline-flex;align-items:center;gap:3px;background:var(--background-secondary);border-radius:4px;padding:3px 6px;font-size:11px;white-space:nowrap} -.gh-chip-icon{font-size:10px} -.gh-chip-val{font-weight:700;color:var(--interactive-accent)} -.gh-stats-list{display:flex;flex-direction:column;gap:1px;margin-bottom:8px} -.gh-stats-list-row{display:flex;align-items:baseline;gap:4px;font-size:11px;line-height:1.6} -.gh-stats-list-icon{font-size:10px;width:14px;text-align:center;flex-shrink:0} -.gh-stats-list-val{font-weight:700;color:var(--interactive-accent);font-size:12px} -.gh-stats-list-lbl{color:var(--text-muted);font-size:11px} -.gh-oauth-code{display:inline-block;font-size:22px;font-weight:700;letter-spacing:4px;color:var(--interactive-accent);font-family:var(--font-monospace);margin:6px 0;padding:4px 8px;background:var(--background-secondary);border-radius:6px} -.gh-repo-line{margin-bottom:6px;padding-bottom:5px;border-bottom:1px solid var(--background-modifier-border)} -.gh-repo-line-icon{font-size:10px;color:var(--text-faint)} -.gh-repo-line-name{font-size:11px;color:var(--text-muted)} -.gh-graph-wrap{overflow-x:auto;padding-bottom:4px} -.gh-month-lbl{font-size:9px;color:var(--text-faint)} -.gh-cell{border-radius:2px;cursor:pointer;transition:transform .1s;flex-shrink:0;box-sizing:border-box} -.gh-cell:hover{transform:scale(1.3);z-index:1;position:relative} -.gh-cell--empty{background:transparent!important;cursor:default!important} -.gh-cell--empty:hover{transform:none!important} -.gh-today{outline:2px solid var(--interactive-accent)!important;outline-offset:1px} -.gh-cell[data-level="0"]{background:var(--gh-c0)} -.gh-cell[data-level="1"]{background:var(--gh-c1)} -.gh-cell[data-level="2"]{background:var(--gh-c2)} -.gh-cell[data-level="3"]{background:var(--gh-c3)} -.gh-cell[data-level="4"]{background:var(--gh-c4)} -.gh-legend{display:flex;align-items:center;gap:3px;margin-top:8px;justify-content:flex-end} -.gh-legend-lbl{font-size:9px;color:var(--text-faint)} -.gh-legend-cell{cursor:default!important} -.gh-legend-cell:hover{transform:none!important} -.gh-refresh-icon{margin-left:2px;font-size:13px!important} -.gh-empty{display:flex;flex-direction:column;align-items:center;padding:24px 12px;text-align:center;gap:10px} -.gh-empty-icon{font-size:32px} -.gh-empty p{font-size:12px;color:var(--text-muted);line-height:1.5;margin:0} -.gh-btn{background:var(--interactive-accent);border:none;border-radius:6px;color:var(--text-on-accent);cursor:pointer;font-size:12px;padding:6px 14px} -.gh-btn:hover{filter:brightness(1.1)} -.gh-error{padding:10px 12px;background:var(--background-modifier-error);border-radius:6px;color:var(--text-error);font-size:12px;margin:8px 0} -.gh-skeleton-wrap{padding:4px 0} -.gh-skeleton-header{height:14px;width:55%;margin-bottom:10px;border-radius:4px} -.gh-skeleton-stats{height:44px;width:100%;margin-bottom:10px;border-radius:6px} -.gh-skeleton-cell{border-radius:2px} -.gh-skeleton{animation:gh-shimmer 1.4s infinite linear;background:linear-gradient(90deg,var(--background-modifier-border) 25%,var(--background-secondary) 50%,var(--background-modifier-border) 75%);background-size:200% 100%} -@keyframes gh-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} -.gh-tooltip{position:fixed;background:#1a1a1a;color:#eee;border-radius:6px;padding:7px 10px;font-size:11px;line-height:1.6;pointer-events:none;display:none;z-index:9999;white-space:pre;box-shadow:0 2px 10px rgba(0,0,0,.35);font-family:var(--font-monospace)} -.gh-tooltip strong{color:#fff;display:block;margin-bottom:2px;font-family:var(--font-interface)} -.theme-light .gh-tooltip{background:#222} -/* Console tooltip */ -.gh-tooltip--console{background:#161b22;border:1px solid rgba(255,255,255,0.18);border-radius:8px;padding:10px 13px;font-family:var(--font-monospace);min-width:180px;white-space:normal;box-shadow:0 0 0 1px rgba(255,255,255,0.06),0 8px 24px rgba(0,0,0,.4)} -.gh-con-date{font-size:11px;font-weight:600;color:#3fb950;margin-bottom:4px} -.gh-con-dash{border-top:1px dashed rgba(255,255,255,0.15);margin:6px 0} -.gh-con-count-row{display:flex;align-items:baseline;gap:4px;margin-bottom:5px} -.gh-con-count{font-size:15px;font-weight:700;color:#3fb950} -.gh-con-count-lbl{font-size:11px;color:#8b949e} -.gh-con-source-row{display:flex;align-items:center;gap:6px;font-size:12px;margin-bottom:3px} -.gh-con-source-icon{color:#8b949e;width:14px;display:flex;align-items:center;flex-shrink:0} -.gh-con-source-icon svg{display:block} -.gh-con-source-lbl{color:#cdd9e5;flex:1} -.gh-con-source-val{color:#fff;font-weight:700;min-width:16px;text-align:right} -.gh-con-repo-row{display:flex;align-items:center;gap:6px;font-size:11px;margin-bottom:3px} -.gh-con-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0} -.gh-con-repo-name{color:#8b949e;flex:1} -.gh-con-repo-count{color:#8b949e;font-size:10px} -.gh-con-hint{font-size:9px;color:#484f58;margin-top:6px;font-style:italic} -.gh-con-no-contrib{font-size:12px;color:#8b949e} -.gh-tooltip--modern{white-space:normal;padding:12px 14px;border-radius:10px;background:#161b22;border:1px solid rgba(255,255,255,0.12);box-shadow:0 8px 32px rgba(0,0,0,.6);min-width:190px;font-family:var(--font-interface)} -.theme-light .gh-tooltip--modern{background:#1c2128;border-color:rgba(255,255,255,0.15)} -/* Date */ -.gh-tip-date{font-size:13px;font-weight:600;color:#fff;margin-bottom:6px} -/* Count row */ -.gh-tip-count-row{display:flex;align-items:baseline;gap:4px;margin-bottom:10px} -.gh-tip-count{font-size:18px;font-weight:700;color:#3fb950} -.gh-tip-count-lbl{font-size:13px;color:#fff;margin-left:3px;font-weight:400} -/* Source section */ -.gh-tip-section{margin-bottom:8px} -.gh-tip-source-row{display:flex;align-items:center;gap:8px;font-size:13px} -.gh-tip-source-icon{color:#6e7681;width:14px;display:flex;align-items:center;flex-shrink:0} -.gh-tip-source-icon svg{display:block} -.gh-tip-source-lbl{flex:1;color:#fff;font-weight:400} -.gh-tip-source-val{color:#fff;font-weight:500;min-width:16px;text-align:right} -/* Repo rows — colored dot, monospace name, gray count */ -.gh-tip-repo-row{display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:5px} -.gh-tip-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0} -.gh-tip-repo-name{color:#fff;flex:1;font-family:var(--font-monospace);font-size:11px} -.gh-tip-repo-count{color:#6e7681;font-size:11px} -/* Divider */ -.gh-tip-divider{height:1px;background:rgba(255,255,255,0.1);margin:8px 0} -/* Hint */ -.gh-tip-no-contrib{font-size:12px;color:#6e7681} - `; - document.head.appendChild(style); - } } diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..f2fc6bb --- /dev/null +++ b/styles.css @@ -0,0 +1,194 @@ +/* ######################################################################## + GitHub Contributions Plugin — styles.css + Obsidian loads this automatically. Do not inject styles via JS. +######################################################################## */ + +/* ── View container ── */ +.gh-contributions-view { + padding: 12px 10px 16px; + overflow-y: auto; + overflow-x: hidden; + user-select: none; +} + +/* ── Header ── */ +.gh-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + gap: 6px; +} +.gh-username { + font-size: 13px; + font-weight: 600; + color: var(--text-normal); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} + +/* ── Nav buttons ── */ +.gh-nav { display: flex; align-items: center; gap: 3px; flex-shrink: 0; } +.gh-nav-label { font-size: 12px; color: var(--text-muted); min-width: 52px; text-align: center; white-space: nowrap; } +.gh-nav-btn { + background: none; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + color: var(--text-muted); + cursor: pointer; + font-size: 14px; + line-height: 1; + padding: 1px 6px; + transition: background .15s; +} +.gh-nav-btn:hover:not(:disabled) { background: var(--background-modifier-hover); color: var(--text-normal); } +.gh-nav-btn:disabled { opacity: .3; cursor: default; } +.gh-refresh-icon { margin-left: 2px; font-size: 13px !important; } + +/* ── Stats — default flex row ── */ +.gh-stats { display: flex; gap: 5px; margin-bottom: 10px; flex-wrap: wrap; } +.gh-stats--grid { display: grid !important; grid-template-columns: 1fr 1fr; } +.gh-stat { + display: flex; + flex-direction: column; + align-items: center; + background: var(--background-secondary); + border-radius: 6px; + padding: 5px 7px; + flex: 1; + min-width: 0; +} +.gh-stats--grid .gh-stat { flex: unset; } +.gh-stat--wide { grid-column: 1 / -1; } +.gh-stat-val { font-size: 13px; font-weight: 700; color: var(--interactive-accent); line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; } +.gh-stat-lbl { font-size: 9px; color: var(--text-faint); text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; text-align: center; } + +/* ── Stats — compact chips ── */ +.gh-stats-compact { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; align-items: center; } +.gh-chip { display: inline-flex; align-items: center; gap: 3px; background: var(--background-secondary); border-radius: 4px; padding: 3px 6px; font-size: 11px; white-space: nowrap; } +.gh-chip-icon { font-size: 10px; } +.gh-chip-val { font-weight: 700; color: var(--interactive-accent); } + +/* ── Stats — list style ── */ +.gh-stats-list { display: flex; flex-direction: column; gap: 1px; margin-bottom: 8px; } +.gh-stats-list-row { display: flex; align-items: baseline; gap: 4px; font-size: 11px; line-height: 1.6; } +.gh-stats-list-icon { font-size: 10px; width: 14px; text-align: center; flex-shrink: 0; } +.gh-stats-list-val { font-weight: 700; color: var(--interactive-accent); font-size: 12px; } +.gh-stats-list-lbl { color: var(--text-muted); font-size: 11px; } + +/* ── Repo line ── */ +.gh-repo-line { margin-bottom: 6px; padding-bottom: 5px; border-bottom: 1px solid var(--background-modifier-border); } +.gh-repo-line-icon { font-size: 10px; color: var(--text-faint); } +.gh-repo-line-name { font-size: 11px; color: var(--text-muted); } + +/* ── OAuth code display ── */ +.gh-oauth-code { + display: inline-block; + font-size: 22px; + font-weight: 700; + letter-spacing: 4px; + color: var(--interactive-accent); + font-family: var(--font-monospace); + margin: 6px 0; + padding: 4px 8px; + background: var(--background-secondary); + border-radius: 6px; +} + +/* ── Graph ── */ +.gh-graph-wrap { overflow-x: auto; padding-bottom: 4px; } +.gh-month-lbl { font-size: 9px; color: var(--text-faint); } +.gh-dow-lbl { font-size: 10px; color: var(--text-faint); text-align: center; overflow: hidden; } + +/* ── Cells ── */ +.gh-cell { + border-radius: 2px; + cursor: pointer; + transition: transform .1s; + flex-shrink: 0; + box-sizing: border-box; +} +.gh-cell:hover { transform: scale(1.3); z-index: 1; position: relative; } +.gh-cell--empty { background: transparent !important; cursor: default !important; } +.gh-cell--empty:hover { transform: none !important; } +.gh-today { outline: 2px solid var(--interactive-accent) !important; outline-offset: 1px; } + +/* Contribution level colours — set via CSS vars updated by injectPaletteStyles() */ +.gh-cell[data-level="0"] { background: var(--gh-c0); } +.gh-cell[data-level="1"] { background: var(--gh-c1); } +.gh-cell[data-level="2"] { background: var(--gh-c2); } +.gh-cell[data-level="3"] { background: var(--gh-c3); } +.gh-cell[data-level="4"] { background: var(--gh-c4); } + +/* ── Legend ── */ +.gh-legend { display: flex; align-items: center; gap: 3px; margin-top: 8px; justify-content: flex-end; } +.gh-legend-lbl { font-size: 9px; color: var(--text-faint); } +.gh-legend-cell { cursor: default !important; } +.gh-legend-cell:hover { transform: none !important; } + +/* ── Empty state ── */ +.gh-empty { display: flex; flex-direction: column; align-items: center; padding: 24px 12px; text-align: center; gap: 10px; } +.gh-empty-icon { font-size: 32px; } +.gh-empty p { font-size: 12px; color: var(--text-muted); line-height: 1.5; margin: 0; } +.gh-btn { background: var(--interactive-accent); border: none; border-radius: 6px; color: var(--text-on-accent); cursor: pointer; font-size: 12px; padding: 6px 14px; } +.gh-btn:hover { filter: brightness(1.1); } + +/* ── Error state ── */ +.gh-error { padding: 10px 12px; background: var(--background-modifier-error); border-radius: 6px; color: var(--text-error); font-size: 12px; margin: 8px 0; } + +/* ── Skeleton loader ── */ +.gh-skeleton-wrap { padding: 4px 0; } +.gh-skeleton-header { height: 14px; width: 55%; margin-bottom: 10px; border-radius: 4px; } +.gh-skeleton-stats { height: 44px; width: 100%; margin-bottom: 10px; border-radius: 6px; } +.gh-skeleton-cell { border-radius: 2px; } +.gh-skeleton { + animation: gh-shimmer 1.4s infinite linear; + background: linear-gradient(90deg, var(--background-modifier-border) 25%, var(--background-secondary) 50%, var(--background-modifier-border) 75%); + background-size: 200% 100%; +} +@keyframes gh-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* ── Tooltip — base ── */ +.gh-tooltip { + position: fixed; + pointer-events: none; + display: none; + z-index: 9999; +} + +/* ── Tooltip — Standard / Simple (modern card style) ── */ +.gh-tooltip--modern { + white-space: normal; + padding: 12px 14px; + border-radius: 10px; + background: #161b22; + border: 1px solid rgba(255,255,255,0.12); + box-shadow: 0 8px 32px rgba(0,0,0,.6); + min-width: 190px; + font-family: var(--font-interface); +} +.theme-light .gh-tooltip--modern { background: #1c2128; border-color: rgba(255,255,255,0.15); } +.gh-tip-date { font-size: 13px; font-weight: 600; color: #fff; margin-bottom: 6px; } +.gh-tip-count-row { display: flex; align-items: baseline; gap: 4px; margin-bottom: 10px; } +.gh-tip-count { font-size: 18px; font-weight: 700; color: #3fb950; } +.gh-tip-count-lbl { font-size: 13px; color: #fff; margin-left: 3px; font-weight: 400; } +.gh-tip-sources { display: flex; flex-direction: column; gap: 5px; margin-bottom: 4px; } +.gh-tip-source-row { display: flex; align-items: center; gap: 8px; font-size: 13px; } +.gh-tip-source-icon { color: #6e7681; width: 14px; display: flex; align-items: center; flex-shrink: 0; } +.gh-tip-source-lbl { flex: 1; color: #fff; font-weight: 400; } +.gh-tip-source-val { color: #fff; font-weight: 500; min-width: 16px; text-align: right; } +.gh-tip-divider { height: 1px; background: rgba(255,255,255,0.1); margin: 8px 0; } +.gh-tip-repo-row { display: flex; align-items: center; gap: 8px; font-size: 12px; margin-bottom: 5px; } +.gh-tip-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; } +.gh-tip-repo-name { color: #fff; flex: 1; font-family: var(--font-monospace); font-size: 11px; } +.gh-tip-repo-count { color: #6e7681; font-size: 11px; } +.gh-tip-no-contrib { font-size: 12px; color: #6e7681; } + +/* ── Settings notice (mobile warning) ── */ +.gh-settings-notice { color: var(--text-warning); font-size: 13px; margin: 8px 0; } diff --git a/versions.json b/versions.json index f671e66..8e8255e 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,4 @@ { - "1.0.0": "0.15.0" + "1.0.0": "0.15.0", + "1.0.1": "0.15.0" } \ No newline at end of file