@tnotesjs/ui 0.4.0 → 0.4.2
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/swiper/hydrate.d.ts +26 -0
- package/dist/swiper/hydrate.js +126 -0
- package/package.json +4 -3
- package/src/components/Footprints/Footprints.vue +4 -0
- package/src/components/ImagePreview/ImagePreview.vue +1 -0
- package/src/components/Mermaid/Mermaid.vue +1 -2
- package/src/components/Mindmap/Mindmap.vue +12 -7
- package/src/components/Mindmap/editor/OutlineView.vue +1 -1
- package/src/components/Mindmap/editor/RichInlineEditor.vue +8 -6
- package/src/components/WordList/RightClickMenu.vue +12 -7
- package/src/components/WordList/WordList.vue +197 -61
- package/src/excalidraw/ExcalidrawSvg.vue +2 -1
- package/src/styles/prose.css +27 -8
- package/src/styles/swiper.css +35 -7
- package/src/styles/tokens.css +20 -4
- package/src/swiper/hydrate.ts +8 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side tab activation for `.tn-swiper` shells emitted by the SSG
|
|
3
|
+
* markdown pipeline (empty `.tn-swiper-tabs`, all slides in the wrapper).
|
|
4
|
+
* Desk also uses this after building slide DOM so both hosts share one path.
|
|
5
|
+
*/
|
|
6
|
+
declare function applySwiperTabsPadding(tabs: HTMLElement, hasNav: boolean): void;
|
|
7
|
+
declare function createSwiperTabNav(handlers: {
|
|
8
|
+
onPrev: () => void;
|
|
9
|
+
onNext: () => void;
|
|
10
|
+
}): {
|
|
11
|
+
prev: HTMLButtonElement;
|
|
12
|
+
line: HTMLSpanElement;
|
|
13
|
+
next: HTMLButtonElement;
|
|
14
|
+
};
|
|
15
|
+
declare function wrapSlideIndex(index: number, length: number, delta: -1 | 1): number;
|
|
16
|
+
declare const SWIPER_EMPTY_TEXT = "empty swiper";
|
|
17
|
+
/** 没有图片幻灯片时的壳。Desk 与站点用同一段结构。 */
|
|
18
|
+
declare function emptySwiperMarkup(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Hydrate every `.tn-swiper` under `root` (or `document`). Idempotent:
|
|
21
|
+
* already-hydrated roots are skipped via `data-tn-swiper-ready`.
|
|
22
|
+
* When `root` itself is a `.tn-swiper`, it is included.
|
|
23
|
+
*/
|
|
24
|
+
declare function hydrateTnSwipers(root?: ParentNode): number;
|
|
25
|
+
|
|
26
|
+
export { SWIPER_EMPTY_TEXT, applySwiperTabsPadding, createSwiperTabNav, emptySwiperMarkup, hydrateTnSwipers, wrapSlideIndex };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/swiper/hydrate.ts
|
|
2
|
+
function applySwiperTabsPadding(tabs, hasNav) {
|
|
3
|
+
tabs.style.padding = hasNav ? "0 0.8rem 0 3rem" : "0 0.8rem";
|
|
4
|
+
}
|
|
5
|
+
function createSwiperTabNav(handlers) {
|
|
6
|
+
const prev = document.createElement("button");
|
|
7
|
+
prev.type = "button";
|
|
8
|
+
prev.className = "tn-tab-nav tn-tab-prev";
|
|
9
|
+
prev.textContent = "<";
|
|
10
|
+
prev.title = "\u4E0A\u4E00\u9875";
|
|
11
|
+
prev.addEventListener("click", (event) => {
|
|
12
|
+
event.preventDefault();
|
|
13
|
+
event.stopPropagation();
|
|
14
|
+
handlers.onPrev();
|
|
15
|
+
});
|
|
16
|
+
const line = document.createElement("span");
|
|
17
|
+
line.className = "tn-tab-nav tab-tab-line";
|
|
18
|
+
line.textContent = "/";
|
|
19
|
+
const next = document.createElement("button");
|
|
20
|
+
next.type = "button";
|
|
21
|
+
next.className = "tn-tab-nav tn-tab-next";
|
|
22
|
+
next.textContent = ">";
|
|
23
|
+
next.title = "\u4E0B\u4E00\u9875";
|
|
24
|
+
next.addEventListener("click", (event) => {
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
event.stopPropagation();
|
|
27
|
+
handlers.onNext();
|
|
28
|
+
});
|
|
29
|
+
return { prev, line, next };
|
|
30
|
+
}
|
|
31
|
+
function wrapSlideIndex(index, length, delta) {
|
|
32
|
+
if (length <= 0) return 0;
|
|
33
|
+
return (index + delta + length) % length;
|
|
34
|
+
}
|
|
35
|
+
var SWIPER_EMPTY_TEXT = "empty swiper";
|
|
36
|
+
function emptySwiperMarkup() {
|
|
37
|
+
return `<div class="tn-swiper is-empty"><div class="swiper-container"><p class="tn-swiper-empty">${SWIPER_EMPTY_TEXT}</p></div></div>`;
|
|
38
|
+
}
|
|
39
|
+
var READY = "tnSwiperReady";
|
|
40
|
+
function hydrateTnSwipers(root = document) {
|
|
41
|
+
const hosts = [];
|
|
42
|
+
if (root instanceof HTMLElement && root.classList.contains("tn-swiper")) {
|
|
43
|
+
hosts.push(root);
|
|
44
|
+
}
|
|
45
|
+
hosts.push(...root.querySelectorAll(".tn-swiper"));
|
|
46
|
+
let count = 0;
|
|
47
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48
|
+
for (const host of hosts) {
|
|
49
|
+
if (seen.has(host)) continue;
|
|
50
|
+
seen.add(host);
|
|
51
|
+
if (host.dataset[READY] === "true") continue;
|
|
52
|
+
if (hydrateOne(host)) {
|
|
53
|
+
host.dataset[READY] = "true";
|
|
54
|
+
count += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return count;
|
|
58
|
+
}
|
|
59
|
+
function hydrateOne(host) {
|
|
60
|
+
if (host.classList.contains("is-empty")) return true;
|
|
61
|
+
const tabs = host.querySelector(".tn-swiper-tabs");
|
|
62
|
+
const wrapper = host.querySelector(".swiper-wrapper");
|
|
63
|
+
if (!wrapper) return false;
|
|
64
|
+
const slides = [...wrapper.querySelectorAll(":scope > .swiper-slide")];
|
|
65
|
+
if (slides.length === 0) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
let tabsEl = tabs;
|
|
69
|
+
if (!tabsEl) {
|
|
70
|
+
tabsEl = document.createElement("div");
|
|
71
|
+
tabsEl.className = "tn-swiper-tabs";
|
|
72
|
+
host.prepend(tabsEl);
|
|
73
|
+
}
|
|
74
|
+
const useTabs = slides.length > 1;
|
|
75
|
+
tabsEl.replaceChildren();
|
|
76
|
+
applySwiperTabsPadding(tabsEl, useTabs);
|
|
77
|
+
const tabButtons = [];
|
|
78
|
+
const activate = (index) => {
|
|
79
|
+
tabButtons.forEach(
|
|
80
|
+
(button, buttonIndex) => button.classList.toggle("active", buttonIndex === index)
|
|
81
|
+
);
|
|
82
|
+
slides.forEach((slide, slideIndex) => {
|
|
83
|
+
const on = slideIndex === index;
|
|
84
|
+
slide.classList.toggle("is-active", on);
|
|
85
|
+
slide.hidden = !on;
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const activeIndex = () => {
|
|
89
|
+
const found = tabButtons.findIndex((button) => button.classList.contains("active"));
|
|
90
|
+
return found >= 0 ? found : 0;
|
|
91
|
+
};
|
|
92
|
+
if (useTabs) {
|
|
93
|
+
const nav = createSwiperTabNav({
|
|
94
|
+
onPrev: () => activate(wrapSlideIndex(activeIndex(), slides.length, -1)),
|
|
95
|
+
onNext: () => activate(wrapSlideIndex(activeIndex(), slides.length, 1))
|
|
96
|
+
});
|
|
97
|
+
tabsEl.append(nav.prev, nav.line);
|
|
98
|
+
slides.forEach((slide, index) => {
|
|
99
|
+
const title = slide.dataset.title?.trim() || slide.querySelector("img")?.getAttribute("alt")?.trim() || "img";
|
|
100
|
+
slide.dataset.title = title;
|
|
101
|
+
const tab = document.createElement("button");
|
|
102
|
+
tab.type = "button";
|
|
103
|
+
tab.className = "tn-tab";
|
|
104
|
+
tab.textContent = title;
|
|
105
|
+
tab.addEventListener("click", () => activate(index));
|
|
106
|
+
tabButtons.push(tab);
|
|
107
|
+
tabsEl.append(tab);
|
|
108
|
+
});
|
|
109
|
+
tabsEl.append(nav.next);
|
|
110
|
+
if (tabsEl.parentElement !== host || host.firstElementChild !== tabsEl) {
|
|
111
|
+
host.prepend(tabsEl);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
tabsEl.remove();
|
|
115
|
+
}
|
|
116
|
+
activate(0);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
SWIPER_EMPTY_TEXT,
|
|
121
|
+
applySwiperTabsPadding,
|
|
122
|
+
createSwiperTabNav,
|
|
123
|
+
emptySwiperMarkup,
|
|
124
|
+
hydrateTnSwipers,
|
|
125
|
+
wrapSlideIndex
|
|
126
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tnotesjs/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Shared Vue UI for TNotes built-in blocks — consumed by the TNotes SSG and Desk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"./styles/swiper.css": "./src/styles/swiper.css",
|
|
34
34
|
"./swiper": {
|
|
35
35
|
"types": "./src/swiper/hydrate.ts",
|
|
36
|
+
"node": "./dist/swiper/hydrate.js",
|
|
36
37
|
"import": "./src/swiper/hydrate.ts",
|
|
37
38
|
"default": "./src/swiper/hydrate.ts"
|
|
38
39
|
},
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
"react-dom": "18.3.1",
|
|
84
85
|
"sass": "^1.90.0",
|
|
85
86
|
"shiki": "4.4.2",
|
|
86
|
-
"@tnotesjs/mindmap-core": "^0.2.
|
|
87
|
+
"@tnotesjs/mindmap-core": "^0.2.4"
|
|
87
88
|
},
|
|
88
89
|
"devDependencies": {
|
|
89
90
|
"@eslint/js": "^10.0.1",
|
|
@@ -126,7 +127,7 @@
|
|
|
126
127
|
],
|
|
127
128
|
"license": "MIT",
|
|
128
129
|
"scripts": {
|
|
129
|
-
"build": "tsup src/code/highlight.ts src/components/Footprints/parse.ts src/components/Mindmap/markdown.ts src/markdown/image.ts --format esm --dts --out-dir dist --clean",
|
|
130
|
+
"build": "tsup src/code/highlight.ts src/components/Footprints/parse.ts src/components/Mindmap/markdown.ts src/markdown/image.ts src/swiper/hydrate.ts --format esm --dts --out-dir dist --clean",
|
|
130
131
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
131
132
|
"test": "vitest run",
|
|
132
133
|
"lint": "eslint . --max-warnings=0",
|
|
@@ -186,6 +186,9 @@ watch(
|
|
|
186
186
|
<style scoped lang="scss">
|
|
187
187
|
.tn-footprints {
|
|
188
188
|
margin: 1rem 0;
|
|
189
|
+
padding: 12px 16px;
|
|
190
|
+
border: 1px solid var(--tn-c-divider);
|
|
191
|
+
border-radius: 8px;
|
|
189
192
|
color: var(--tn-c-text);
|
|
190
193
|
}
|
|
191
194
|
|
|
@@ -312,6 +315,7 @@ watch(
|
|
|
312
315
|
.tn-footprints__modal-img {
|
|
313
316
|
max-width: min(92vw, 960px);
|
|
314
317
|
max-height: 88vh;
|
|
318
|
+
border-radius: 4px;
|
|
315
319
|
object-fit: contain;
|
|
316
320
|
}
|
|
317
321
|
|
|
@@ -502,8 +502,7 @@ watch(
|
|
|
502
502
|
.tn-mermaid {
|
|
503
503
|
position: relative;
|
|
504
504
|
margin: 1rem 0;
|
|
505
|
-
|
|
506
|
-
border: 1px solid transparent;
|
|
505
|
+
border: 1px solid var(--tn-c-divider);
|
|
507
506
|
border-radius: 8px;
|
|
508
507
|
background: var(--tn-c-bg);
|
|
509
508
|
overflow: hidden;
|
|
@@ -1185,11 +1185,13 @@ onBeforeUnmount(() => {
|
|
|
1185
1185
|
}
|
|
1186
1186
|
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.code) {
|
|
1187
1187
|
padding: 0 4px;
|
|
1188
|
+
border: 1px solid var(--tn-c-inline-code-border);
|
|
1188
1189
|
border-radius: 4px;
|
|
1189
|
-
background:
|
|
1190
|
-
color:
|
|
1190
|
+
background: var(--tn-c-inline-code-bg);
|
|
1191
|
+
color: inherit;
|
|
1191
1192
|
font-family: var(--tn-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1192
|
-
font-size:
|
|
1193
|
+
font-size: 1em;
|
|
1194
|
+
font-weight: 400;
|
|
1193
1195
|
}
|
|
1194
1196
|
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.link) {
|
|
1195
1197
|
color: var(--mm-accent);
|
|
@@ -1326,7 +1328,7 @@ onBeforeUnmount(() => {
|
|
|
1326
1328
|
max-width: min(100%, 560px);
|
|
1327
1329
|
max-height: 360px;
|
|
1328
1330
|
margin: 5px 0 12px 25px;
|
|
1329
|
-
border-radius:
|
|
1331
|
+
border-radius: 4px;
|
|
1330
1332
|
}
|
|
1331
1333
|
.mindmap-outline :deep(.is-bold) {
|
|
1332
1334
|
font-weight: 700;
|
|
@@ -1346,11 +1348,14 @@ onBeforeUnmount(() => {
|
|
|
1346
1348
|
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
1347
1349
|
}
|
|
1348
1350
|
.mindmap-outline :deep(.is-code) {
|
|
1349
|
-
padding:
|
|
1351
|
+
padding: 0 4px;
|
|
1352
|
+
border: 1px solid var(--tn-c-inline-code-border);
|
|
1350
1353
|
border-radius: 4px;
|
|
1351
|
-
background: var(--tn-c-bg
|
|
1352
|
-
color:
|
|
1354
|
+
background: var(--tn-c-inline-code-bg);
|
|
1355
|
+
color: inherit;
|
|
1353
1356
|
font-family: var(--tn-font-mono);
|
|
1357
|
+
font-size: 1em;
|
|
1358
|
+
font-weight: 400;
|
|
1354
1359
|
}
|
|
1355
1360
|
.mindmap-outline :deep(.is-link) {
|
|
1356
1361
|
color: var(--tn-c-brand);
|
|
@@ -2345,7 +2345,7 @@ function onDragUp() {
|
|
|
2345
2345
|
position: relative;
|
|
2346
2346
|
max-width: 100%;
|
|
2347
2347
|
line-height: 0;
|
|
2348
|
-
border-radius:
|
|
2348
|
+
border-radius: 4px;
|
|
2349
2349
|
overflow: hidden;
|
|
2350
2350
|
background: color-mix(in srgb, var(--mm-text-dim) 12%, transparent);
|
|
2351
2351
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--mm-text-dim) 25%, transparent);
|
|
@@ -419,12 +419,14 @@ onBeforeUnmount(emitCommit)
|
|
|
419
419
|
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
420
420
|
}
|
|
421
421
|
.rich-inline-editor :deep(.inline-run.code) {
|
|
422
|
-
padding:
|
|
423
|
-
border
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
font-
|
|
422
|
+
padding: 0 4px;
|
|
423
|
+
border: 1px solid var(--tn-c-inline-code-border, #e7e9e8);
|
|
424
|
+
border-radius: 4px;
|
|
425
|
+
background: var(--tn-c-inline-code-bg, rgba(0, 0, 0, 0.06));
|
|
426
|
+
color: inherit;
|
|
427
|
+
font-family: var(--tn-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
428
|
+
font-size: 1em;
|
|
429
|
+
font-weight: 400;
|
|
428
430
|
box-decoration-break: clone;
|
|
429
431
|
-webkit-box-decoration-break: clone;
|
|
430
432
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
v-if="show"
|
|
4
|
+
class="rightClickMenu"
|
|
5
|
+
:style="{ left: x + 'px', top: y + 'px' }"
|
|
6
|
+
@mousedown.stop
|
|
7
|
+
>
|
|
3
8
|
<div v-if="showPin" class="menuItem" @click="handlePin">📌 Pin</div>
|
|
4
9
|
<div class="menuItem" @click="(e) => handlePronounce(e, 'en-GB')">📢 Pronounce(英)</div>
|
|
5
10
|
<div class="menuItem" @click="(e) => handlePronounce(e, 'en-US')">📢 Pronounce(美)</div>
|
|
@@ -61,12 +66,12 @@ const handleReset = () => {
|
|
|
61
66
|
.rightClickMenu {
|
|
62
67
|
position: fixed;
|
|
63
68
|
z-index: 99999;
|
|
64
|
-
background:
|
|
65
|
-
border: 1px solid
|
|
66
|
-
border-radius:
|
|
67
|
-
box-shadow:
|
|
69
|
+
background: var(--tn-c-bg-elv, var(--tn-c-bg));
|
|
70
|
+
border: 1px solid var(--tn-c-divider);
|
|
71
|
+
border-radius: 8px;
|
|
72
|
+
box-shadow: var(--tn-shadow-2);
|
|
68
73
|
font-size: 13px;
|
|
69
|
-
color:
|
|
74
|
+
color: var(--tn-c-text);
|
|
70
75
|
cursor: pointer;
|
|
71
76
|
user-select: none;
|
|
72
77
|
}
|
|
@@ -75,7 +80,7 @@ const handleReset = () => {
|
|
|
75
80
|
padding: 8px 16px;
|
|
76
81
|
|
|
77
82
|
&:hover {
|
|
78
|
-
background
|
|
83
|
+
background: var(--tn-c-hover);
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
</style>
|
|
@@ -48,6 +48,7 @@ const sortedWords = computed(() => {
|
|
|
48
48
|
if (!props.needSort) return unique
|
|
49
49
|
return unique.sort((a, b) => a.toLowerCase().charCodeAt(0) - b.toLowerCase().charCodeAt(0))
|
|
50
50
|
})
|
|
51
|
+
const islandWords = computed(() => encodeURIComponent(JSON.stringify(sortedWords.value)))
|
|
51
52
|
const checkedStates = ref({})
|
|
52
53
|
|
|
53
54
|
const updateCheckedState = (word, isChecked) => {
|
|
@@ -84,6 +85,7 @@ const cardX = ref(0)
|
|
|
84
85
|
const cardY = ref(0)
|
|
85
86
|
const cardContent = ref('')
|
|
86
87
|
const wordCache = ref({})
|
|
88
|
+
const wordMarkdown = ref({})
|
|
87
89
|
|
|
88
90
|
// 加载失败的词汇(也就是词库中不存在的词汇)
|
|
89
91
|
const failedWords = ref({})
|
|
@@ -112,6 +114,66 @@ let currentWordForContextMenu = null
|
|
|
112
114
|
// 防抖计时器
|
|
113
115
|
let hoverTimer = null
|
|
114
116
|
|
|
117
|
+
function wordSlug(word) {
|
|
118
|
+
return encodeURIComponent(word.toLowerCase().replaceAll(/\s/g, '_'))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function wordGithubUrl(word) {
|
|
122
|
+
return `${props.wordsBaseUrl}${wordSlug(word)}.md`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function ensureWord(word) {
|
|
126
|
+
if (wordCache.value[word]) return
|
|
127
|
+
const url = `${props.wordsRawBaseUrl}${wordSlug(word)}.md`
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetch(url)
|
|
130
|
+
if (res.ok) {
|
|
131
|
+
const text = await res.text()
|
|
132
|
+
wordMarkdown.value[word] = text
|
|
133
|
+
wordCache.value[word] = marked.parse(text)
|
|
134
|
+
} else {
|
|
135
|
+
wordCache.value[word] = `<em>无法加载单词内容</em>`
|
|
136
|
+
failedWords.value[word] = true
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error(err)
|
|
140
|
+
wordCache.value[word] = `<em>加载失败</em>`
|
|
141
|
+
failedWords.value[word] = true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const copiedCardId = ref(null)
|
|
146
|
+
let copiedTimer = null
|
|
147
|
+
|
|
148
|
+
async function copyCardMarkdown(card, event) {
|
|
149
|
+
event.stopPropagation()
|
|
150
|
+
const markdown = wordMarkdown.value[card.word]
|
|
151
|
+
if (!markdown) return
|
|
152
|
+
try {
|
|
153
|
+
await navigator.clipboard.writeText(markdown)
|
|
154
|
+
} catch {
|
|
155
|
+
const field = document.createElement('textarea')
|
|
156
|
+
field.value = markdown
|
|
157
|
+
field.setAttribute('readonly', '')
|
|
158
|
+
field.style.position = 'fixed'
|
|
159
|
+
field.style.left = '-9999px'
|
|
160
|
+
document.body.append(field)
|
|
161
|
+
field.select()
|
|
162
|
+
document.execCommand('copy')
|
|
163
|
+
field.remove()
|
|
164
|
+
}
|
|
165
|
+
copiedCardId.value = card.id
|
|
166
|
+
clearTimeout(copiedTimer)
|
|
167
|
+
copiedTimer = setTimeout(() => {
|
|
168
|
+
copiedCardId.value = null
|
|
169
|
+
}, 1500)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function openCardOnGithub(card, event) {
|
|
173
|
+
event.stopPropagation()
|
|
174
|
+
window.open(wordGithubUrl(card.word), '_blank', 'noopener')
|
|
175
|
+
}
|
|
176
|
+
|
|
115
177
|
/**
|
|
116
178
|
* 显示单词卡片
|
|
117
179
|
*/
|
|
@@ -128,29 +190,8 @@ const showWordCard = async (e, word) => {
|
|
|
128
190
|
cardY.value = clientY + 10
|
|
129
191
|
showCard.value = true
|
|
130
192
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
resolve()
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const url = `${props.wordsRawBaseUrl}${encodeURIComponent(
|
|
138
|
-
word.toLowerCase().replaceAll(/\s/g, '_')
|
|
139
|
-
)}.md`
|
|
140
|
-
try {
|
|
141
|
-
const res = await fetch(url)
|
|
142
|
-
if (res.ok) {
|
|
143
|
-
let text = await res.text()
|
|
144
|
-
text = marked.parse(text)
|
|
145
|
-
wordCache.value[word] = text
|
|
146
|
-
cardContent.value = text
|
|
147
|
-
} else {
|
|
148
|
-
cardContent.value = `<em>无法加载单词内容</em>`
|
|
149
|
-
}
|
|
150
|
-
} catch (err) {
|
|
151
|
-
console.error(err)
|
|
152
|
-
cardContent.value = `<em>加载失败</em>`
|
|
153
|
-
}
|
|
193
|
+
await ensureWord(word)
|
|
194
|
+
cardContent.value = wordCache.value[word] || `<em>加载失败</em>`
|
|
154
195
|
resolve()
|
|
155
196
|
}, 300)
|
|
156
197
|
})
|
|
@@ -208,28 +249,8 @@ const preloadWords = async () => {
|
|
|
208
249
|
for (let i = 0; i < wordsToPreload.length; i++) {
|
|
209
250
|
const word = wordsToPreload[i]
|
|
210
251
|
|
|
211
|
-
// 如果已经缓存过,跳过
|
|
212
252
|
if (wordCache.value[word]) continue
|
|
213
|
-
|
|
214
|
-
const url = `${props.wordsRawBaseUrl}${encodeURIComponent(
|
|
215
|
-
word.toLowerCase().replaceAll(/\s/g, '_')
|
|
216
|
-
)}.md`
|
|
217
|
-
try {
|
|
218
|
-
const res = await fetch(url)
|
|
219
|
-
if (res.ok) {
|
|
220
|
-
let text = await res.text()
|
|
221
|
-
text = marked.parse(text)
|
|
222
|
-
wordCache.value[word] = text
|
|
223
|
-
console.log(`✅ 预加载完成: ${word}`)
|
|
224
|
-
} else {
|
|
225
|
-
wordCache.value[word] = `<em>无法加载单词内容</em>`
|
|
226
|
-
failedWords.value[word] = true
|
|
227
|
-
}
|
|
228
|
-
} catch (err) {
|
|
229
|
-
console.error(`❌ 加载失败: ${word}`, err)
|
|
230
|
-
wordCache.value[word] = `<em>加载失败</em>`
|
|
231
|
-
failedWords.value[word] = true
|
|
232
|
-
}
|
|
253
|
+
await ensureWord(word)
|
|
233
254
|
|
|
234
255
|
// 可选:加个延迟避免并发请求过多
|
|
235
256
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
@@ -446,6 +467,7 @@ onMounted(() => {
|
|
|
446
467
|
*/
|
|
447
468
|
onUnmounted(() => {
|
|
448
469
|
clearTimeout(hoverTimer)
|
|
470
|
+
clearTimeout(copiedTimer)
|
|
449
471
|
if (typeof document !== 'undefined') {
|
|
450
472
|
document.removeEventListener('mousemove', onDragging)
|
|
451
473
|
document.removeEventListener('mouseup', stopDrag)
|
|
@@ -455,7 +477,12 @@ onUnmounted(() => {
|
|
|
455
477
|
</script>
|
|
456
478
|
|
|
457
479
|
<template>
|
|
458
|
-
<div
|
|
480
|
+
<div
|
|
481
|
+
class="tn-word-list"
|
|
482
|
+
data-tn-island="word-list"
|
|
483
|
+
:data-words="islandWords"
|
|
484
|
+
:data-need-sort="needSort ? 'true' : 'false'"
|
|
485
|
+
>
|
|
459
486
|
<ol>
|
|
460
487
|
<li
|
|
461
488
|
v-for="(word, index) in sortedWords"
|
|
@@ -515,10 +542,60 @@ onUnmounted(() => {
|
|
|
515
542
|
@mousedown="(e) => startDrag(card, e)"
|
|
516
543
|
@click="bringToFront(card)"
|
|
517
544
|
>
|
|
545
|
+
<div class="wordCardTools">
|
|
546
|
+
<button
|
|
547
|
+
type="button"
|
|
548
|
+
class="cardTool"
|
|
549
|
+
aria-label="在 GitHub 打开"
|
|
550
|
+
title="在 GitHub 打开"
|
|
551
|
+
@mousedown.stop
|
|
552
|
+
@click.stop="openCardOnGithub(card, $event)"
|
|
553
|
+
>
|
|
554
|
+
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
|
555
|
+
<path
|
|
556
|
+
fill="currentColor"
|
|
557
|
+
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"
|
|
558
|
+
/>
|
|
559
|
+
</svg>
|
|
560
|
+
</button>
|
|
561
|
+
<button
|
|
562
|
+
type="button"
|
|
563
|
+
class="cardTool"
|
|
564
|
+
:aria-label="copiedCardId === card.id ? '已复制' : '复制 Markdown'"
|
|
565
|
+
:title="copiedCardId === card.id ? '已复制' : '复制 Markdown'"
|
|
566
|
+
@mousedown.stop
|
|
567
|
+
@click.stop="copyCardMarkdown(card, $event)"
|
|
568
|
+
>
|
|
569
|
+
<svg
|
|
570
|
+
v-if="copiedCardId !== card.id"
|
|
571
|
+
viewBox="0 0 24 24"
|
|
572
|
+
width="16"
|
|
573
|
+
height="16"
|
|
574
|
+
aria-hidden="true"
|
|
575
|
+
>
|
|
576
|
+
<path
|
|
577
|
+
fill="none"
|
|
578
|
+
stroke="currentColor"
|
|
579
|
+
stroke-width="1.8"
|
|
580
|
+
d="M9 9h10v10H9zM5 15V5h10"
|
|
581
|
+
/>
|
|
582
|
+
</svg>
|
|
583
|
+
<svg v-else viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
|
584
|
+
<path
|
|
585
|
+
fill="none"
|
|
586
|
+
stroke="currentColor"
|
|
587
|
+
stroke-width="1.8"
|
|
588
|
+
stroke-linecap="round"
|
|
589
|
+
stroke-linejoin="round"
|
|
590
|
+
d="m5 12 5 5L20 7"
|
|
591
|
+
/>
|
|
592
|
+
</svg>
|
|
593
|
+
</button>
|
|
594
|
+
</div>
|
|
518
595
|
<div class="wordCardContentWrapper">
|
|
519
596
|
<div class="wordCardContent" v-html="card.content"></div>
|
|
520
597
|
</div>
|
|
521
|
-
<button class="closeBtn" @click.stop="removeCard(card.id)">✖</button>
|
|
598
|
+
<button class="closeBtn" @mousedown.stop @click.stop="removeCard(card.id)">✖</button>
|
|
522
599
|
<div class="resizeHandle" @mousedown.stop="startResize(card, $event)"></div>
|
|
523
600
|
</div>
|
|
524
601
|
</template>
|
|
@@ -549,11 +626,41 @@ onUnmounted(() => {
|
|
|
549
626
|
|
|
550
627
|
<style scoped lang="scss">
|
|
551
628
|
.tn-word-list {
|
|
629
|
+
padding: 12px 16px;
|
|
630
|
+
border: 1px solid var(--tn-c-divider);
|
|
631
|
+
border-radius: 8px;
|
|
632
|
+
|
|
552
633
|
// Checkbox 样式
|
|
553
634
|
input[type='checkbox'] {
|
|
554
|
-
|
|
555
|
-
|
|
635
|
+
appearance: none;
|
|
636
|
+
-webkit-appearance: none;
|
|
637
|
+
position: relative;
|
|
638
|
+
box-sizing: border-box;
|
|
639
|
+
flex: none;
|
|
640
|
+
width: 16px;
|
|
641
|
+
height: 16px;
|
|
642
|
+
margin: 0 8px;
|
|
643
|
+
border: 1px solid var(--tn-c-task-check-border);
|
|
644
|
+
border-radius: 4px;
|
|
645
|
+
background: var(--tn-c-task-check-bg);
|
|
556
646
|
cursor: pointer;
|
|
647
|
+
|
|
648
|
+
&:checked {
|
|
649
|
+
border-color: var(--tn-c-task-check);
|
|
650
|
+
background: var(--tn-c-task-check);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
&:checked::after {
|
|
654
|
+
content: '';
|
|
655
|
+
position: absolute;
|
|
656
|
+
left: 4.5px;
|
|
657
|
+
top: 1px;
|
|
658
|
+
width: 4px;
|
|
659
|
+
height: 9px;
|
|
660
|
+
border: solid #fff;
|
|
661
|
+
border-width: 0 2px 2px 0;
|
|
662
|
+
transform: rotate(45deg);
|
|
663
|
+
}
|
|
557
664
|
}
|
|
558
665
|
|
|
559
666
|
// 链接样式
|
|
@@ -577,8 +684,9 @@ onUnmounted(() => {
|
|
|
577
684
|
|
|
578
685
|
// 单词列表样式
|
|
579
686
|
ol {
|
|
580
|
-
list-style
|
|
581
|
-
|
|
687
|
+
list-style: none;
|
|
688
|
+
margin: 0;
|
|
689
|
+
padding-left: 0;
|
|
582
690
|
|
|
583
691
|
li {
|
|
584
692
|
display: flex;
|
|
@@ -599,13 +707,12 @@ onUnmounted(() => {
|
|
|
599
707
|
}
|
|
600
708
|
}
|
|
601
709
|
|
|
602
|
-
// 单词卡片样式(🌑 暗色悬浮卡片)
|
|
603
710
|
.wordCard {
|
|
604
711
|
position: fixed;
|
|
605
712
|
z-index: 9999;
|
|
606
|
-
background:
|
|
607
|
-
border: 1px solid
|
|
608
|
-
box-shadow:
|
|
713
|
+
background: var(--tn-c-bg-elv, var(--tn-c-bg));
|
|
714
|
+
border: 1px solid var(--tn-c-divider);
|
|
715
|
+
box-shadow: var(--tn-shadow-2);
|
|
609
716
|
padding: 12px 16px;
|
|
610
717
|
max-width: 600px;
|
|
611
718
|
min-width: 200px;
|
|
@@ -613,12 +720,11 @@ onUnmounted(() => {
|
|
|
613
720
|
font-size: 14px;
|
|
614
721
|
line-height: 1.4;
|
|
615
722
|
border-radius: 8px;
|
|
616
|
-
color:
|
|
723
|
+
color: var(--tn-c-text);
|
|
617
724
|
pointer-events: auto;
|
|
618
|
-
font-family: sans-serif;
|
|
725
|
+
font-family: var(--tn-font-sans, sans-serif);
|
|
619
726
|
cursor: move;
|
|
620
727
|
|
|
621
|
-
// 关闭按钮
|
|
622
728
|
.closeBtn {
|
|
623
729
|
position: absolute;
|
|
624
730
|
right: 5px;
|
|
@@ -627,19 +733,49 @@ onUnmounted(() => {
|
|
|
627
733
|
border: none;
|
|
628
734
|
font-size: 16px;
|
|
629
735
|
cursor: pointer;
|
|
630
|
-
color:
|
|
736
|
+
color: var(--tn-c-text-2);
|
|
631
737
|
|
|
632
738
|
&:hover {
|
|
633
|
-
color:
|
|
739
|
+
color: var(--tn-c-text);
|
|
634
740
|
}
|
|
635
741
|
}
|
|
636
742
|
}
|
|
637
743
|
|
|
638
744
|
// 卡片内容包裹器
|
|
745
|
+
.wordCardTools {
|
|
746
|
+
position: absolute;
|
|
747
|
+
top: 6px;
|
|
748
|
+
left: 8px;
|
|
749
|
+
z-index: 2;
|
|
750
|
+
display: flex;
|
|
751
|
+
gap: 2px;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.cardTool {
|
|
755
|
+
display: inline-flex;
|
|
756
|
+
align-items: center;
|
|
757
|
+
justify-content: center;
|
|
758
|
+
width: 24px;
|
|
759
|
+
height: 24px;
|
|
760
|
+
padding: 0;
|
|
761
|
+
border: 0;
|
|
762
|
+
border-radius: 4px;
|
|
763
|
+
background: transparent;
|
|
764
|
+
color: var(--tn-c-text-2);
|
|
765
|
+
cursor: pointer;
|
|
766
|
+
|
|
767
|
+
&:hover {
|
|
768
|
+
background: var(--tn-c-hover);
|
|
769
|
+
color: var(--tn-c-text);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
639
773
|
.wordCardContentWrapper {
|
|
640
774
|
width: 100%;
|
|
641
775
|
height: 100%;
|
|
642
776
|
overflow: auto;
|
|
777
|
+
box-sizing: border-box;
|
|
778
|
+
padding-top: 22px;
|
|
643
779
|
}
|
|
644
780
|
|
|
645
781
|
// 卡片内容样式
|
|
@@ -657,13 +793,13 @@ onUnmounted(() => {
|
|
|
657
793
|
bottom: 0;
|
|
658
794
|
width: 12px;
|
|
659
795
|
height: 12px;
|
|
660
|
-
background-color:
|
|
796
|
+
background-color: var(--tn-c-text-2);
|
|
661
797
|
cursor: nwse-resize;
|
|
662
798
|
z-index: 2;
|
|
663
799
|
border-radius: 50%;
|
|
664
800
|
|
|
665
801
|
&:hover {
|
|
666
|
-
background-color:
|
|
802
|
+
background-color: var(--tn-c-text);
|
|
667
803
|
}
|
|
668
804
|
}
|
|
669
805
|
</style>
|
|
@@ -105,12 +105,13 @@ defineExpose({ state, errorReason })
|
|
|
105
105
|
align-items: center;
|
|
106
106
|
justify-content: center;
|
|
107
107
|
overflow: hidden;
|
|
108
|
-
border-radius:
|
|
108
|
+
border-radius: 4px;
|
|
109
109
|
background: var(--tn-excalidraw-bg, transparent);
|
|
110
110
|
}
|
|
111
111
|
.tn-excalidraw-view__image {
|
|
112
112
|
max-width: 100%;
|
|
113
113
|
max-height: 100%;
|
|
114
|
+
border-radius: 4px;
|
|
114
115
|
object-fit: contain;
|
|
115
116
|
}
|
|
116
117
|
.tn-excalidraw-view__placeholder {
|
package/src/styles/prose.css
CHANGED
|
@@ -47,15 +47,15 @@
|
|
|
47
47
|
line-height: var(--tn-heading-6-line);
|
|
48
48
|
}
|
|
49
49
|
:is(.tn-prose, .vp-doc) :where(a) {
|
|
50
|
-
color: var(--tn-c-
|
|
50
|
+
color: var(--tn-c-link);
|
|
51
51
|
text-decoration: none;
|
|
52
|
-
font-weight:
|
|
52
|
+
font-weight: 400;
|
|
53
53
|
overflow-wrap: anywhere;
|
|
54
54
|
word-break: break-word;
|
|
55
55
|
}
|
|
56
56
|
:is(.tn-prose, .vp-doc) :where(a:hover) {
|
|
57
|
-
color: var(--tn-c-
|
|
58
|
-
text-decoration:
|
|
57
|
+
color: var(--tn-c-link);
|
|
58
|
+
text-decoration: none;
|
|
59
59
|
}
|
|
60
60
|
:is(.tn-prose, .vp-doc) :where(ul, ol) {
|
|
61
61
|
padding-left: 1.5rem;
|
|
@@ -106,11 +106,15 @@
|
|
|
106
106
|
background-size: 12px 12px;
|
|
107
107
|
}
|
|
108
108
|
:is(.tn-prose, .vp-doc) :where(:not(pre) > code) {
|
|
109
|
-
color:
|
|
110
|
-
background: var(--tn-c-
|
|
109
|
+
color: inherit;
|
|
110
|
+
background: var(--tn-c-inline-code-bg);
|
|
111
|
+
border: 1px solid var(--tn-c-inline-code-border);
|
|
111
112
|
border-radius: 4px;
|
|
112
|
-
|
|
113
|
-
padding:
|
|
113
|
+
margin: 1px 3px;
|
|
114
|
+
padding: 0 4px;
|
|
115
|
+
font-family: var(--tn-font-mono);
|
|
116
|
+
font-size: 1em;
|
|
117
|
+
font-weight: 400;
|
|
114
118
|
}
|
|
115
119
|
:is(.tn-prose, .vp-doc) :where(mark) {
|
|
116
120
|
color: inherit;
|
|
@@ -161,6 +165,13 @@
|
|
|
161
165
|
max-width: 100%;
|
|
162
166
|
height: auto;
|
|
163
167
|
}
|
|
168
|
+
/* 笔记里的图片默认 4px 圆角。Desk 和静态站共用;按钮里的图标保持直角。 */
|
|
169
|
+
:is(.tn-prose, .vp-doc, .live-editor) :where(img) {
|
|
170
|
+
border-radius: 4px;
|
|
171
|
+
}
|
|
172
|
+
:is(.tn-prose, .vp-doc, .live-editor) button img {
|
|
173
|
+
border-radius: 0;
|
|
174
|
+
}
|
|
164
175
|
:is(.tn-prose, .vp-doc) :where(figure.tn-image) {
|
|
165
176
|
display: block;
|
|
166
177
|
width: fit-content;
|
|
@@ -180,6 +191,14 @@
|
|
|
180
191
|
width: 100%;
|
|
181
192
|
height: auto;
|
|
182
193
|
}
|
|
194
|
+
|
|
195
|
+
/* 正文图片边界。Desk 可视化图和静态站图共用;Swiper 的边画在 .tn-swiper 上。 */
|
|
196
|
+
:is(.tn-prose, .vp-doc) :where(figure.tn-image > img),
|
|
197
|
+
.cm-lp-image-frame > img,
|
|
198
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block img) {
|
|
199
|
+
border: 1px solid var(--tn-c-media-edge);
|
|
200
|
+
border-radius: 4px;
|
|
201
|
+
}
|
|
183
202
|
:is(.tn-prose, .vp-doc) :where(figure.tn-image > figcaption),
|
|
184
203
|
:is(.tn-prose, .vp-doc) :where(figcaption) {
|
|
185
204
|
display: block;
|
package/src/styles/swiper.css
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
.tn-swiper {
|
|
4
4
|
margin: 1rem 0;
|
|
5
|
-
overflow:
|
|
6
|
-
border
|
|
7
|
-
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
border: 1px solid var(--tn-c-media-edge);
|
|
7
|
+
border-radius: 4px;
|
|
8
|
+
box-shadow: none;
|
|
8
9
|
background: transparent;
|
|
9
10
|
padding: 0;
|
|
10
|
-
border: none;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
.tn-swiper:hover {
|
|
14
|
-
box-shadow:
|
|
14
|
+
box-shadow: none;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
.tn-swiper-tabs {
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
overflow-x: auto;
|
|
23
23
|
overflow-y: hidden;
|
|
24
24
|
border-bottom: 1px solid var(--tn-c-divider);
|
|
25
|
-
border-radius: 8px 8px 0 0;
|
|
26
25
|
background: var(--tn-c-bg-soft);
|
|
27
26
|
box-shadow: inset 0 -1px var(--tn-c-divider);
|
|
28
27
|
}
|
|
@@ -125,7 +124,6 @@
|
|
|
125
124
|
width: 100%;
|
|
126
125
|
overflow: hidden;
|
|
127
126
|
margin: 0;
|
|
128
|
-
border-radius: 0 0 8px 8px;
|
|
129
127
|
background: var(--tn-c-bg);
|
|
130
128
|
}
|
|
131
129
|
|
|
@@ -147,5 +145,35 @@
|
|
|
147
145
|
max-width: 100%;
|
|
148
146
|
height: auto;
|
|
149
147
|
margin: 0 auto;
|
|
148
|
+
border: 0;
|
|
149
|
+
border-radius: 4px;
|
|
150
150
|
object-fit: contain;
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
.tn-swiper .swiper-slide.is-align-left img {
|
|
154
|
+
margin-left: 0;
|
|
155
|
+
margin-right: auto;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.tn-swiper .swiper-slide.is-align-right img {
|
|
159
|
+
margin-left: auto;
|
|
160
|
+
margin-right: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.tn-swiper.is-empty .swiper-container {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
min-height: 180px;
|
|
168
|
+
border: 0;
|
|
169
|
+
border-radius: 0;
|
|
170
|
+
background: var(--tn-c-bg-soft);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.tn-swiper-empty {
|
|
174
|
+
margin: 0;
|
|
175
|
+
color: var(--tn-c-text-2);
|
|
176
|
+
font-size: 14px;
|
|
177
|
+
line-height: 1.5;
|
|
178
|
+
text-align: center;
|
|
179
|
+
}
|
package/src/styles/tokens.css
CHANGED
|
@@ -12,9 +12,18 @@
|
|
|
12
12
|
--tn-c-bg-input: #f6f6f7;
|
|
13
13
|
--tn-c-divider: #e2e2e3;
|
|
14
14
|
--tn-c-divider-strong: #c2c2c4;
|
|
15
|
+
/* 正文图片和 Swiper 外框。浅色里分割线就能和白底分开。 */
|
|
16
|
+
--tn-c-media-edge: var(--tn-c-divider);
|
|
15
17
|
--tn-c-hover: rgba(142, 150, 170, 0.14);
|
|
16
18
|
--tn-c-highlight: rgba(100, 108, 255, 0.16);
|
|
17
19
|
--tn-c-brand-soft: var(--tn-c-highlight);
|
|
20
|
+
/* 阅读选区。语雀正文 ::selection:rgba(27, 162, 227, 0.2),字色不改。 */
|
|
21
|
+
--tn-c-selection: rgba(27, 162, 227, 0.2);
|
|
22
|
+
/* 正文链接。语雀看得见的字色是 #3B82CE,悬停不变,没有底。 */
|
|
23
|
+
--tn-c-link: #3b82ce;
|
|
24
|
+
/* 行内代码。语雀 ne-code-content:黑 6% 底,浅边 #e7e9e8,字色跟正文。 */
|
|
25
|
+
--tn-c-inline-code-bg: rgba(0, 0, 0, 0.06);
|
|
26
|
+
--tn-c-inline-code-border: #e7e9e8;
|
|
18
27
|
--tn-c-success: #18794e;
|
|
19
28
|
--tn-c-warning: #915930;
|
|
20
29
|
--tn-c-danger: #b8272c;
|
|
@@ -30,11 +39,11 @@
|
|
|
30
39
|
--tn-c-task-check-bg: #ffffff;
|
|
31
40
|
--tn-shadow-2: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
32
41
|
--tn-font-sans:
|
|
33
|
-
'
|
|
34
|
-
'Segoe UI Emoji', 'Segoe UI
|
|
42
|
+
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial,
|
|
43
|
+
sans-serif, 'Segoe UI', 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji';
|
|
35
44
|
--tn-font-mono: 'SFMono-Regular', 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
|
|
36
|
-
--tn-prose-size:
|
|
37
|
-
--tn-prose-line-height:
|
|
45
|
+
--tn-prose-size: 15px;
|
|
46
|
+
--tn-prose-line-height: 26.1px;
|
|
38
47
|
--tn-heading-weight: 600;
|
|
39
48
|
--tn-heading-1-size: 32px;
|
|
40
49
|
--tn-heading-1-line: 40px;
|
|
@@ -63,7 +72,14 @@
|
|
|
63
72
|
--tn-c-bg-input: #161618;
|
|
64
73
|
--tn-c-divider: #2e2e32;
|
|
65
74
|
--tn-c-divider-strong: #3c3f44;
|
|
75
|
+
/* 暗色分割线贴近编辑区,掺文字色后白底图和深色图都能看见边。 */
|
|
76
|
+
--tn-c-media-edge: color-mix(in srgb, var(--tn-c-text-2) 42%, var(--tn-c-bg));
|
|
66
77
|
--tn-c-hover: rgba(101, 117, 133, 0.16);
|
|
78
|
+
/* 同一青色。暗底上 0.2 太淡,提高到 0.35 才看得清。 */
|
|
79
|
+
--tn-c-selection: rgba(27, 162, 227, 0.35);
|
|
80
|
+
--tn-c-link: #8ebef5;
|
|
81
|
+
--tn-c-inline-code-bg: rgba(255, 255, 255, 0.08);
|
|
82
|
+
--tn-c-inline-code-border: rgba(255, 255, 255, 0.12);
|
|
67
83
|
--tn-c-success: #3dd68c;
|
|
68
84
|
--tn-c-warning: #f9b44e;
|
|
69
85
|
--tn-c-danger: #f66f81;
|
package/src/swiper/hydrate.ts
CHANGED
|
@@ -47,6 +47,13 @@ export function wrapSlideIndex(index: number, length: number, delta: -1 | 1): nu
|
|
|
47
47
|
return (index + delta + length) % length
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export const SWIPER_EMPTY_TEXT = 'empty swiper'
|
|
51
|
+
|
|
52
|
+
/** 没有图片幻灯片时的壳。Desk 与站点用同一段结构。 */
|
|
53
|
+
export function emptySwiperMarkup(): string {
|
|
54
|
+
return `<div class="tn-swiper is-empty"><div class="swiper-container"><p class="tn-swiper-empty">${SWIPER_EMPTY_TEXT}</p></div></div>`
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
const READY = 'tnSwiperReady'
|
|
51
58
|
|
|
52
59
|
/**
|
|
@@ -76,6 +83,7 @@ export function hydrateTnSwipers(root: ParentNode = document): number {
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
function hydrateOne(host: HTMLElement): boolean {
|
|
86
|
+
if (host.classList.contains('is-empty')) return true
|
|
79
87
|
const tabs = host.querySelector<HTMLElement>('.tn-swiper-tabs')
|
|
80
88
|
const wrapper = host.querySelector<HTMLElement>('.swiper-wrapper')
|
|
81
89
|
if (!wrapper) return false
|