From 1045082df63e93c2c6dd7ed3a731ef3af6784fe3 Mon Sep 17 00:00:00 2001 From: meels Date: Tue, 28 Jul 2026 13:23:40 +0200 Subject: [PATCH] feat: render source pipeline in left pane --- .obsidian/plugins/webinar-dash/main.js | 109 ++++++++++++++++++- .obsidian/plugins/webinar-dash/styles.css | 122 ++++++++++++++++++++++ 2 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 .obsidian/plugins/webinar-dash/styles.css diff --git a/.obsidian/plugins/webinar-dash/main.js b/.obsidian/plugins/webinar-dash/main.js index 23e0c79..445961b 100644 --- a/.obsidian/plugins/webinar-dash/main.js +++ b/.obsidian/plugins/webinar-dash/main.js @@ -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; diff --git a/.obsidian/plugins/webinar-dash/styles.css b/.obsidian/plugins/webinar-dash/styles.css new file mode 100644 index 0000000..a029b92 --- /dev/null +++ b/.obsidian/plugins/webinar-dash/styles.css @@ -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; } +}