mirror of
https://github.com/kwhittle/obsidian-github-tools.git
synced 2026-07-22 07:44:31 +00:00
Initial scaffold: Obsidian GitHub Wiki Tools plugin
This commit is contained in:
commit
537eab4783
7 changed files with 698 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
main.js
|
||||
*.js.map
|
||||
39
esbuild.config.mjs
Normal file
39
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
const context = await esbuild.context({
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
481
main.ts
Normal file
481
main.ts
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
import {
|
||||
App,
|
||||
ItemView,
|
||||
Notice,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
WorkspaceLeaf,
|
||||
} from "obsidian";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const VIEW_TYPE = "github-wiki-sidebar";
|
||||
|
||||
// ── Settings ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Settings {
|
||||
repoPath: string;
|
||||
githubToken: string;
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
githubUsername: string;
|
||||
pollIntervalMinutes: number;
|
||||
// PR number → last-seen updatedAt timestamp (ISO string)
|
||||
seenPRActivity: Record<number, string>;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
repoPath: "",
|
||||
githubToken: "",
|
||||
repoOwner: "",
|
||||
repoName: "",
|
||||
githubUsername: "",
|
||||
pollIntervalMinutes: 15,
|
||||
seenPRActivity: {},
|
||||
};
|
||||
|
||||
// ── Git service ──────────────────────────────────────────────────────────────
|
||||
|
||||
interface GitStatus {
|
||||
branch: string;
|
||||
hasUncommittedChanges: boolean;
|
||||
commitsBehind: number;
|
||||
}
|
||||
|
||||
async function gitExec(repoPath: string, args: string): Promise<string> {
|
||||
const { stdout } = await execAsync(`git -C "${repoPath}" ${args}`);
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
async function fetchGitStatus(repoPath: string): Promise<GitStatus> {
|
||||
await gitExec(repoPath, "fetch origin");
|
||||
|
||||
const branch = await gitExec(repoPath, "branch --show-current");
|
||||
const porcelain = await gitExec(repoPath, "status --porcelain");
|
||||
const hasUncommittedChanges = porcelain.length > 0;
|
||||
|
||||
const behindStr = await gitExec(
|
||||
repoPath,
|
||||
`rev-list --count HEAD..origin/${branch}`
|
||||
);
|
||||
const commitsBehind = parseInt(behindStr, 10) || 0;
|
||||
|
||||
return { branch, hasUncommittedChanges, commitsBehind };
|
||||
}
|
||||
|
||||
async function runGitPull(repoPath: string): Promise<void> {
|
||||
await gitExec(repoPath, "pull");
|
||||
}
|
||||
|
||||
// ── GitHub service ───────────────────────────────────────────────────────────
|
||||
|
||||
interface PullRequest {
|
||||
number: number;
|
||||
title: string;
|
||||
url: string;
|
||||
author: string;
|
||||
updatedAt: string;
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
async function fetchOpenPRs(
|
||||
owner: string,
|
||||
repo: string,
|
||||
token: string
|
||||
): Promise<PullRequest[]> {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls?state=open&per_page=50`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: any[] = await response.json();
|
||||
|
||||
return data.map((pr) => ({
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
url: pr.html_url,
|
||||
author: pr.user.login,
|
||||
updatedAt: pr.updated_at,
|
||||
draft: pr.draft ?? false,
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Sidebar view ─────────────────────────────────────────────────────────────
|
||||
|
||||
class WikiSidebarView extends ItemView {
|
||||
private plugin: GitHubWikiPlugin;
|
||||
private gitStatus: GitStatus | null = null;
|
||||
private prs: PullRequest[] = [];
|
||||
private loading = false;
|
||||
private errorMsg: string | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: GitHubWikiPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string { return VIEW_TYPE; }
|
||||
getDisplayText(): string { return "GitHub Wiki"; }
|
||||
getIcon(): string { return "github"; }
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
this.loading = true;
|
||||
this.errorMsg = null;
|
||||
this.render();
|
||||
|
||||
try {
|
||||
const s = this.plugin.settings;
|
||||
|
||||
if (s.repoPath) {
|
||||
this.gitStatus = await fetchGitStatus(s.repoPath);
|
||||
}
|
||||
|
||||
if (s.githubToken && s.repoOwner && s.repoName) {
|
||||
this.prs = await fetchOpenPRs(s.repoOwner, s.repoName, s.githubToken);
|
||||
this.plugin.initSeenActivity(this.prs);
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorMsg = e.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
render(): void {
|
||||
const root = this.containerEl.children[1] as HTMLElement;
|
||||
root.empty();
|
||||
root.addClass("gwt-sidebar");
|
||||
|
||||
const header = root.createDiv("gwt-header");
|
||||
header.createEl("h4", { text: "GitHub Wiki" });
|
||||
const refreshBtn = header.createEl("button", { cls: "gwt-btn-icon", text: "↻" });
|
||||
refreshBtn.setAttribute("aria-label", "Refresh");
|
||||
refreshBtn.addEventListener("click", () => this.refresh());
|
||||
|
||||
if (this.loading) {
|
||||
root.createDiv("gwt-muted").setText("Loading…");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.errorMsg) {
|
||||
root.createDiv("gwt-error").setText(this.errorMsg);
|
||||
}
|
||||
|
||||
this.renderGitStatus(root);
|
||||
this.renderPRs(root);
|
||||
}
|
||||
|
||||
private renderGitStatus(root: HTMLElement): void {
|
||||
const s = this.gitStatus;
|
||||
if (!s) {
|
||||
if (!this.plugin.settings.repoPath) {
|
||||
root.createDiv("gwt-muted").setText("Set repo path in settings to see git status.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const section = root.createDiv("gwt-section");
|
||||
section.createEl("h5", { text: "Local Repo" });
|
||||
|
||||
const branchRow = section.createDiv("gwt-row");
|
||||
branchRow.createSpan({ cls: "gwt-label", text: "Branch: " });
|
||||
branchRow.createSpan({ text: s.branch });
|
||||
|
||||
if (s.hasUncommittedChanges) {
|
||||
section.createDiv("gwt-row gwt-warning").setText("⚠ Uncommitted changes");
|
||||
}
|
||||
|
||||
if (s.commitsBehind > 0) {
|
||||
const behindRow = section.createDiv("gwt-row gwt-behind");
|
||||
behindRow.createSpan({
|
||||
text: `⬇ ${s.commitsBehind} commit${s.commitsBehind !== 1 ? "s" : ""} behind origin/${s.branch}`,
|
||||
});
|
||||
const pullBtn = behindRow.createEl("button", { cls: "gwt-btn", text: "Pull" });
|
||||
pullBtn.addEventListener("click", () => this.handlePull());
|
||||
} else {
|
||||
section.createDiv("gwt-row gwt-ok").setText("✓ Up to date");
|
||||
}
|
||||
}
|
||||
|
||||
private renderPRs(root: HTMLElement): void {
|
||||
const s = this.plugin.settings;
|
||||
if (!s.githubToken || !s.repoOwner || !s.repoName) {
|
||||
root.createDiv("gwt-muted").setText("Configure GitHub settings to see PRs.");
|
||||
return;
|
||||
}
|
||||
|
||||
const myPRs = this.prs.filter((pr) => pr.author === s.githubUsername);
|
||||
const otherPRs = this.prs.filter((pr) => pr.author !== s.githubUsername);
|
||||
|
||||
const mySection = root.createDiv("gwt-section");
|
||||
mySection.createEl("h5", { text: `My PRs (${myPRs.length})` });
|
||||
if (myPRs.length === 0) {
|
||||
mySection.createDiv("gwt-empty").setText("No open PRs");
|
||||
} else {
|
||||
myPRs.forEach((pr) => this.renderPR(mySection, pr));
|
||||
}
|
||||
|
||||
const allSection = root.createDiv("gwt-section");
|
||||
allSection.createEl("h5", { text: `All Open PRs (${otherPRs.length})` });
|
||||
if (otherPRs.length === 0) {
|
||||
allSection.createDiv("gwt-empty").setText("No other open PRs");
|
||||
} else {
|
||||
otherPRs.forEach((pr) => this.renderPR(allSection, pr));
|
||||
}
|
||||
}
|
||||
|
||||
private renderPR(container: HTMLElement, pr: PullRequest): void {
|
||||
const hasNew = this.plugin.hasNewActivity(pr);
|
||||
const row = container.createDiv(hasNew ? "gwt-pr gwt-pr-new" : "gwt-pr");
|
||||
|
||||
const link = row.createEl("a", {
|
||||
cls: "gwt-pr-link",
|
||||
href: pr.url,
|
||||
text: `#${pr.number} ${pr.title}`,
|
||||
});
|
||||
link.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
window.open(pr.url, "_blank");
|
||||
this.plugin.markSeen(pr);
|
||||
row.removeClass("gwt-pr-new");
|
||||
row.querySelector(".gwt-new-badge")?.remove();
|
||||
});
|
||||
|
||||
const meta = row.createDiv("gwt-pr-meta");
|
||||
meta.createSpan({ text: `@${pr.author}` });
|
||||
if (pr.draft) meta.createSpan({ cls: "gwt-draft", text: " · Draft" });
|
||||
if (hasNew) meta.createSpan({ cls: "gwt-new-badge", text: " · New activity" });
|
||||
}
|
||||
|
||||
private async handlePull(): Promise<void> {
|
||||
const { gitStatus } = this;
|
||||
if (!gitStatus) return;
|
||||
|
||||
if (gitStatus.hasUncommittedChanges) {
|
||||
new Notice(
|
||||
"Cannot pull: you have uncommitted changes. Commit or stash them first.",
|
||||
8000
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await runGitPull(this.plugin.settings.repoPath);
|
||||
new Notice("Pull successful.");
|
||||
await this.refresh();
|
||||
} catch (e: any) {
|
||||
new Notice(`Pull failed: ${e.message}`, 8000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Plugin ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export default class GitHubWikiPlugin extends Plugin {
|
||||
settings: Settings;
|
||||
private pollTimer: number | null = null;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
|
||||
this.registerView(VIEW_TYPE, (leaf) => new WikiSidebarView(leaf, this));
|
||||
|
||||
this.addRibbonIcon("github", "GitHub Wiki", () => this.activateView());
|
||||
|
||||
this.addSettingTab(new GitHubWikiSettingTab(this.app, this));
|
||||
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.activateView();
|
||||
this.startPolling();
|
||||
});
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
this.stopPolling();
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
if (!this.settings.seenPRActivity) this.settings.seenPRActivity = {};
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
async activateView(): Promise<void> {
|
||||
const { workspace } = this.app;
|
||||
let leaf = workspace.getLeavesOfType(VIEW_TYPE)[0];
|
||||
if (!leaf) {
|
||||
leaf = workspace.getRightLeaf(false)!;
|
||||
await leaf.setViewState({ type: VIEW_TYPE, active: true });
|
||||
}
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
restartPolling(): void {
|
||||
this.stopPolling();
|
||||
this.startPolling();
|
||||
}
|
||||
|
||||
private startPolling(): void {
|
||||
const intervalMs = (this.settings.pollIntervalMinutes || 15) * 60 * 1000;
|
||||
this.pollTimer = window.setInterval(() => this.refreshView(), intervalMs);
|
||||
}
|
||||
|
||||
private stopPolling(): void {
|
||||
if (this.pollTimer !== null) {
|
||||
window.clearInterval(this.pollTimer);
|
||||
this.pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshView(): Promise<void> {
|
||||
for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE)) {
|
||||
await (leaf.view as WikiSidebarView).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Record updatedAt for PRs we haven't seen before so first load doesn't
|
||||
// flag everything as new.
|
||||
initSeenActivity(prs: PullRequest[]): void {
|
||||
let changed = false;
|
||||
for (const pr of prs) {
|
||||
if (pr.author !== this.settings.githubUsername) continue;
|
||||
if (!(pr.number in this.settings.seenPRActivity)) {
|
||||
this.settings.seenPRActivity[pr.number] = pr.updatedAt;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) this.saveSettings();
|
||||
}
|
||||
|
||||
hasNewActivity(pr: PullRequest): boolean {
|
||||
if (pr.author !== this.settings.githubUsername) return false;
|
||||
const lastSeen = this.settings.seenPRActivity[pr.number];
|
||||
if (!lastSeen) return false;
|
||||
return pr.updatedAt > lastSeen;
|
||||
}
|
||||
|
||||
markSeen(pr: PullRequest): void {
|
||||
this.settings.seenPRActivity[pr.number] = pr.updatedAt;
|
||||
this.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Settings tab ─────────────────────────────────────────────────────────────
|
||||
|
||||
class GitHubWikiSettingTab extends PluginSettingTab {
|
||||
plugin: GitHubWikiPlugin;
|
||||
|
||||
constructor(app: App, plugin: GitHubWikiPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "GitHub Wiki Tools" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Local repo path")
|
||||
.setDesc("Absolute path to your local clone of the wiki repo")
|
||||
.addText((t) =>
|
||||
t
|
||||
.setPlaceholder("/Users/you/wiki-repo")
|
||||
.setValue(this.plugin.settings.repoPath)
|
||||
.onChange(async (v) => {
|
||||
this.plugin.settings.repoPath = v;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("GitHub personal access token")
|
||||
.setDesc("Classic PAT with repo scope")
|
||||
.addText((t) => {
|
||||
t
|
||||
.setPlaceholder("ghp_…")
|
||||
.setValue(this.plugin.settings.githubToken)
|
||||
.onChange(async (v) => {
|
||||
this.plugin.settings.githubToken = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
t.inputEl.type = "password";
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("GitHub username")
|
||||
.setDesc("Your GitHub username — used to identify your PRs")
|
||||
.addText((t) =>
|
||||
t
|
||||
.setPlaceholder("your-username")
|
||||
.setValue(this.plugin.settings.githubUsername)
|
||||
.onChange(async (v) => {
|
||||
this.plugin.settings.githubUsername = v;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Repo owner")
|
||||
.setDesc("GitHub org or user that owns the wiki repo")
|
||||
.addText((t) =>
|
||||
t
|
||||
.setPlaceholder("org-name")
|
||||
.setValue(this.plugin.settings.repoOwner)
|
||||
.onChange(async (v) => {
|
||||
this.plugin.settings.repoOwner = v;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Repo name")
|
||||
.setDesc("Name of the wiki repo on GitHub")
|
||||
.addText((t) =>
|
||||
t
|
||||
.setPlaceholder("wiki-repo")
|
||||
.setValue(this.plugin.settings.repoName)
|
||||
.onChange(async (v) => {
|
||||
this.plugin.settings.repoName = v;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Poll interval (minutes)")
|
||||
.setDesc("How often to check for updates (default: 15)")
|
||||
.addText((t) =>
|
||||
t
|
||||
.setPlaceholder("15")
|
||||
.setValue(String(this.plugin.settings.pollIntervalMinutes))
|
||||
.onChange(async (v) => {
|
||||
const n = parseInt(v, 10);
|
||||
if (!isNaN(n) && n > 0) {
|
||||
this.plugin.settings.pollIntervalMinutes = n;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.restartPolling();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "obsidian-github-tools",
|
||||
"name": "GitHub Wiki Tools",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Monitor your GitHub wiki repo — staleness alerts, PR dashboard, and activity notifications.",
|
||||
"author": "Kyle Whittle",
|
||||
"authorUrl": "",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
21
package.json
Normal file
21
package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "obsidian-github-tools",
|
||||
"version": "1.0.0",
|
||||
"description": "GitHub wiki repo monitor for Obsidian",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Kyle Whittle",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
128
styles.css
Normal file
128
styles.css
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
.gwt-sidebar {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gwt-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.gwt-header h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gwt-btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: var(--text-muted);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.gwt-btn-icon:hover {
|
||||
color: var(--text-normal);
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.gwt-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.gwt-section h5 {
|
||||
margin: 0 0 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gwt-row {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.gwt-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.gwt-warning {
|
||||
color: var(--color-orange);
|
||||
}
|
||||
|
||||
.gwt-behind {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--color-yellow);
|
||||
}
|
||||
|
||||
.gwt-ok {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.gwt-btn {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gwt-pr {
|
||||
margin-bottom: 6px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--background-secondary);
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.gwt-pr-new {
|
||||
border-left-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.gwt-pr-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: var(--text-normal);
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.gwt-pr-link:hover {
|
||||
color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.gwt-pr-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.gwt-new-badge {
|
||||
color: var(--interactive-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gwt-draft {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.gwt-empty {
|
||||
font-size: 12px;
|
||||
color: var(--text-faint);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.gwt-muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.gwt-error {
|
||||
color: var(--color-red);
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-word;
|
||||
}
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in a new issue