@tnotesjs/ui 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/alerts/icons.d.ts +8 -0
- package/dist/alerts/icons.js +29 -0
- package/dist/markdown/lineBreaks.d.ts +47 -0
- package/dist/markdown/lineBreaks.js +78 -0
- package/dist/swiper/hydrate.d.ts +4 -1
- package/dist/swiper/hydrate.js +9 -4
- package/package.json +23 -11
- package/src/alerts/icons.ts +32 -0
- package/src/browser/fullscreenLayer.ts +16 -0
- package/src/components/ContributionHeatmap/ContributionHeatmap.vue +638 -0
- package/src/components/ImagePreview/ImagePreview.vue +29 -6
- package/src/components/Mermaid/Mermaid.vue +5 -1
- package/src/components/Mindmap/InlineRuns.ts +4 -1
- package/src/components/Mindmap/Mindmap.vue +27 -12
- package/src/components/Mindmap/editor/AppIcon.vue +0 -3
- package/src/components/Mindmap/editor/MindmapView.vue +16 -1
- package/src/components/Mindmap/editor/OutlineView.vue +44 -3
- package/src/components/Mindmap/editor/RichInlineEditor.vue +56 -22
- package/src/components/Mindmap/editor/SearchBar.vue +8 -2
- package/src/components/Mindmap/editor/SelectionToolbar.vue +2 -7
- package/src/components/Mindmap/editor/mindmapClipboard.ts +4 -4
- package/src/components/Mindmap/inlineOptions.ts +21 -0
- package/src/components/WordList/WordList.vue +29 -9
- package/src/components/WordList/wordListStorage.ts +21 -0
- package/src/index.ts +3 -0
- package/src/markdown/lineBreaks.ts +133 -0
- package/src/styles/prose.css +138 -30
- package/src/swiper/hydrate.ts +15 -4
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
完成热力图(GitHub contribution graph 风格)
|
|
3
|
+
|
|
4
|
+
⚠️ 本文件在两处保持逐字节一致,改动请同步:
|
|
5
|
+
- tnotes/packages/ui/src/components/ContributionHeatmap/ContributionHeatmap.vue(@tnotesjs/ui,子库站点 SSG 使用)
|
|
6
|
+
- TNotes.root/src/components/ContributionHeatmap.vue(根库站点;根库未依赖 @tnotesjs/ui,暂以副本形式存在)
|
|
7
|
+
|
|
8
|
+
着色:按「当天 commit 数」(更新热度,非负)
|
|
9
|
+
- GitHub 同款 4 档绿色,强度按当年最大 commit 数做对数分档
|
|
10
|
+
- 0 / 无记录:GitHub 空格子色(暗色主题下比背景亮一档,避免黑底黑格)
|
|
11
|
+
底栏左侧:当年累计完成数(+ 本年净增),静态,不随悬停变化;
|
|
12
|
+
单日详情(commits / delta / total)只在悬停气泡里展示;
|
|
13
|
+
delta < 0(取消勾选净减)在详情中以粉色标注。
|
|
14
|
+
顶栏:左侧为当前范围 + 年份的 commit 总数,右侧为年份下拉(无标题)。
|
|
15
|
+
-->
|
|
16
|
+
<template>
|
|
17
|
+
<div v-if="cells.length" ref="rootRef" class="tn-heatmap">
|
|
18
|
+
<div class="tn-heatmap-toolbar">
|
|
19
|
+
<div class="tn-heatmap-total">
|
|
20
|
+
<strong>{{ yearStats.commits }}</strong> 次提交 · {{ activeYear }} 年
|
|
21
|
+
</div>
|
|
22
|
+
<select
|
|
23
|
+
v-if="years.length"
|
|
24
|
+
v-model="activeYear"
|
|
25
|
+
class="tn-heatmap-year-select"
|
|
26
|
+
aria-label="选择年份"
|
|
27
|
+
>
|
|
28
|
+
<option v-for="y in yearsDesc" :key="y" :value="y">{{ y }}</option>
|
|
29
|
+
</select>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="tn-heatmap-panel">
|
|
33
|
+
<div ref="scrollRef" class="tn-heatmap-scroll" @mouseleave="hideTip">
|
|
34
|
+
<div class="tn-heatmap-graph">
|
|
35
|
+
<div class="tn-heatmap-months" :style="{ width: gridWidth }">
|
|
36
|
+
<span v-for="m in monthLabels" :key="m.month" :style="{ left: colOffset(m.col) }">{{
|
|
37
|
+
m.text
|
|
38
|
+
}}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="tn-heatmap-body">
|
|
41
|
+
<div class="tn-heatmap-weekdays" aria-hidden="true">
|
|
42
|
+
<span v-for="(label, i) in weekdayLabels" :key="i">{{ label }}</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div
|
|
45
|
+
class="tn-heatmap-cells"
|
|
46
|
+
:style="{ gridTemplateColumns: `repeat(${weekCount}, var(--tn-hm-cell))` }"
|
|
47
|
+
>
|
|
48
|
+
<span
|
|
49
|
+
v-for="cell in yearCells"
|
|
50
|
+
:key="cell.date"
|
|
51
|
+
class="tn-heatmap-cell"
|
|
52
|
+
:class="levelClass(cell.level)"
|
|
53
|
+
:style="{ gridColumn: String(cell.week + 1), gridRow: String(cell.weekday + 1) }"
|
|
54
|
+
:aria-label="cell.level === LEVEL_OUT ? undefined : cellTitle(cell)"
|
|
55
|
+
:aria-hidden="cell.level === LEVEL_OUT ? 'true' : undefined"
|
|
56
|
+
@mouseenter="showTip(cell, $event)"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="tn-heatmap-footer">
|
|
64
|
+
<div class="tn-heatmap-detail">
|
|
65
|
+
<span class="tn-heatmap-year-done">
|
|
66
|
+
{{ activeYear }} 年累计完成 <strong>{{ yearStats.total }}</strong> 篇<template
|
|
67
|
+
v-if="yearStats.net"
|
|
68
|
+
>
|
|
69
|
+
· 本年
|
|
70
|
+
<span :class="deltaClass(yearStats.net)"
|
|
71
|
+
>{{ yearStats.net > 0 ? '+' : '' }}{{ yearStats.net }}</span
|
|
72
|
+
></template
|
|
73
|
+
>
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="tn-heatmap-legend">
|
|
77
|
+
<span>少</span>
|
|
78
|
+
<i class="tn-heatmap-swatch l0" />
|
|
79
|
+
<i class="tn-heatmap-swatch l1" />
|
|
80
|
+
<i class="tn-heatmap-swatch l2" />
|
|
81
|
+
<i class="tn-heatmap-swatch l3" />
|
|
82
|
+
<i class="tn-heatmap-swatch l4" />
|
|
83
|
+
<span>多</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div
|
|
88
|
+
v-if="tip"
|
|
89
|
+
class="tn-heatmap-tooltip"
|
|
90
|
+
role="tooltip"
|
|
91
|
+
:style="{ left: `${tip.x}px`, top: `${tip.y}px` }"
|
|
92
|
+
>
|
|
93
|
+
<div>{{ tip.cell.date }} · {{ tip.cell.commits }} 次提交</div>
|
|
94
|
+
<div v-if="tip.cell.recorded" class="tn-heatmap-tooltip-sub">
|
|
95
|
+
<span :class="deltaClass(tip.cell.delta)">{{ deltaText(tip.cell.delta) }}</span>
|
|
96
|
+
· 累计 {{ tip.cell.total }}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
|
|
103
|
+
<script setup lang="ts">
|
|
104
|
+
import { computed, nextTick, ref, watch } from 'vue'
|
|
105
|
+
|
|
106
|
+
export interface DayStat {
|
|
107
|
+
delta: number
|
|
108
|
+
total: number
|
|
109
|
+
/** 当天 commit 数(tnotes.stats.json v2);着色依据 */
|
|
110
|
+
commits?: number
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type ByYear = Record<string, Record<string, Record<string, DayStat>>>
|
|
114
|
+
|
|
115
|
+
const props = defineProps<{
|
|
116
|
+
byYear: ByYear
|
|
117
|
+
title?: string
|
|
118
|
+
}>()
|
|
119
|
+
|
|
120
|
+
/** 年外补位格(不显示) */
|
|
121
|
+
const LEVEL_OUT = -99
|
|
122
|
+
|
|
123
|
+
interface Cell {
|
|
124
|
+
date: string
|
|
125
|
+
year: number
|
|
126
|
+
month: number
|
|
127
|
+
day: number
|
|
128
|
+
delta: number
|
|
129
|
+
total: number
|
|
130
|
+
commits: number
|
|
131
|
+
/** 当天是否有统计记录 */
|
|
132
|
+
recorded: boolean
|
|
133
|
+
week: number
|
|
134
|
+
weekday: number
|
|
135
|
+
/** LEVEL_OUT 年外;0 空;1..4 绿(按 commits) */
|
|
136
|
+
level: number
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const years = computed(() => Object.keys(props.byYear || {}).sort())
|
|
140
|
+
const yearsDesc = computed(() => [...years.value].reverse())
|
|
141
|
+
const activeYear = ref<string>('')
|
|
142
|
+
watch(
|
|
143
|
+
years,
|
|
144
|
+
(list) => {
|
|
145
|
+
if (!list.length) {
|
|
146
|
+
activeYear.value = ''
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if (!list.includes(activeYear.value)) {
|
|
150
|
+
activeYear.value = list[list.length - 1]
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{ immediate: true }
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
function pad2(n: number): string {
|
|
157
|
+
return String(n).padStart(2, '0')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function fmtDate(dt: Date): string {
|
|
161
|
+
return `${dt.getFullYear()}-${pad2(dt.getMonth() + 1)}-${pad2(dt.getDate())}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 周一 = 0 … 周日 = 6 */
|
|
165
|
+
function mondayIndex(dt: Date): number {
|
|
166
|
+
return (dt.getDay() + 6) % 7
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function flatten(byYear: ByYear): Cell[] {
|
|
170
|
+
const out: Cell[] = []
|
|
171
|
+
for (const y of Object.keys(byYear || {})) {
|
|
172
|
+
for (const m of Object.keys(byYear[y] || {})) {
|
|
173
|
+
for (const d of Object.keys(byYear[y][m] || {})) {
|
|
174
|
+
const stat = byYear[y][m][d]
|
|
175
|
+
const dt = new Date(Number(y), Number(m) - 1, Number(d))
|
|
176
|
+
out.push({
|
|
177
|
+
date: fmtDate(dt),
|
|
178
|
+
year: dt.getFullYear(),
|
|
179
|
+
month: dt.getMonth() + 1,
|
|
180
|
+
day: dt.getDate(),
|
|
181
|
+
delta: Number(stat?.delta) || 0,
|
|
182
|
+
total: Number(stat?.total) || 0,
|
|
183
|
+
commits: Number(stat?.commits) || 0,
|
|
184
|
+
recorded: true,
|
|
185
|
+
week: 0,
|
|
186
|
+
weekday: mondayIndex(dt),
|
|
187
|
+
level: 0
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const cells = computed(() => flatten(props.byYear || {}))
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 强度分档:按当天 commit 数对数分档 → 1..4(绿)。
|
|
199
|
+
*/
|
|
200
|
+
function levelFor(commits: number, max: number): number {
|
|
201
|
+
if (commits <= 0 || max <= 0) return 0
|
|
202
|
+
// 刻度下限取 4,避免全年最大值很小(如只有 1)时所有格子直接顶到最深档
|
|
203
|
+
const ratio = Math.min(1, Math.log1p(commits) / Math.log1p(Math.max(max, 4)))
|
|
204
|
+
return Math.min(4, Math.max(1, Math.ceil(ratio * 4)))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const yearStats = computed(() => {
|
|
208
|
+
const y = Number(activeYear.value)
|
|
209
|
+
const list = cells.value.filter((c) => c.year === y)
|
|
210
|
+
let commits = 0
|
|
211
|
+
let maxCommits = 0
|
|
212
|
+
let net = 0
|
|
213
|
+
let total = 0
|
|
214
|
+
let lastDate = ''
|
|
215
|
+
for (const c of list) {
|
|
216
|
+
commits += c.commits
|
|
217
|
+
net += c.delta
|
|
218
|
+
maxCommits = Math.max(maxCommits, c.commits)
|
|
219
|
+
if (c.date > lastDate) {
|
|
220
|
+
lastDate = c.date
|
|
221
|
+
total = c.total
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// total:该年最后一个有记录日的累计完成数;net:该年净增
|
|
225
|
+
return { byDate: new Map(list.map((c) => [c.date, c])), commits, maxCommits, total, net }
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const yearCells = computed<Cell[]>(() => {
|
|
229
|
+
const y = Number(activeYear.value)
|
|
230
|
+
if (!y) return []
|
|
231
|
+
const first = new Date(y, 0, 1)
|
|
232
|
+
const last = new Date(y, 11, 31)
|
|
233
|
+
// 网格从 1/1 所在周的周一开始,到 12/31 所在周的周日结束(周一为每列第一行)
|
|
234
|
+
const cursor = new Date(first)
|
|
235
|
+
cursor.setDate(first.getDate() - mondayIndex(first))
|
|
236
|
+
const gridEnd = new Date(last)
|
|
237
|
+
gridEnd.setDate(last.getDate() + (6 - mondayIndex(last)))
|
|
238
|
+
|
|
239
|
+
const { byDate, maxCommits } = yearStats.value
|
|
240
|
+
const out: Cell[] = []
|
|
241
|
+
let week = 0
|
|
242
|
+
while (cursor <= gridEnd) {
|
|
243
|
+
const date = fmtDate(cursor)
|
|
244
|
+
const weekday = mondayIndex(cursor)
|
|
245
|
+
const inYear = cursor.getFullYear() === y
|
|
246
|
+
const hit = inYear ? byDate.get(date) : undefined
|
|
247
|
+
out.push({
|
|
248
|
+
date,
|
|
249
|
+
year: cursor.getFullYear(),
|
|
250
|
+
month: cursor.getMonth() + 1,
|
|
251
|
+
day: cursor.getDate(),
|
|
252
|
+
delta: hit?.delta ?? 0,
|
|
253
|
+
total: hit?.total ?? 0,
|
|
254
|
+
commits: hit?.commits ?? 0,
|
|
255
|
+
recorded: Boolean(hit),
|
|
256
|
+
week,
|
|
257
|
+
weekday,
|
|
258
|
+
level: !inYear ? LEVEL_OUT : hit ? levelFor(hit.commits, maxCommits) : 0
|
|
259
|
+
})
|
|
260
|
+
if (weekday === 6) week += 1
|
|
261
|
+
cursor.setDate(cursor.getDate() + 1)
|
|
262
|
+
}
|
|
263
|
+
return out
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const weekCount = computed(() => Math.max(...yearCells.value.map((c) => c.week + 1), 1))
|
|
267
|
+
|
|
268
|
+
/** 与 GitHub 一样只标隔行(周一为首行:一、三、五、日) */
|
|
269
|
+
const weekdayLabels = ['一', '', '三', '', '五', '', '日']
|
|
270
|
+
|
|
271
|
+
function colOffset(col: number): string {
|
|
272
|
+
return `calc(${col} * (var(--tn-hm-cell) + var(--tn-hm-gap)))`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const gridWidth = computed(
|
|
276
|
+
() => `calc(${weekCount.value} * (var(--tn-hm-cell) + var(--tn-hm-gap)) - var(--tn-hm-gap))`
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 月份标签:落在该月 1 号所在列(1 号在周五及以后时顺延一列,和 GitHub 一样让标签对齐“整周”),
|
|
281
|
+
* 与上一个标签相距不足 3 列时丢弃上一个,避免文字重叠。
|
|
282
|
+
*/
|
|
283
|
+
const monthLabels = computed(() => {
|
|
284
|
+
const y = Number(activeYear.value)
|
|
285
|
+
if (!y) return []
|
|
286
|
+
const byDate = new Map(yearCells.value.map((c) => [c.date, c]))
|
|
287
|
+
const labels: Array<{ month: number; text: string; col: number }> = []
|
|
288
|
+
for (let m = 1; m <= 12; m++) {
|
|
289
|
+
const cell = byDate.get(`${y}-${pad2(m)}-01`)
|
|
290
|
+
if (!cell) continue
|
|
291
|
+
const col = cell.weekday >= 4 && m !== 1 ? cell.week + 1 : cell.week
|
|
292
|
+
const prev = labels[labels.length - 1]
|
|
293
|
+
if (prev && col - prev.col < 3) labels.pop()
|
|
294
|
+
labels.push({ month: m, text: `${m}月`, col })
|
|
295
|
+
}
|
|
296
|
+
return labels
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
function levelClass(level: number): string {
|
|
300
|
+
if (level === LEVEL_OUT) return 'out'
|
|
301
|
+
if (level > 0) return `l${level}`
|
|
302
|
+
return 'l0'
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function deltaText(delta: number): string {
|
|
306
|
+
if (delta > 0) return `完成 +${delta}`
|
|
307
|
+
if (delta < 0) return `回退 ${delta}`
|
|
308
|
+
return '完成 ±0'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function deltaClass(delta: number): string {
|
|
312
|
+
if (delta > 0) return 'tn-hm-pos'
|
|
313
|
+
if (delta < 0) return 'tn-hm-neg'
|
|
314
|
+
return ''
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function cellTitle(cell: Cell): string {
|
|
318
|
+
const head = `${cell.date} · ${cell.commits} 次提交`
|
|
319
|
+
return cell.recorded ? `${head} · ${deltaText(cell.delta)} · 累计 ${cell.total}` : head
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---- tooltip(GitHub 风格:悬浮在格子正上方) ----
|
|
323
|
+
const rootRef = ref<HTMLElement | null>(null)
|
|
324
|
+
const scrollRef = ref<HTMLElement | null>(null)
|
|
325
|
+
const tip = ref<{ cell: Cell; x: number; y: number } | null>(null)
|
|
326
|
+
function showTip(cell: Cell, ev: MouseEvent): void {
|
|
327
|
+
const root = rootRef.value
|
|
328
|
+
const target = ev.currentTarget as HTMLElement | null
|
|
329
|
+
if (!root || !target || cell.level === LEVEL_OUT) {
|
|
330
|
+
tip.value = null
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
const r = target.getBoundingClientRect()
|
|
334
|
+
const base = root.getBoundingClientRect()
|
|
335
|
+
tip.value = {
|
|
336
|
+
cell,
|
|
337
|
+
x: r.left - base.left + r.width / 2,
|
|
338
|
+
y: r.top - base.top - 6
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function hideTip(): void {
|
|
343
|
+
tip.value = null
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 窄容器(如子库侧边栏)里横向滚动时,默认把最近有记录的那一列滚进视野
|
|
347
|
+
watch(
|
|
348
|
+
[activeYear, yearCells],
|
|
349
|
+
async () => {
|
|
350
|
+
tip.value = null
|
|
351
|
+
await nextTick()
|
|
352
|
+
const el = scrollRef.value
|
|
353
|
+
if (!el || el.scrollWidth <= el.clientWidth) return
|
|
354
|
+
let lastWeek = 0
|
|
355
|
+
for (const c of yearCells.value) if (c.recorded) lastWeek = c.week
|
|
356
|
+
const cs = getComputedStyle(el)
|
|
357
|
+
const pitch =
|
|
358
|
+
(parseFloat(cs.getPropertyValue('--tn-hm-cell')) || 10) +
|
|
359
|
+
(parseFloat(cs.getPropertyValue('--tn-hm-gap')) || 3)
|
|
360
|
+
el.scrollLeft = Math.max(0, (lastWeek + 4) * pitch - el.clientWidth + 32)
|
|
361
|
+
},
|
|
362
|
+
{ flush: 'post', immediate: true }
|
|
363
|
+
)
|
|
364
|
+
</script>
|
|
365
|
+
|
|
366
|
+
<style scoped>
|
|
367
|
+
.tn-heatmap {
|
|
368
|
+
/* 尺寸:对齐 GitHub(10px 格 + 3px 间距) */
|
|
369
|
+
--tn-hm-cell: 10px;
|
|
370
|
+
--tn-hm-gap: 3px;
|
|
371
|
+
--tn-hm-radius: 2px;
|
|
372
|
+
--tn-hm-label-w: 22px;
|
|
373
|
+
|
|
374
|
+
/* 文本 / 边框(兼容根库 --vp-* 与子库 SSG --tn-* 变量) */
|
|
375
|
+
--tn-hm-text: var(--vp-c-text-1, var(--tn-c-text, #1f2328));
|
|
376
|
+
--tn-hm-text-2: var(--vp-c-text-2, var(--tn-c-text-2, #59636e));
|
|
377
|
+
--tn-hm-text-3: var(--vp-c-text-3, var(--tn-c-text-2, #8c959f));
|
|
378
|
+
--tn-hm-divider: var(--vp-c-divider, var(--tn-c-divider, #d1d9e0));
|
|
379
|
+
--tn-hm-bg: var(--vp-c-bg, var(--tn-c-bg, #ffffff));
|
|
380
|
+
--tn-hm-bg-soft: var(--vp-c-bg-soft, var(--tn-c-bg-soft, #f6f8fa));
|
|
381
|
+
--tn-hm-brand: var(--vp-c-brand, var(--tn-c-brand, #3451b2));
|
|
382
|
+
|
|
383
|
+
/* GitHub light 色阶 */
|
|
384
|
+
--tn-hm-l0: #ebedf0;
|
|
385
|
+
--tn-hm-l1: #9be9a8;
|
|
386
|
+
--tn-hm-l2: #40c463;
|
|
387
|
+
--tn-hm-l3: #30a14e;
|
|
388
|
+
--tn-hm-l4: #216e39;
|
|
389
|
+
/* 回退(粉 / 浅红) */
|
|
390
|
+
--tn-hm-n1: #ffd3dc;
|
|
391
|
+
--tn-hm-n2: #ff9aac;
|
|
392
|
+
--tn-hm-n3: #e5566f;
|
|
393
|
+
--tn-hm-cell-outline: rgba(27, 31, 36, 0.06);
|
|
394
|
+
--tn-hm-tip-bg: #24292f;
|
|
395
|
+
--tn-hm-tip-fg: #ffffff;
|
|
396
|
+
|
|
397
|
+
display: flex;
|
|
398
|
+
flex-direction: column;
|
|
399
|
+
gap: 8px;
|
|
400
|
+
padding: 10px 12px 12px;
|
|
401
|
+
margin-bottom: 10px;
|
|
402
|
+
border: 1px solid var(--tn-glass-border, var(--tn-hm-divider));
|
|
403
|
+
border-radius: 12px;
|
|
404
|
+
background: color-mix(in srgb, var(--tn-hm-bg-soft) 70%, transparent);
|
|
405
|
+
position: relative;
|
|
406
|
+
min-width: 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* GitHub dark 色阶;空格子比面板背景亮一档,避免黑底黑格 */
|
|
410
|
+
.dark .tn-heatmap,
|
|
411
|
+
:root[data-theme='dark'] .tn-heatmap {
|
|
412
|
+
--tn-hm-l0: #2d333b;
|
|
413
|
+
--tn-hm-l1: #0e4429;
|
|
414
|
+
--tn-hm-l2: #006d32;
|
|
415
|
+
--tn-hm-l3: #26a641;
|
|
416
|
+
--tn-hm-l4: #39d353;
|
|
417
|
+
--tn-hm-n1: #6b2a3a;
|
|
418
|
+
--tn-hm-n2: #a3384f;
|
|
419
|
+
--tn-hm-n3: #f0738b;
|
|
420
|
+
--tn-hm-cell-outline: rgba(255, 255, 255, 0.04);
|
|
421
|
+
--tn-hm-tip-bg: #6e7681;
|
|
422
|
+
--tn-hm-tip-fg: #ffffff;
|
|
423
|
+
}
|
|
424
|
+
:root[data-theme='light'] .tn-heatmap {
|
|
425
|
+
--tn-hm-l0: #ebedf0;
|
|
426
|
+
--tn-hm-l1: #9be9a8;
|
|
427
|
+
--tn-hm-l2: #40c463;
|
|
428
|
+
--tn-hm-l3: #30a14e;
|
|
429
|
+
--tn-hm-l4: #216e39;
|
|
430
|
+
--tn-hm-n1: #ffd3dc;
|
|
431
|
+
--tn-hm-n2: #ff9aac;
|
|
432
|
+
--tn-hm-n3: #e5566f;
|
|
433
|
+
--tn-hm-cell-outline: rgba(27, 31, 36, 0.06);
|
|
434
|
+
--tn-hm-tip-bg: #24292f;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.tn-heatmap-toolbar {
|
|
438
|
+
display: flex;
|
|
439
|
+
flex-wrap: wrap;
|
|
440
|
+
align-items: center;
|
|
441
|
+
gap: 8px 12px;
|
|
442
|
+
}
|
|
443
|
+
.tn-heatmap-total {
|
|
444
|
+
font-size: 12px;
|
|
445
|
+
color: var(--tn-hm-text-2);
|
|
446
|
+
}
|
|
447
|
+
.tn-heatmap-total strong {
|
|
448
|
+
color: var(--tn-hm-text);
|
|
449
|
+
font-weight: 600;
|
|
450
|
+
}
|
|
451
|
+
.tn-heatmap-year-select {
|
|
452
|
+
margin-left: auto;
|
|
453
|
+
border: 1px solid var(--tn-hm-divider);
|
|
454
|
+
color: var(--tn-hm-text-2);
|
|
455
|
+
border-radius: 6px;
|
|
456
|
+
/* 自绘箭头:原生箭头贴边,改为 appearance:none + 右侧留白 */
|
|
457
|
+
appearance: none;
|
|
458
|
+
-webkit-appearance: none;
|
|
459
|
+
background-color: var(--tn-hm-bg);
|
|
460
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238c959f' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
461
|
+
background-repeat: no-repeat;
|
|
462
|
+
background-position: right 8px center;
|
|
463
|
+
background-size: 10px 6px;
|
|
464
|
+
padding: 2px 24px 2px 8px;
|
|
465
|
+
font-size: 12px;
|
|
466
|
+
line-height: 18px;
|
|
467
|
+
cursor: pointer;
|
|
468
|
+
}
|
|
469
|
+
.tn-heatmap-year-select:focus-visible {
|
|
470
|
+
outline: 2px solid color-mix(in srgb, var(--tn-hm-brand) 50%, transparent);
|
|
471
|
+
outline-offset: 1px;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* ---- 网格 ---- */
|
|
475
|
+
/* 面板宽度跟随图本身(像 GitHub 一样让图例贴着图的右边缘),窄容器里再横向滚动 */
|
|
476
|
+
.tn-heatmap-panel {
|
|
477
|
+
display: flex;
|
|
478
|
+
flex-direction: column;
|
|
479
|
+
gap: 6px;
|
|
480
|
+
width: max-content;
|
|
481
|
+
max-width: 100%;
|
|
482
|
+
}
|
|
483
|
+
.tn-heatmap-scroll {
|
|
484
|
+
overflow-x: auto;
|
|
485
|
+
overflow-y: hidden;
|
|
486
|
+
padding-bottom: 2px;
|
|
487
|
+
scrollbar-width: thin;
|
|
488
|
+
}
|
|
489
|
+
.tn-heatmap-graph {
|
|
490
|
+
display: inline-flex;
|
|
491
|
+
flex-direction: column;
|
|
492
|
+
gap: 4px;
|
|
493
|
+
font-size: 10px;
|
|
494
|
+
line-height: 1;
|
|
495
|
+
color: var(--tn-hm-text-2);
|
|
496
|
+
}
|
|
497
|
+
.tn-heatmap-months {
|
|
498
|
+
position: relative;
|
|
499
|
+
height: 12px;
|
|
500
|
+
margin-left: calc(var(--tn-hm-label-w) + var(--tn-hm-gap));
|
|
501
|
+
}
|
|
502
|
+
.tn-heatmap-months span {
|
|
503
|
+
position: absolute;
|
|
504
|
+
top: 0;
|
|
505
|
+
white-space: nowrap;
|
|
506
|
+
}
|
|
507
|
+
.tn-heatmap-body {
|
|
508
|
+
display: flex;
|
|
509
|
+
gap: var(--tn-hm-gap);
|
|
510
|
+
}
|
|
511
|
+
.tn-heatmap-weekdays {
|
|
512
|
+
width: var(--tn-hm-label-w);
|
|
513
|
+
flex: none;
|
|
514
|
+
display: grid;
|
|
515
|
+
grid-template-rows: repeat(7, var(--tn-hm-cell));
|
|
516
|
+
row-gap: var(--tn-hm-gap);
|
|
517
|
+
}
|
|
518
|
+
.tn-heatmap-weekdays span {
|
|
519
|
+
font-size: 9px;
|
|
520
|
+
line-height: var(--tn-hm-cell);
|
|
521
|
+
height: var(--tn-hm-cell);
|
|
522
|
+
overflow: visible;
|
|
523
|
+
white-space: nowrap;
|
|
524
|
+
}
|
|
525
|
+
.tn-heatmap-cells {
|
|
526
|
+
display: grid;
|
|
527
|
+
grid-template-rows: repeat(7, var(--tn-hm-cell));
|
|
528
|
+
gap: var(--tn-hm-gap);
|
|
529
|
+
}
|
|
530
|
+
.tn-heatmap-cell,
|
|
531
|
+
.tn-heatmap-swatch {
|
|
532
|
+
display: block;
|
|
533
|
+
width: var(--tn-hm-cell);
|
|
534
|
+
height: var(--tn-hm-cell);
|
|
535
|
+
border-radius: var(--tn-hm-radius);
|
|
536
|
+
outline: 1px solid var(--tn-hm-cell-outline);
|
|
537
|
+
outline-offset: -1px;
|
|
538
|
+
background: var(--tn-hm-l0);
|
|
539
|
+
}
|
|
540
|
+
.tn-heatmap-cell:hover {
|
|
541
|
+
outline: 1px solid var(--tn-hm-text-2);
|
|
542
|
+
outline-offset: -1px;
|
|
543
|
+
}
|
|
544
|
+
.tn-heatmap-cell.out {
|
|
545
|
+
visibility: hidden;
|
|
546
|
+
}
|
|
547
|
+
.l0 {
|
|
548
|
+
background: var(--tn-hm-l0);
|
|
549
|
+
}
|
|
550
|
+
.l1 {
|
|
551
|
+
background: var(--tn-hm-l1);
|
|
552
|
+
}
|
|
553
|
+
.l2 {
|
|
554
|
+
background: var(--tn-hm-l2);
|
|
555
|
+
}
|
|
556
|
+
.l3 {
|
|
557
|
+
background: var(--tn-hm-l3);
|
|
558
|
+
}
|
|
559
|
+
.l4 {
|
|
560
|
+
background: var(--tn-hm-l4);
|
|
561
|
+
}
|
|
562
|
+
/* 详情中的完成数:正 → 绿,负(回退)→ 粉 */
|
|
563
|
+
.tn-hm-pos {
|
|
564
|
+
color: var(--tn-hm-l3);
|
|
565
|
+
}
|
|
566
|
+
.tn-hm-neg {
|
|
567
|
+
color: var(--tn-hm-n3);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ---- 底栏:左侧当年完成汇总(静态),右侧图例(少 → 多) ---- */
|
|
571
|
+
.tn-heatmap-footer {
|
|
572
|
+
display: flex;
|
|
573
|
+
flex-wrap: wrap;
|
|
574
|
+
align-items: center;
|
|
575
|
+
justify-content: space-between;
|
|
576
|
+
gap: 4px 16px;
|
|
577
|
+
font-size: 11px;
|
|
578
|
+
color: var(--tn-hm-text-2);
|
|
579
|
+
}
|
|
580
|
+
.tn-heatmap-detail {
|
|
581
|
+
display: inline-flex;
|
|
582
|
+
flex-wrap: wrap;
|
|
583
|
+
gap: 2px 8px;
|
|
584
|
+
min-height: 14px;
|
|
585
|
+
color: var(--tn-hm-text-2);
|
|
586
|
+
}
|
|
587
|
+
.tn-heatmap-year-done strong {
|
|
588
|
+
color: var(--tn-hm-text);
|
|
589
|
+
font-weight: 600;
|
|
590
|
+
}
|
|
591
|
+
.tn-heatmap-legend {
|
|
592
|
+
display: inline-flex;
|
|
593
|
+
align-items: center;
|
|
594
|
+
gap: var(--tn-hm-gap);
|
|
595
|
+
line-height: 1;
|
|
596
|
+
}
|
|
597
|
+
.tn-heatmap-legend > span:first-child {
|
|
598
|
+
margin-right: 2px;
|
|
599
|
+
}
|
|
600
|
+
.tn-heatmap-legend > span:nth-of-type(2) {
|
|
601
|
+
margin-left: 2px;
|
|
602
|
+
}
|
|
603
|
+
.tn-heatmap-tooltip {
|
|
604
|
+
position: absolute;
|
|
605
|
+
z-index: 10;
|
|
606
|
+
transform: translate(-50%, -100%);
|
|
607
|
+
pointer-events: none;
|
|
608
|
+
white-space: nowrap;
|
|
609
|
+
font-size: 12px;
|
|
610
|
+
line-height: 1.4;
|
|
611
|
+
color: var(--tn-hm-tip-fg);
|
|
612
|
+
background: var(--tn-hm-tip-bg);
|
|
613
|
+
border-radius: 6px;
|
|
614
|
+
padding: 4px 8px;
|
|
615
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
|
616
|
+
}
|
|
617
|
+
.tn-heatmap-tooltip-sub {
|
|
618
|
+
font-size: 11px;
|
|
619
|
+
opacity: 0.9;
|
|
620
|
+
}
|
|
621
|
+
/* tooltip 深底上用更亮的粉 / 绿 */
|
|
622
|
+
.tn-heatmap-tooltip .tn-hm-neg {
|
|
623
|
+
color: #ff9aac;
|
|
624
|
+
}
|
|
625
|
+
.tn-heatmap-tooltip .tn-hm-pos {
|
|
626
|
+
color: #7ee2a0;
|
|
627
|
+
}
|
|
628
|
+
.tn-heatmap-tooltip::after {
|
|
629
|
+
content: '';
|
|
630
|
+
position: absolute;
|
|
631
|
+
left: 50%;
|
|
632
|
+
bottom: -4px;
|
|
633
|
+
width: 8px;
|
|
634
|
+
height: 8px;
|
|
635
|
+
background: inherit;
|
|
636
|
+
transform: translateX(-50%) rotate(45deg);
|
|
637
|
+
}
|
|
638
|
+
</style>
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
3
|
|
|
4
|
-
const props = withDefaults(
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
/** 点击哪些图直接打开预览(文档级 click 委托用)。 */
|
|
7
|
+
selector?: string
|
|
8
|
+
/** 打开后左右翻页的图集;只在 `scope` 命中的同一根节点里收集。缺省同 `selector`。 */
|
|
9
|
+
gallerySelector?: string
|
|
10
|
+
/**
|
|
11
|
+
* 图集的根:从被点的图往上找最近的匹配节点,只收它里面的图。
|
|
12
|
+
* Desk 多标签 / 分栏同时挂着好几个编辑器(`.cm-editor`),不能扫整页 document,
|
|
13
|
+
* 否则会把别的笔记的图串进来。找不到时退回 document(站点单页)。
|
|
14
|
+
*/
|
|
15
|
+
scope?: string
|
|
16
|
+
}>(),
|
|
17
|
+
{
|
|
18
|
+
selector: '.tn-prose img, .vp-doc img',
|
|
19
|
+
gallerySelector: '',
|
|
20
|
+
scope: '[data-tn-preview-scope], .cm-editor, .vp-doc'
|
|
21
|
+
}
|
|
22
|
+
)
|
|
7
23
|
const visible = ref(false)
|
|
8
24
|
const images = ref<string[]>([])
|
|
9
25
|
const index = ref(0)
|
|
@@ -31,10 +47,17 @@ function reset(): void {
|
|
|
31
47
|
y.value = 0
|
|
32
48
|
}
|
|
33
49
|
|
|
50
|
+
/** 当前图所在的预览根(笔记 / 编辑器),而不是整页 document。 */
|
|
51
|
+
function previewRoot(image: HTMLImageElement): Document | Element {
|
|
52
|
+
return (props.scope ? image.closest(props.scope) : null) ?? image.ownerDocument ?? document
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
function open(image: HTMLImageElement): void {
|
|
35
|
-
const candidates = [
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
const candidates = [
|
|
57
|
+
...previewRoot(image).querySelectorAll<HTMLImageElement>(
|
|
58
|
+
props.gallerySelector || props.selector
|
|
59
|
+
)
|
|
60
|
+
].filter(eligible)
|
|
38
61
|
images.value = candidates.map((item) => item.currentSrc || item.src).filter(Boolean)
|
|
39
62
|
const selected = image.currentSrc || image.src
|
|
40
63
|
const selectedIndex = images.value.indexOf(selected)
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
import mermaid from 'mermaid'
|
|
86
86
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
87
87
|
|
|
88
|
+
import { setFullscreenLayer } from '../../browser/fullscreenLayer'
|
|
88
89
|
import { adoptLiveSvg } from './adoptLiveSvg'
|
|
89
90
|
import iconCenterOff from './icons/icon__center_off.svg?url'
|
|
90
91
|
import iconCenterOn from './icons/icon__center_on.svg?url'
|
|
@@ -172,7 +173,7 @@ const props = defineProps({
|
|
|
172
173
|
},
|
|
173
174
|
securityLevel: {
|
|
174
175
|
type: String,
|
|
175
|
-
default: '
|
|
176
|
+
default: 'strict'
|
|
176
177
|
},
|
|
177
178
|
enableCopy: {
|
|
178
179
|
type: Boolean,
|
|
@@ -488,8 +489,11 @@ onBeforeUnmount(() => {
|
|
|
488
489
|
document.removeEventListener('keydown', handleDocumentKeydown, true)
|
|
489
490
|
if (copyResetTimer) clearTimeout(copyResetTimer)
|
|
490
491
|
if (isFullscreen.value) exitFullscreenOverlay()
|
|
492
|
+
setFullscreenLayer(rootRef.value, false)
|
|
491
493
|
})
|
|
492
494
|
|
|
495
|
+
watch(isFullscreen, (on) => setFullscreenLayer(rootRef.value, on), { flush: 'sync' })
|
|
496
|
+
|
|
493
497
|
watch(
|
|
494
498
|
() => [decodedSource.value, props.securityLevel, props.isDark],
|
|
495
499
|
() => {
|