@tnotesjs/core 0.1.17 → 0.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tnotesjs/core",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.17.1",
@@ -29,7 +29,7 @@
29
29
  @click="copyCode"
30
30
  :title="copied ? '已复制' : '复制代码'"
31
31
  >
32
- <img v-if="!copied" :src="icon__copy" alt="复制" />
32
+ <img v-if="!copied" :src="icon__clipboard" alt="复制" />
33
33
  <img v-else :src="icon__check" alt="已复制" />
34
34
  </button>
35
35
  <!-- 关闭按钮 -->
@@ -56,7 +56,7 @@
56
56
  <script setup lang="ts">
57
57
  import { ref, watch, onMounted, onUnmounted } from 'vue'
58
58
 
59
- import { icon__copy, icon__check, icon__close, icon__rotate } from '../../assets/icons'
59
+ import { icon__clipboard, icon__check, icon__close, icon__rotate } from '../../assets/icons'
60
60
 
61
61
  const ORIENTATION_KEY = 'code-fullscreen-orientation'
62
62
 
@@ -366,9 +366,4 @@ onUnmounted(() => {
366
366
  .fullscreen-fade-leave-to .code-fullscreen-container {
367
367
  transform: scale(0.95);
368
368
  }
369
-
370
- /* 暗色模式图标适配 */
371
- .dark .action-btn img {
372
- filter: invert(1);
373
- }
374
369
  </style>
@@ -1,40 +1,133 @@
1
- import { onMounted, onUnmounted } from 'vue'
2
- import { createApp } from 'vue'
1
+ import { createApp, onMounted, onUnmounted } from 'vue'
3
2
 
4
3
  import CodeBlockFullscreen from './CodeBlockFullscreen.vue'
4
+ import { icon__check, icon__clipboard, icon__fullscreen } from '../../assets/icons'
5
+
6
+ type CopyState = 'idle' | 'copied' | 'failed'
7
+
8
+ const CODE_BLOCK_SELECTOR = 'div[class*="language-"]'
9
+ const CODE_ACTIONS_CLASS = 'tn-code-actions'
10
+ const COPY_RESET_DELAY = 2000
5
11
 
6
12
  export function useCodeBlockFullscreen() {
7
13
  let fullscreenApp: any = null
8
14
  let fullscreenContainer: HTMLElement | null = null
15
+ const copyResetTimers = new WeakMap<HTMLElement, number>()
9
16
 
10
- function addFullscreenButtons() {
17
+ function addCodeActions() {
11
18
  // 找到所有代码块
12
- const codeBlocks = document.querySelectorAll('div[class*="language-"]')
19
+ const codeBlocks = document.querySelectorAll(CODE_BLOCK_SELECTOR)
13
20
 
14
21
  codeBlocks.forEach((block) => {
15
- // 避免重复添加按钮
16
- if (block.querySelector('.fullscreen-btn')) return
17
-
18
- // 创建全屏按钮
19
- const button = document.createElement('button')
20
- button.className = 'fullscreen-btn'
21
- button.title = '全屏查看代码'
22
- button.innerHTML = `
23
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
24
- <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
25
- </svg>
26
- `
27
-
28
- // 点击事件
29
- button.addEventListener('click', () => {
30
- openFullscreen(block as HTMLElement)
22
+ if (!(block instanceof HTMLElement)) return
23
+ if (!block.querySelector('pre code')) return
24
+ block.querySelectorAll('.fullscreen-btn').forEach((button) => {
25
+ button.remove()
26
+ })
27
+ if (block.querySelector(`.${CODE_ACTIONS_CLASS}`)) return
28
+
29
+ const actions = document.createElement('div')
30
+ actions.className = CODE_ACTIONS_CLASS
31
+ actions.setAttribute('aria-label', '代码块操作')
32
+
33
+ const fullscreenButton = createActionButton({
34
+ className: 'tn-code-action-fullscreen',
35
+ title: '全屏查看代码',
36
+ icon: icon__fullscreen,
37
+ alt: '全屏',
38
+ })
39
+ fullscreenButton.addEventListener('click', () => {
40
+ openFullscreen(block)
31
41
  })
32
42
 
33
- // 添加按钮到代码块
34
- block.appendChild(button)
43
+ const copyButton = createActionButton({
44
+ className: 'tn-code-action-copy',
45
+ title: '复制代码',
46
+ icon: icon__clipboard,
47
+ alt: '复制',
48
+ })
49
+ copyButton.addEventListener('click', () => {
50
+ void copyCode(block, copyButton)
51
+ })
52
+
53
+ actions.append(fullscreenButton, copyButton)
54
+ block.appendChild(actions)
35
55
  })
36
56
  }
37
57
 
58
+ function createActionButton(options: {
59
+ className: string
60
+ title: string
61
+ icon: string
62
+ alt: string
63
+ }) {
64
+ const button = document.createElement('button')
65
+ button.className = `tn-code-action ${options.className}`
66
+ button.type = 'button'
67
+ button.title = options.title
68
+ button.setAttribute('aria-label', options.title)
69
+ button.innerHTML = `<img src="${options.icon}" alt="${options.alt}" />`
70
+
71
+ return button
72
+ }
73
+
74
+ async function copyCode(codeBlock: HTMLElement, button: HTMLElement) {
75
+ const codeElement = codeBlock.querySelector('pre code')
76
+ const text = codeElement?.textContent ?? ''
77
+ if (!text) {
78
+ setCopyState(button, 'failed')
79
+ return
80
+ }
81
+
82
+ try {
83
+ await navigator.clipboard.writeText(text)
84
+ setCopyState(button, 'copied')
85
+ } catch {
86
+ setCopyState(button, 'failed')
87
+ }
88
+ }
89
+
90
+ function setCopyState(button: HTMLElement, state: CopyState) {
91
+ const currentTimer = copyResetTimers.get(button)
92
+ if (currentTimer) {
93
+ window.clearTimeout(currentTimer)
94
+ }
95
+
96
+ button.dataset.copyState = state
97
+ if (button.parentElement) {
98
+ button.parentElement.dataset.copyState = state
99
+ }
100
+ button.title = getCopyTitle(state)
101
+ button.setAttribute('aria-label', getCopyTitle(state))
102
+
103
+ const icon = button.querySelector('img')
104
+ if (icon) {
105
+ icon.setAttribute('src', state === 'copied' ? icon__check : icon__clipboard)
106
+ icon.setAttribute('alt', state === 'copied' ? '已复制' : '复制')
107
+ }
108
+
109
+ const timer = window.setTimeout(() => {
110
+ button.dataset.copyState = 'idle'
111
+ if (button.parentElement) {
112
+ button.parentElement.dataset.copyState = 'idle'
113
+ }
114
+ button.title = getCopyTitle('idle')
115
+ button.setAttribute('aria-label', getCopyTitle('idle'))
116
+ icon?.setAttribute('src', icon__clipboard)
117
+ icon?.setAttribute('alt', '复制')
118
+ copyResetTimers.delete(button)
119
+ }, COPY_RESET_DELAY)
120
+
121
+ copyResetTimers.set(button, timer)
122
+ }
123
+
124
+ function getCopyTitle(state: CopyState): string {
125
+ if (state === 'copied') return '已复制'
126
+ if (state === 'failed') return '复制失败'
127
+
128
+ return '复制代码'
129
+ }
130
+
38
131
  function openFullscreen(codeBlock: HTMLElement) {
39
132
  // 获取代码内容
40
133
  const pre = codeBlock.querySelector('pre')
@@ -88,19 +181,23 @@ export function useCodeBlockFullscreen() {
88
181
 
89
182
  function cleanup() {
90
183
  closeFullscreen()
91
- // 移除所有全屏按钮
92
- document.querySelectorAll('.fullscreen-btn').forEach((btn) => btn.remove())
184
+ document.querySelectorAll(`.${CODE_ACTIONS_CLASS}`).forEach((actions) => {
185
+ actions.remove()
186
+ })
187
+ document.querySelectorAll('.fullscreen-btn').forEach((button) => {
188
+ button.remove()
189
+ })
93
190
  }
94
191
 
95
192
  onMounted(() => {
96
193
  // 初始化
97
194
  setTimeout(() => {
98
- addFullscreenButtons()
195
+ addCodeActions()
99
196
  }, 100)
100
197
 
101
198
  // 监听路由变化
102
199
  const observer = new MutationObserver(() => {
103
- addFullscreenButtons()
200
+ addCodeActions()
104
201
  })
105
202
 
106
203
  observer.observe(document.body, {
@@ -1,9 +1,13 @@
1
- /* 代码块全屏按钮样式 */
1
+ /* 代码块操作按钮样式 */
2
2
  div[class*='language-'] {
3
3
  position: relative;
4
4
  }
5
5
 
6
- .fullscreen-btn {
6
+ div[class*='language-'] button.copy {
7
+ display: none !important;
8
+ }
9
+
10
+ .tn-code-actions {
7
11
  position: absolute;
8
12
  top: 8px;
9
13
  right: 8px;
@@ -11,54 +15,112 @@ div[class*='language-'] {
11
15
  display: flex;
12
16
  align-items: center;
13
17
  justify-content: center;
18
+ gap: 4px;
19
+ padding: 2px;
20
+ background-color: color-mix(in srgb, var(--vp-code-block-bg) 92%, transparent);
21
+ border: .1px solid var(--vp-c-divider);
22
+ border-radius: 7px;
23
+ opacity: 0;
24
+ transition: opacity 0.2s;
25
+ }
26
+
27
+ .tn-code-action {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ justify-content: center;
14
31
  width: 32px;
15
32
  height: 32px;
16
33
  padding: 0;
17
34
  border: none;
18
- background-color: var(--vp-code-block-bg);
35
+ background-color: transparent;
19
36
  color: var(--vp-c-text-2);
20
37
  border-radius: 6px;
21
38
  cursor: pointer;
22
- opacity: 0;
23
39
  transition: all 0.2s;
24
40
  }
25
41
 
26
- div[class*='language-']:hover .fullscreen-btn {
42
+ .tn-code-action img {
43
+ width: 16px;
44
+ height: 16px;
45
+ filter: var(--vp-icon-filter, none);
46
+ }
47
+
48
+ div[class*='language-']:hover .tn-code-actions,
49
+ .tn-code-actions:focus-within,
50
+ .tn-code-actions[data-copy-state='copied'],
51
+ .tn-code-actions[data-copy-state='failed'] {
27
52
  opacity: 1;
28
53
  }
29
54
 
30
- .fullscreen-btn:hover {
55
+ .tn-code-action:hover {
31
56
  background-color: var(--vp-c-default-soft);
32
57
  color: var(--vp-c-brand-1);
33
58
  transform: scale(1.05);
34
59
  }
35
60
 
36
- .fullscreen-btn:active {
61
+ .tn-code-action:active {
37
62
  transform: scale(0.95);
38
63
  }
39
64
 
40
- .fullscreen-btn svg {
41
- width: 16px;
42
- height: 16px;
65
+ .tn-code-action-copy {
66
+ position: relative;
67
+ }
68
+
69
+ .tn-code-action-copy[data-copy-state='copied'] {
70
+ background-color: var(--vp-c-green-soft);
71
+ }
72
+
73
+ .tn-code-action-copy[data-copy-state='failed'] {
74
+ background-color: var(--vp-c-danger-soft);
75
+ }
76
+
77
+ .tn-code-action-copy::after {
78
+ position: absolute;
79
+ top: calc(100% + 6px);
80
+ right: 0;
81
+ padding: 4px 7px;
82
+ color: var(--vp-c-text-1);
83
+ background: var(--vp-c-bg-elv);
84
+ border: 1px solid var(--vp-c-divider);
85
+ border-radius: 6px;
86
+ box-shadow: var(--vp-shadow-2);
87
+ content: attr(title);
88
+ font-size: 12px;
89
+ line-height: 18px;
90
+ opacity: 0;
91
+ pointer-events: none;
92
+ transform: translateY(-2px);
93
+ transition:
94
+ opacity 0.16s ease,
95
+ transform 0.16s ease;
96
+ white-space: nowrap;
43
97
  }
44
98
 
99
+ .tn-code-action-copy[data-copy-state='copied']::after,
100
+ .tn-code-action-copy[data-copy-state='failed']::after {
101
+ opacity: 1;
102
+ transform: translateY(0);
103
+ }
104
+
105
+ /* .dark .tn-code-action-copy img {
106
+ filter: invert(1);
107
+ } */
108
+
45
109
  /* 移动端始终显示按钮 */
46
110
  @media (max-width: 768px) {
47
- .fullscreen-btn {
48
- opacity: 0.8;
111
+ .tn-code-actions {
112
+ /* opacity: 0.8; */
49
113
  top: 6px;
50
114
  right: 6px;
115
+ }
116
+
117
+ .tn-code-action {
51
118
  width: 28px;
52
119
  height: 28px;
53
120
  }
54
121
 
55
- .fullscreen-btn svg {
122
+ .tn-code-action img {
56
123
  width: 14px;
57
124
  height: 14px;
58
125
  }
59
126
  }
60
-
61
- div[class*='language-']:has(.copy) .fullscreen-btn {
62
- /* 代码块已经有复制按钮,需要微调位置 */
63
- right: 54px;
64
- }
@@ -40,8 +40,8 @@
40
40
  <script setup lang="ts">
41
41
  import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
42
42
 
43
- import { icon__next, icon__prev } from '../../assets/icons'
44
43
  import { useSidebarLayout } from './composables/useSidebarLayout'
44
+ import { icon__next, icon__prev } from '../../assets/icons'
45
45
 
46
46
  const {
47
47
  hidden,
@@ -13,8 +13,8 @@
13
13
  <script setup lang="ts">
14
14
  import { onMounted } from 'vue'
15
15
 
16
- import { icon__next, icon__prev } from '../../assets/icons'
17
16
  import { useSidebarLayout } from './composables/useSidebarLayout'
17
+ import { icon__next, icon__prev } from '../../assets/icons'
18
18
 
19
19
  const { hidden, initSidebarLayout, toggleSidebar } = useSidebarLayout()
20
20
 
@@ -5,9 +5,9 @@
5
5
  */
6
6
  import { FileWatcherService, NoteService, ReadmeService } from '../../services'
7
7
 
8
- import type { PluginOption } from 'vite'
9
8
  import type { IncomingMessage, ServerResponse } from 'http'
10
9
  import type { NoteInfo } from '../../types'
10
+ import type { PluginOption } from 'vite'
11
11
 
12
12
  interface JsonResponse {
13
13
  success: boolean
@@ -48,4 +48,12 @@ ul > li.task-list-item > ul {
48
48
  border-top: none;
49
49
  }
50
50
 
51
+ // 代码块外层 div
52
+ .vp-doc div[class*=language-] {
53
+ // 默认值:
54
+ // margin: 16px 24px;
55
+ // 移除外层 margin:
56
+ margin: 0px !important;
57
+ }
58
+
51
59
  /* #endregion */
@@ -156,21 +156,8 @@ h2.collapsible-h2 {
156
156
  /* ===================================== */
157
157
 
158
158
  .collapse-content {
159
- overflow: hidden;
160
- max-height: 10000px;
161
- opacity: 1;
162
- transition:
163
- max-height 0.3s ease,
164
- opacity 0.3s ease,
165
- margin 0.3s ease;
166
-
167
159
  &.collapsed {
168
- max-height: 0 !important;
169
- opacity: 0 !important;
170
- margin-top: 0 !important;
171
- margin-bottom: 0 !important;
172
- padding-top: 0 !important;
173
- padding-bottom: 0 !important;
160
+ display: none !important;
174
161
  }
175
162
 
176
163
  > :first-child {