From 460caf676428b825148dd9800dfd9c3df006bcff Mon Sep 17 00:00:00 2001 From: Jaycelu Date: Tue, 23 Jun 2026 20:19:42 +0800 Subject: [PATCH] Release Smart Review 0.2.1 --- apps/smart-review-plugin/analytics-service.ts | 18 ++- apps/smart-review-plugin/analytics-types.ts | 7 + apps/smart-review-plugin/i18n.ts | 8 ++ apps/smart-review-plugin/manifest.json | 2 +- apps/smart-review-plugin/package.json | 2 +- .../smart-review-plugin/review-center-view.ts | 122 ++++++++++++++++-- apps/smart-review-plugin/styles.css | 105 +++++++++++++-- apps/smart-review-plugin/versions.json | 3 +- manifest.json | 2 +- package.json | 2 +- packages/shared/package.json | 2 +- versions.json | 3 +- 12 files changed, 244 insertions(+), 32 deletions(-) diff --git a/apps/smart-review-plugin/analytics-service.ts b/apps/smart-review-plugin/analytics-service.ts index 8007bfe..de34cb7 100644 --- a/apps/smart-review-plugin/analytics-service.ts +++ b/apps/smart-review-plugin/analytics-service.ts @@ -3,7 +3,7 @@ import { getLocalDateString, normalizeDate, type ReviewIndex, type ReviewItem, t import type { SmartReviewSettings } from "./settings"; import { DEFAULT_SETTINGS } from "./settings"; import { buildHeatmapDays, getDateDaysAgo, incrementDateCount } from "./heatmap"; -import { UNCATEGORIZED_DOMAIN, type DistributionItem, type ReviewHistoryDetail, type SmartReviewAnalytics } from "./analytics-types"; +import { UNCATEGORIZED_DOMAIN, type DistributionItem, type NoteCreationDetail, type ReviewHistoryDetail, type SmartReviewAnalytics } from "./analytics-types"; import { collectTags, getOptionalString, @@ -28,6 +28,7 @@ interface VaultMetadataStats { notesWithNextReview: number; metadataCompleteness: number; creationCountsByDate: Map; + creationDetails: NoteCreationDetail[]; } const RATING_NAMES: ReviewRating[] = ["again", "hard", "good", "easy"]; @@ -95,7 +96,8 @@ export async function buildSmartReviewAnalytics( byState: buildDistribution(index.items.map((item) => item.review_state)) }, details: { - reviewHistory: history.map(toReviewHistoryDetail) + reviewHistory: history.map(toReviewHistoryDetail), + noteCreation: vaultStats.creationDetails } }; } @@ -148,6 +150,7 @@ function parseReviewHistoryLine(line: string): ReviewHistoryRecord[] { function collectVaultMetadataStats(app: App, settings: SmartReviewSettings): VaultMetadataStats { const creationCountsByDate = new Map(); + const creationDetails: NoteCreationDetail[] = []; let activeNotes = 0; let notesWithNextReview = 0; let completenessTotal = 0; @@ -165,14 +168,21 @@ function collectVaultMetadataStats(app: App, settings: SmartReviewSettings): Vau } completenessTotal += calculateMetadataCompleteness(frontmatter, file, cache); - incrementDateCount(creationCountsByDate, getCreationDate(frontmatter, file)); + const creationDate = getCreationDate(frontmatter, file); + incrementDateCount(creationCountsByDate, creationDate); + creationDetails.push({ + date: creationDate, + file: file.path, + title: getOptionalString(frontmatter.title) ?? file.basename + }); } return { activeNotes, notesWithNextReview, metadataCompleteness: activeNotes > 0 ? completenessTotal / activeNotes : 1, - creationCountsByDate + creationCountsByDate, + creationDetails }; } diff --git a/apps/smart-review-plugin/analytics-types.ts b/apps/smart-review-plugin/analytics-types.ts index e3391fe..5b092e3 100644 --- a/apps/smart-review-plugin/analytics-types.ts +++ b/apps/smart-review-plugin/analytics-types.ts @@ -34,6 +34,7 @@ export interface SmartReviewAnalytics { }; details: { reviewHistory: ReviewHistoryDetail[]; + noteCreation: NoteCreationDetail[]; }; } @@ -57,4 +58,10 @@ export interface ReviewHistoryDetail { nextReview: string | null; } +export interface NoteCreationDetail { + date: string; + file: string; + title: string; +} + export type ReviewStateDistributionName = ReviewState | "unknown"; diff --git a/apps/smart-review-plugin/i18n.ts b/apps/smart-review-plugin/i18n.ts index ba2f2c7..c8600d4 100644 --- a/apps/smart-review-plugin/i18n.ts +++ b/apps/smart-review-plugin/i18n.ts @@ -63,6 +63,10 @@ const TRANSLATIONS = { creationHeatmapTitle: "Vault Creation Heatmap", creationHeatmapSubtitle: "Based on frontmatter.created or file timestamps from the past 365 days", creationUnit: "notes", + heatmapClickHint: "Click to view details", + reviewDayDetail: "Reviews on {date}", + creationDayDetail: "Notes created on {date}", + createdAt: "created", noChartData: "No data to show.", domainDistribution: "Domain Distribution", ratingDistribution: "Rating Distribution", @@ -204,6 +208,10 @@ const TRANSLATIONS = { creationHeatmapTitle: "笔记创建热力图", creationHeatmapSubtitle: "基于 frontmatter.created 或文件时间统计过去 365 天的笔记创建情况", creationUnit: "篇笔记", + heatmapClickHint: "点击查看详情", + reviewDayDetail: "{date} 的复习记录", + creationDayDetail: "{date} 创建的笔记", + createdAt: "创建日期", noChartData: "暂无可展示的数据。", domainDistribution: "领域分布", ratingDistribution: "评分分布", diff --git a/apps/smart-review-plugin/manifest.json b/apps/smart-review-plugin/manifest.json index e335773..68ece30 100644 --- a/apps/smart-review-plugin/manifest.json +++ b/apps/smart-review-plugin/manifest.json @@ -1,7 +1,7 @@ { "id": "smart-review", "name": "Smart Review", - "version": "0.2.0", + "version": "0.2.1", "minAppVersion": "1.5.0", "description": "Review center for Properties and YAML frontmatter with spaced feedback, history, and AI payload export.", "author": "Jayce", diff --git a/apps/smart-review-plugin/package.json b/apps/smart-review-plugin/package.json index 63bd4e6..de51470 100644 --- a/apps/smart-review-plugin/package.json +++ b/apps/smart-review-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@smart-review/obsidian-plugin", - "version": "0.2.0", + "version": "0.2.1", "private": true, "type": "module", "scripts": { diff --git a/apps/smart-review-plugin/review-center-view.ts b/apps/smart-review-plugin/review-center-view.ts index 363d7f4..4f97bd9 100644 --- a/apps/smart-review-plugin/review-center-view.ts +++ b/apps/smart-review-plugin/review-center-view.ts @@ -1,6 +1,6 @@ import { ItemView, Notice, type WorkspaceLeaf } from "obsidian"; import type { ReviewItem, ReviewRating, ReviewState } from "@smart-review/shared"; -import { UNCATEGORIZED_DOMAIN, type DistributionItem, type HeatmapDay, type ReviewHistoryDetail, type SmartReviewAnalytics } from "./analytics-types"; +import { UNCATEGORIZED_DOMAIN, type DistributionItem, type HeatmapDay, type NoteCreationDetail, type ReviewHistoryDetail, type SmartReviewAnalytics } from "./analytics-types"; import { t, type SmartReviewLocale, type SmartReviewTranslationKey } from "./i18n"; import type SmartReviewPlugin from "./main"; import { REVIEW_RATINGS } from "./settings"; @@ -23,16 +23,24 @@ const PLAN_GROUPS: PlanGroup[] = [ ]; type DrilldownKind = "domain" | "tag" | "rating"; +type HeatmapKind = "review" | "creation"; interface DrilldownSelection { kind: DrilldownKind; name: string; } +interface HeatmapSelection { + kind: HeatmapKind; + date: string; +} + export class ReviewCenterView extends ItemView { private readonly expandedGroups = new Set(); private selectedDrilldown: DrilldownSelection | null = null; private drilldownExpanded = false; + private selectedHeatmap: HeatmapSelection | null = null; + private heatmapExpanded = false; constructor(leaf: WorkspaceLeaf, private readonly plugin: SmartReviewPlugin) { super(leaf); @@ -90,8 +98,8 @@ export class ReviewCenterView extends ItemView { this.renderMetrics(container, analytics); this.renderTaskFlow(container, analytics); - this.renderHeatmapSection(container, t(this.plugin.locale, "reviewHeatmapTitle"), t(this.plugin.locale, "reviewHeatmapSubtitle"), analytics.heatmaps.reviewActivity, t(this.plugin.locale, "reviewUnit")); - this.renderHeatmapSection(container, t(this.plugin.locale, "creationHeatmapTitle"), t(this.plugin.locale, "creationHeatmapSubtitle"), analytics.heatmaps.noteCreation, t(this.plugin.locale, "creationUnit")); + this.renderHeatmapSection(container, t(this.plugin.locale, "reviewHeatmapTitle"), t(this.plugin.locale, "reviewHeatmapSubtitle"), analytics.heatmaps.reviewActivity, t(this.plugin.locale, "reviewUnit"), "review", analytics); + this.renderHeatmapSection(container, t(this.plugin.locale, "creationHeatmapTitle"), t(this.plugin.locale, "creationHeatmapSubtitle"), analytics.heatmaps.noteCreation, t(this.plugin.locale, "creationUnit"), "creation", analytics); this.renderDistributions(container, analytics); } @@ -263,7 +271,7 @@ export class ReviewCenterView extends ItemView { this.renderProgress(row, analytics.taskFlow.completionRate); } - private renderHeatmapSection(container: HTMLElement, title: string, subtitle: string, days: HeatmapDay[], unit: string): void { + private renderHeatmapSection(container: HTMLElement, title: string, subtitle: string, days: HeatmapDay[], unit: string, kind: HeatmapKind, analytics: SmartReviewAnalytics): void { const section = container.createDiv({ cls: "smart-review-heatmap-section" }); this.renderSectionHeading(section, title, subtitle); @@ -280,10 +288,69 @@ export class ReviewCenterView extends ItemView { const heatmap = viewport.createDiv({ cls: "smart-review-heatmap" }); for (const [index, day] of days.entries()) { - const cell = heatmap.createDiv({ cls: `smart-review-heatmap-cell smart-review-heatmap-level-${day.level}` }); + const selected = this.selectedHeatmap?.kind === kind && this.selectedHeatmap.date === day.date; + const cell = heatmap.createEl("button", { + cls: `smart-review-heatmap-cell smart-review-heatmap-level-${day.level}${selected ? " smart-review-heatmap-cell-selected" : ""}` + }); cell.setAttr("style", `grid-column: ${Math.floor(index / 7) + 1}; grid-row: ${(index % 7) + 1};`); + cell.setAttr("type", "button"); cell.setAttr("title", `${day.date}: ${day.count} ${unit}`); - cell.setAttr("aria-label", `${day.date}: ${day.count} ${unit}`); + cell.setAttr("aria-label", `${day.date}: ${day.count} ${unit}. ${t(this.plugin.locale, "heatmapClickHint")}`); + cell.setAttr("data-tooltip", `${day.date}\A${day.count} ${unit}\A${t(this.plugin.locale, "heatmapClickHint")}`); + cell.onclick = () => { + this.selectedHeatmap = selected ? null : { kind, date: day.date }; + this.heatmapExpanded = false; + this.render(); + if (!selected) { + this.focusHeatmapDetailAfterRender(); + } + }; + } + + this.renderHeatmapDrilldown(section, kind, analytics); + } + + private renderHeatmapDrilldown(container: HTMLElement, kind: HeatmapKind, analytics: SmartReviewAnalytics): void { + if (this.selectedHeatmap === null || this.selectedHeatmap.kind !== kind) { + return; + } + + const rows = getHeatmapRows(this.selectedHeatmap, analytics); + const panel = container.createDiv({ cls: "smart-review-drilldown-panel smart-review-heatmap-detail-panel" }); + panel.setAttr("tabindex", "-1"); + const header = panel.createDiv({ cls: "smart-review-drilldown-header" }); + const title = header.createDiv({ cls: "smart-review-drilldown-title" }); + title.createEl("h3", { text: getHeatmapDrilldownTitle(this.plugin.locale, this.selectedHeatmap) }); + title.createSpan({ cls: "smart-review-subtle-badge", text: t(this.plugin.locale, "detailCount", { count: rows.length }) }); + + const actions = header.createDiv({ cls: "smart-review-drilldown-actions" }); + if (rows.length > 10) { + const toggle = actions.createEl("button", { + cls: "smart-review-link-button", + text: this.heatmapExpanded ? t(this.plugin.locale, "collapse") : t(this.plugin.locale, "expandAll") + }); + toggle.onclick = () => { + this.heatmapExpanded = !this.heatmapExpanded; + this.render(); + this.focusHeatmapDetailAfterRender(); + }; + } + + const close = actions.createEl("button", { cls: "smart-review-link-button", text: t(this.plugin.locale, "close") }); + close.onclick = () => { + this.selectedHeatmap = null; + this.heatmapExpanded = false; + this.render(); + }; + + if (rows.length === 0) { + panel.createDiv({ cls: "smart-review-empty-state smart-review-empty-inline", text: t(this.plugin.locale, "noDetailRecords") }); + return; + } + + const list = panel.createDiv({ cls: "smart-review-drilldown-list" }); + for (const row of this.heatmapExpanded ? rows : rows.slice(0, 10)) { + this.renderDrilldownRow(list, row); } } @@ -371,7 +438,7 @@ export class ReviewCenterView extends ItemView { } } - private renderDrilldownRow(container: HTMLElement, row: ReviewItem | ReviewHistoryDetail): void { + private renderDrilldownRow(container: HTMLElement, row: ReviewItem | ReviewHistoryDetail | NoteCreationDetail): void { const item = container.createDiv({ cls: "smart-review-drilldown-row" }); const main = item.createDiv({ cls: "smart-review-drilldown-main" }); const title = main.createEl("button", { text: row.title, cls: "smart-review-task-title" }); @@ -395,10 +462,12 @@ export class ReviewCenterView extends ItemView { renderMeta(meta, t(this.plugin.locale, "statusLabel"), stateLabel(this.plugin.locale, row)); renderMeta(meta, "next_review", row.next_review ?? t(this.plugin.locale, "invalid")); renderMeta(meta, "domain", row.domain); - } else { + } else if ("reviewedAt" in row) { renderMeta(meta, t(this.plugin.locale, "reviewedAt"), row.reviewedAt); renderMeta(meta, t(this.plugin.locale, "rating"), row.rating); renderMeta(meta, "next_review", row.nextReview ?? t(this.plugin.locale, "noRecord")); + } else { + renderMeta(meta, t(this.plugin.locale, "createdAt"), row.date); } } @@ -419,6 +488,23 @@ export class ReviewCenterView extends ItemView { }, 0); } + private focusHeatmapDetailAfterRender(): void { + window.setTimeout(() => { + const container = this.containerEl.children[1] as HTMLElement; + const panel = container.querySelector(".smart-review-heatmap-detail-panel"); + if (panel === null) { + return; + } + + panel.addClass("smart-review-drilldown-panel-focus"); + panel.scrollIntoView({ behavior: "smooth", block: "start" }); + panel.focus({ preventScroll: true }); + window.setTimeout(() => { + panel.removeClass("smart-review-drilldown-panel-focus"); + }, 1_200); + }, 0); + } + private renderSectionHeading(container: HTMLElement, title: string, subtitle: string): void { const heading = container.createDiv({ cls: "smart-review-section-heading" }); heading.createEl("h2", { text: title }); @@ -543,6 +629,26 @@ function getDrilldownRows( .sort((left, right) => right.reviewedAt.localeCompare(left.reviewedAt)); } +function getHeatmapRows(selection: HeatmapSelection, analytics: SmartReviewAnalytics): Array { + if (selection.kind === "review") { + return analytics.details.reviewHistory + .filter((entry) => entry.reviewedAt === selection.date) + .sort((left, right) => left.title.localeCompare(right.title)); + } + + return analytics.details.noteCreation + .filter((entry) => entry.date === selection.date) + .sort((left, right) => left.title.localeCompare(right.title)); +} + +function getHeatmapDrilldownTitle(locale: SmartReviewLocale, selection: HeatmapSelection): string { + if (selection.kind === "review") { + return t(locale, "reviewDayDetail", { date: selection.date }); + } + + return t(locale, "creationDayDetail", { date: selection.date }); +} + function getDistributionDisplayName(locale: SmartReviewLocale, name: string): string { return name === UNCATEGORIZED_DOMAIN ? t(locale, "uncategorized") : name; } diff --git a/apps/smart-review-plugin/styles.css b/apps/smart-review-plugin/styles.css index 184f310..2ddca61 100644 --- a/apps/smart-review-plugin/styles.css +++ b/apps/smart-review-plugin/styles.css @@ -409,20 +409,21 @@ } .smart-review-heatmap-viewport { - --smart-review-heatmap-cell: clamp(10px, calc((100vw - 520px) / 68), 18px); - --smart-review-heatmap-gap: 4px; + --smart-review-heatmap-gap: clamp(2px, 0.22vw, 4px); + --smart-review-heatmap-row: clamp(8px, 0.72vw, 14px); width: 100%; max-width: 100%; - overflow-x: auto; + overflow: visible; padding-bottom: 2px; } .smart-review-heatmap-months, .smart-review-heatmap { display: grid; - grid-template-columns: repeat(53, minmax(var(--smart-review-heatmap-cell), 1fr)); + grid-template-columns: repeat(53, minmax(0, 1fr)); gap: var(--smart-review-heatmap-gap); - min-width: calc(53 * 10px + 52 * var(--smart-review-heatmap-gap)); + width: 100%; + min-width: 0; } .smart-review-heatmap-months { @@ -437,33 +438,111 @@ } .smart-review-heatmap { - grid-template-rows: repeat(7, var(--smart-review-heatmap-cell)); + grid-template-rows: repeat(7, var(--smart-review-heatmap-row)); } .smart-review-heatmap-cell { + position: relative; + display: block; width: 100%; height: 100%; - min-width: 10px; - min-height: 10px; + min-width: 0 !important; + min-height: 0 !important; + margin: 0; + padding: 0 !important; border: 1px solid var(--background-modifier-border); border-radius: 3px; - background: var(--background-secondary); + background-color: var(--background-secondary) !important; + background-image: none !important; + box-shadow: none !important; + color: transparent; + cursor: pointer; + appearance: none; + line-height: 0; + font-size: 0; + overflow: visible; +} + +.smart-review-heatmap-cell:hover, +.smart-review-heatmap-cell:focus-visible, +.smart-review-heatmap-cell-selected { + border-color: var(--interactive-accent); + outline: none; +} + +.smart-review-heatmap-cell:focus-visible, +.smart-review-heatmap-cell-selected { + box-shadow: 0 0 0 2px color-mix(in srgb, var(--interactive-accent) 24%, transparent); +} + +.smart-review-heatmap-cell::after { + content: attr(data-tooltip); + position: absolute; + top: calc(100% + 8px); + left: 50%; + z-index: 20; + display: block; + min-width: 118px; + padding: 6px 8px; + border: 1px solid var(--smart-review-border); + border-radius: var(--radius-s); + background: var(--background-primary); + color: var(--text-normal); + box-shadow: var(--shadow-s); + font-size: var(--font-ui-smaller); + line-height: 1.35; + text-align: left; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transform: translate(-50%, -2px); + transition: opacity 120ms ease, transform 120ms ease; + white-space: pre-line; +} + +.smart-review-heatmap-cell::before { + content: ""; + position: absolute; + top: calc(100% + 4px); + left: 50%; + z-index: 21; + width: 8px; + height: 8px; + border-left: 1px solid var(--smart-review-border); + border-top: 1px solid var(--smart-review-border); + background: var(--background-primary); + pointer-events: none; + opacity: 0; + transform: translate(-50%, 0) rotate(45deg); + transition: opacity 120ms ease; +} + +.smart-review-heatmap-cell:hover::after, +.smart-review-heatmap-cell:focus-visible::after, +.smart-review-heatmap-cell:hover::before, +.smart-review-heatmap-cell:focus-visible::before { + opacity: 1; +} + +.smart-review-heatmap-cell:hover::after, +.smart-review-heatmap-cell:focus-visible::after { + transform: translate(-50%, 0); } .smart-review-heatmap-level-1 { - background: color-mix(in srgb, var(--interactive-accent) 20%, var(--smart-review-card-bg)); + background-color: color-mix(in srgb, var(--interactive-accent) 20%, var(--smart-review-card-bg)) !important; } .smart-review-heatmap-level-2 { - background: color-mix(in srgb, var(--interactive-accent) 38%, var(--smart-review-card-bg)); + background-color: color-mix(in srgb, var(--interactive-accent) 38%, var(--smart-review-card-bg)) !important; } .smart-review-heatmap-level-3 { - background: color-mix(in srgb, var(--interactive-accent) 58%, var(--smart-review-card-bg)); + background-color: color-mix(in srgb, var(--interactive-accent) 58%, var(--smart-review-card-bg)) !important; } .smart-review-heatmap-level-4 { - background: color-mix(in srgb, var(--interactive-accent) 78%, var(--smart-review-card-bg)); + background-color: color-mix(in srgb, var(--interactive-accent) 78%, var(--smart-review-card-bg)) !important; } .smart-review-distributions-grid { diff --git a/apps/smart-review-plugin/versions.json b/apps/smart-review-plugin/versions.json index 66d7dc5..9533b52 100644 --- a/apps/smart-review-plugin/versions.json +++ b/apps/smart-review-plugin/versions.json @@ -1,5 +1,6 @@ { "0.1.0": "1.5.0", "0.1.1": "1.5.0", - "0.2.0": "1.5.0" + "0.2.0": "1.5.0", + "0.2.1": "1.5.0" } diff --git a/manifest.json b/manifest.json index e335773..68ece30 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "smart-review", "name": "Smart Review", - "version": "0.2.0", + "version": "0.2.1", "minAppVersion": "1.5.0", "description": "Review center for Properties and YAML frontmatter with spaced feedback, history, and AI payload export.", "author": "Jayce", diff --git a/package.json b/package.json index 102f744..70088ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smart-review", - "version": "0.2.0", + "version": "0.2.1", "private": true, "description": "Smart Review plugin workspace.", "type": "module", diff --git a/packages/shared/package.json b/packages/shared/package.json index 4bf736a..0326d7b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@smart-review/shared", - "version": "0.2.0", + "version": "0.2.1", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/versions.json b/versions.json index 66d7dc5..9533b52 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,6 @@ { "0.1.0": "1.5.0", "0.1.1": "1.5.0", - "0.2.0": "1.5.0" + "0.2.0": "1.5.0", + "0.2.1": "1.5.0" }