feat: render source pipeline in left pane
This commit is contained in:
109
.obsidian/plugins/webinar-dash/main.js
vendored
109
.obsidian/plugins/webinar-dash/main.js
vendored
@@ -76,15 +76,118 @@ function parseConfig(source) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const ICONS = {
|
||||
terminal: "M4 17l6-6-6-6M12 19h8",
|
||||
check: "M20 6 9 17l-5-5",
|
||||
minus: "M5 12h14",
|
||||
x: "M18 6 6 18M6 6l12 12",
|
||||
refresh: "M3 12a9 9 0 0 1 9-9 9 9 0 0 1 6.7 3L21 8M21 3v5h-5 M21 12a9 9 0 0 1-9 9 9 9 0 0 1-6.7-3L3 16M3 21v-5h5",
|
||||
};
|
||||
|
||||
// Built with createElementNS rather than Obsidian's createSvg helper, whose
|
||||
// availability varies by version. This works on any Obsidian build.
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
function addIcon(parent, name) {
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
const path = document.createElementNS(SVG_NS, "path");
|
||||
path.setAttribute("d", ICONS[name]);
|
||||
path.setAttribute("stroke", "currentColor");
|
||||
path.setAttribute("stroke-width", "1.75");
|
||||
path.setAttribute("stroke-linecap", "round");
|
||||
path.setAttribute("stroke-linejoin", "round");
|
||||
svg.appendChild(path);
|
||||
parent.appendChild(svg);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function formatBytes(n) {
|
||||
return `${n.toLocaleString("en-US")} B`;
|
||||
}
|
||||
|
||||
function renderLeftPane(container, pipeline, onIngest) {
|
||||
const pane = container.createDiv({ cls: "wd-pane" });
|
||||
|
||||
const queue = pane.createDiv({ cls: "wd-block" });
|
||||
queue.createDiv({
|
||||
cls: "wd-eyebrow",
|
||||
text: `Queue — ${pipeline.unprocessed.length} unprocessed`,
|
||||
});
|
||||
if (pipeline.unprocessed.length === 0) {
|
||||
queue.createDiv({ cls: "wd-note", text: "Every raw source has a summary page." });
|
||||
}
|
||||
for (const file of pipeline.unprocessed) {
|
||||
const row = queue.createDiv({ cls: "wd-row" });
|
||||
const main = row.createDiv({ cls: "wd-row-main" });
|
||||
main.createDiv({ cls: "wd-row-name", text: file.name });
|
||||
main.createDiv({ cls: "wd-mono", text: formatBytes(file.size) });
|
||||
const btn = row.createEl("button", { cls: "wd-btn" });
|
||||
addIcon(btn, "terminal");
|
||||
btn.createSpan({ text: "Ingest" });
|
||||
btn.addEventListener("click", () => onIngest(file, row));
|
||||
}
|
||||
|
||||
const done = pane.createDiv({ cls: "wd-block" });
|
||||
done.createDiv({ cls: "wd-eyebrow", text: `Ingested — ${pipeline.processed.length}` });
|
||||
const list = done.createDiv({ cls: "wd-done" });
|
||||
for (const file of pipeline.processed) {
|
||||
const row = list.createDiv({ cls: "wd-done-row" });
|
||||
const date = file.page.name.slice(0, 10);
|
||||
row.createSpan({ cls: "wd-mono", text: /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : "—" });
|
||||
row.createSpan({ cls: "wd-done-name", text: file.name.replace(/\.md$/, "") });
|
||||
}
|
||||
|
||||
if (pipeline.orphaned.length > 0) {
|
||||
const orphan = pane.createDiv({ cls: "wd-block" });
|
||||
orphan.createDiv({
|
||||
cls: "wd-eyebrow",
|
||||
text: `Orphaned — ${pipeline.orphaned.length}`,
|
||||
});
|
||||
for (const page of pipeline.orphaned) {
|
||||
const row = orphan.createDiv({ cls: "wd-done-row" });
|
||||
row.createSpan({ cls: "wd-done-name", text: page.name });
|
||||
row.createSpan({ cls: "wd-mono", text: page.rawPath || "no raw path" });
|
||||
}
|
||||
}
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
class WebinarDashPlugin extends PluginBase {
|
||||
async onload() {
|
||||
this.registerMarkdownCodeBlockProcessor("webinar-dash", (source, el, ctx) => {
|
||||
this.registerMarkdownCodeBlockProcessor("webinar-dash", async (source, el, ctx) => {
|
||||
const cfg = parseConfig(source);
|
||||
const root = el.createDiv({ cls: "webinar-dash" });
|
||||
root.createDiv({ cls: "wd-eyebrow", text: "Webinar dashboard" });
|
||||
root.createEl("p", { text: `Reading coverage from ${cfg.coverage}` });
|
||||
try {
|
||||
const pipeline = await this.readPipeline(cfg);
|
||||
renderLeftPane(root, pipeline, () => {});
|
||||
} catch (err) {
|
||||
root.createDiv({ cls: "wd-error", text: `Dashboard failed: ${err.message}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async readPipeline(cfg) {
|
||||
const all = this.app.vault.getFiles();
|
||||
const rawDir = cfg.rawDir.replace(/\/+$/, "") + "/";
|
||||
const wikiDir = cfg.wikiSourceDir.replace(/\/+$/, "") + "/";
|
||||
|
||||
const rawFiles = all
|
||||
.filter((f) => f.path.startsWith(rawDir) && f.extension === "md")
|
||||
.map((f) => ({ path: f.path, name: f.name, size: f.stat.size }));
|
||||
|
||||
const pageFiles = all.filter((f) => f.path.startsWith(wikiDir) && f.extension === "md");
|
||||
const sourcePages = [];
|
||||
for (const f of pageFiles) {
|
||||
const text = await this.app.vault.cachedRead(f);
|
||||
sourcePages.push({ path: f.path, name: f.name, rawPath: extractRawPath(text) });
|
||||
}
|
||||
|
||||
return derivePipeline({ rawFiles, sourcePages });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebinarDashPlugin;
|
||||
|
||||
122
.obsidian/plugins/webinar-dash/styles.css
vendored
Normal file
122
.obsidian/plugins/webinar-dash/styles.css
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
.webinar-dash {
|
||||
--wd-ink-000: #ffffff; --wd-ink-050: #f7f7f7; --wd-ink-100: #ececec;
|
||||
--wd-ink-200: #d9d9d9; --wd-ink-400: #8a8a8a; --wd-ink-500: #5e5e5e;
|
||||
--wd-ink-700: #262626; --wd-ink-900: #0a0a0a; --wd-ink-999: #000000;
|
||||
--wd-red-500: #e1261c; --wd-red-600: #c31c14;
|
||||
|
||||
--wd-bg: var(--wd-ink-000);
|
||||
--wd-bg-subtle: var(--wd-ink-050);
|
||||
--wd-fg: var(--wd-ink-999);
|
||||
--wd-fg-2: var(--wd-ink-700);
|
||||
--wd-fg-3: var(--wd-ink-500);
|
||||
--wd-fg-4: var(--wd-ink-400);
|
||||
--wd-border: var(--wd-ink-200);
|
||||
--wd-border-strong: var(--wd-ink-999);
|
||||
--wd-border-subtle: var(--wd-ink-100);
|
||||
--wd-accent: var(--wd-red-500);
|
||||
--wd-accent-press: var(--wd-red-600);
|
||||
--wd-accent-on: #ffffff;
|
||||
--wd-ok: #0a8a3f;
|
||||
--wd-warn: #c68a00;
|
||||
--wd-danger: var(--wd-red-500);
|
||||
|
||||
--wd-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
--wd-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
|
||||
|
||||
font-family: var(--wd-sans);
|
||||
color: var(--wd-fg);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 38fr) minmax(0, 62fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.theme-dark .webinar-dash {
|
||||
--wd-bg: var(--wd-ink-900);
|
||||
--wd-bg-subtle: #161616;
|
||||
--wd-fg: var(--wd-ink-000);
|
||||
--wd-fg-2: var(--wd-ink-200);
|
||||
--wd-fg-3: var(--wd-ink-400);
|
||||
--wd-fg-4: var(--wd-ink-500);
|
||||
--wd-border: var(--wd-ink-700);
|
||||
--wd-border-strong: #b8b8b8;
|
||||
--wd-border-subtle: #161616;
|
||||
--wd-accent: #ff4a3d;
|
||||
--wd-accent-press: #ff6d62;
|
||||
--wd-accent-on: #0a0a0a;
|
||||
--wd-ok: #2fbf6a;
|
||||
--wd-warn: #e0a516;
|
||||
--wd-danger: #ff4a3d;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.webinar-dash { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
|
||||
.webinar-dash > * { min-width: 0; }
|
||||
|
||||
.wd-pane { display: flex; flex-direction: column; gap: 20px; }
|
||||
|
||||
.wd-eyebrow {
|
||||
font-family: var(--wd-mono); font-size: 11px; line-height: 1;
|
||||
letter-spacing: 0.12em; text-transform: uppercase;
|
||||
color: var(--wd-fg-3); font-weight: 500;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.wd-eyebrow::before {
|
||||
content: ""; width: 5px; height: 5px; flex: none; background: var(--wd-accent);
|
||||
}
|
||||
|
||||
.wd-block { display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
.wd-row {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
border: 1px solid var(--wd-border); border-radius: 4px;
|
||||
padding: 12px 12px 12px 16px; background: var(--wd-bg);
|
||||
}
|
||||
.wd-row + .wd-row { margin-top: -1px; }
|
||||
.wd-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.wd-row-name {
|
||||
font-size: 14px; font-weight: 600;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.wd-mono {
|
||||
font-family: var(--wd-mono); font-size: 11px; color: var(--wd-fg-3);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.wd-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px; flex: none;
|
||||
font-family: var(--wd-sans); font-size: 13px; font-weight: 600;
|
||||
padding: 6px 12px; border-radius: 6px; cursor: pointer;
|
||||
border: 1px solid var(--wd-accent); background: var(--wd-accent);
|
||||
color: var(--wd-accent-on);
|
||||
transition: background 120ms cubic-bezier(0.2, 0, 0, 1);
|
||||
}
|
||||
.wd-btn:hover { background: var(--wd-accent-press); border-color: var(--wd-accent-press); }
|
||||
.wd-btn:focus-visible { outline: 2px solid var(--wd-accent); outline-offset: 2px; }
|
||||
.wd-btn[disabled] {
|
||||
opacity: 0.45; cursor: not-allowed;
|
||||
background: transparent; color: var(--wd-fg-3); border-color: var(--wd-border);
|
||||
}
|
||||
.wd-btn svg { width: 14px; height: 14px; flex: none; }
|
||||
|
||||
.wd-done { display: flex; flex-direction: column; }
|
||||
.wd-done-row {
|
||||
display: flex; align-items: baseline; gap: 12px; padding: 5px 0;
|
||||
border-bottom: 1px solid var(--wd-border-subtle); font-size: 13px;
|
||||
}
|
||||
.wd-done-row:last-child { border-bottom: 0; }
|
||||
.wd-done-name {
|
||||
color: var(--wd-fg-2);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
|
||||
.wd-note { font-size: 12px; color: var(--wd-fg-3); }
|
||||
.wd-error {
|
||||
font-size: 13px; color: var(--wd-danger);
|
||||
border: 1px solid var(--wd-danger); border-radius: 4px; padding: 12px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.webinar-dash * { transition-duration: 0.01ms !important; }
|
||||
}
|
||||
Reference in New Issue
Block a user