@tnotesjs/ui 0.1.5 → 0.2.0

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.
Files changed (67) hide show
  1. package/README.md +30 -97
  2. package/dist/code/highlight.js +6 -15
  3. package/dist/components/Footprints/parse.js +1 -3
  4. package/dist/components/Mindmap/markdown.d.ts +1 -7
  5. package/dist/components/Mindmap/markdown.js +1 -15
  6. package/package.json +24 -8
  7. package/src/browser/clipboard.ts +16 -19
  8. package/src/browser/theme.ts +10 -15
  9. package/src/code/highlight.test.ts +23 -6
  10. package/src/code/highlight.ts +89 -105
  11. package/src/components/Badge/Badge.vue +4 -4
  12. package/src/components/CodeBlock/CodeBlock.vue +43 -53
  13. package/src/components/CodeGroup/CodeGroup.vue +32 -35
  14. package/src/components/Footprints/Footprints.vue +2 -14
  15. package/src/components/Footprints/parse.ts +1 -3
  16. package/src/components/ImagePreview/ImagePreview.vue +101 -126
  17. package/src/components/Mermaid/Mermaid.vue +9 -12
  18. package/src/components/Mindmap/FocusBreadcrumbs.test.ts +40 -30
  19. package/src/components/Mindmap/FocusBreadcrumbs.vue +31 -28
  20. package/src/components/Mindmap/InlineRuns.ts +25 -16
  21. package/src/components/Mindmap/Mindmap.vue +147 -81
  22. package/src/components/Mindmap/MindmapOutlineNode.vue +4 -1
  23. package/src/components/Mindmap/editor/AppIcon.vue +32 -11
  24. package/src/components/Mindmap/editor/CanvasContextMenu.vue +126 -49
  25. package/src/components/Mindmap/editor/LinkPopover.vue +101 -23
  26. package/src/components/Mindmap/editor/MarkdownView.vue +7 -4
  27. package/src/components/Mindmap/editor/MindmapView.vue +11 -3
  28. package/src/components/Mindmap/editor/OutlineView.vue +144 -58
  29. package/src/components/Mindmap/editor/RichInlineEditor.vue +93 -44
  30. package/src/components/Mindmap/editor/SelectionToolbar.vue +133 -41
  31. package/src/components/Mindmap/editor/canvasClipboard.ts +1 -5
  32. package/src/components/Mindmap/editor/imagePaste.ts +1 -1
  33. package/src/components/Mindmap/editor/mindmapClipboard.ts +17 -5
  34. package/src/components/Mindmap/editor/outlineDrag.ts +1 -1
  35. package/src/components/Mindmap/markdown.ts +10 -29
  36. package/src/components/WordList/RightClickMenu.vue +10 -35
  37. package/src/components/WordList/WordList.vue +19 -42
  38. package/src/components/sharedMedia.test.ts +94 -112
  39. package/src/entries/bilibili-video.ts +1 -1
  40. package/src/entries/excalidraw-editor.ts +20 -0
  41. package/src/entries/excalidraw-view.ts +15 -0
  42. package/src/entries/footprints.ts +1 -1
  43. package/src/entries/mermaid.ts +1 -1
  44. package/src/entries/mindmap.ts +1 -1
  45. package/src/entries/notes-table.ts +2 -2
  46. package/src/entries/word-list.ts +1 -1
  47. package/src/excalidraw/ExcalidrawSvg.vue +125 -0
  48. package/src/excalidraw/editorHost.ts +110 -0
  49. package/src/excalidraw/editorSession.test.ts +205 -0
  50. package/src/excalidraw/editorSession.ts +224 -0
  51. package/src/excalidraw/exporter.ts +50 -0
  52. package/src/excalidraw/focus.test.ts +60 -0
  53. package/src/excalidraw/focus.ts +33 -0
  54. package/src/excalidraw/fonts.test.ts +66 -0
  55. package/src/excalidraw/fonts.ts +92 -0
  56. package/src/excalidraw/renderer.test.ts +148 -0
  57. package/src/excalidraw/renderer.ts +105 -0
  58. package/src/excalidraw/scene.ts +93 -0
  59. package/src/index.ts +25 -33
  60. package/src/markdown/image.test.ts +1 -3
  61. package/src/markdown/image.ts +4 -13
  62. package/src/styles/code.css +5 -5
  63. package/src/styles/prose.css +1 -1
  64. package/src/styles/swiper.css +1 -1
  65. package/src/styles/tokens.css +5 -6
  66. package/src/swiper/hydrate.test.ts +37 -41
  67. package/src/swiper/hydrate.ts +91 -102
@@ -1,17 +1,13 @@
1
- import {
2
- createHighlighterCore,
3
- type HighlighterCore,
4
- type ShikiTransformer,
5
- } from "shiki/core";
6
- import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
7
- import { bundledLanguages, bundledLanguagesAlias } from "shiki/langs";
1
+ import { createHighlighterCore, type HighlighterCore, type ShikiTransformer } from 'shiki/core'
2
+ import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
3
+ import { bundledLanguages, bundledLanguagesAlias } from 'shiki/langs'
8
4
 
9
5
  export interface CodeMeta {
10
- language: string;
11
- title: string;
12
- lineNumbers: boolean;
13
- startLine: number;
14
- highlightedLines: number[];
6
+ language: string
7
+ title: string
8
+ lineNumbers: boolean
9
+ startLine: number
10
+ highlightedLines: number[]
15
11
  }
16
12
 
17
13
  /**
@@ -20,113 +16,103 @@ export interface CodeMeta {
20
16
  */
21
17
  export const SHARED_CODE_GROUP_CONTRACT = {
22
18
  source: [
23
- "::: code-group",
24
- "```js:line-numbers=4 {2} [one.js]",
25
- "console.log(1)",
26
- "console.log(2)",
27
- "```",
28
- "```ts:no-line-numbers [two.ts]",
29
- "const value: number = 2",
30
- "```",
31
- ":::",
32
- ].join("\n"),
19
+ '::: code-group',
20
+ '```js:line-numbers=4 {2} [one.js]',
21
+ 'console.log(1)',
22
+ 'console.log(2)',
23
+ '```',
24
+ '```ts:no-line-numbers [two.ts]',
25
+ 'const value: number = 2',
26
+ '```',
27
+ ':::'
28
+ ].join('\n'),
33
29
  items: [
34
30
  {
35
- info: "js:line-numbers=4 {2} [one.js]",
36
- code: "console.log(1)\nconsole.log(2)",
37
- title: "one.js",
38
- language: "js",
31
+ info: 'js:line-numbers=4 {2} [one.js]',
32
+ code: 'console.log(1)\nconsole.log(2)',
33
+ title: 'one.js',
34
+ language: 'js',
39
35
  lineNumbers: true,
40
36
  startLine: 4,
41
- highlightedLines: [2],
37
+ highlightedLines: [2]
42
38
  },
43
39
  {
44
- info: "ts:no-line-numbers [two.ts]",
45
- code: "const value: number = 2",
46
- title: "two.ts",
47
- language: "ts",
40
+ info: 'ts:no-line-numbers [two.ts]',
41
+ code: 'const value: number = 2',
42
+ title: 'two.ts',
43
+ language: 'ts',
48
44
  lineNumbers: false,
49
45
  startLine: 1,
50
- highlightedLines: [],
51
- },
52
- ],
53
- } as const;
46
+ highlightedLines: []
47
+ }
48
+ ]
49
+ } as const
54
50
 
55
- export function parseCodeMeta(info = "", defaultLineNumbers = true): CodeMeta {
51
+ export function parseCodeMeta(info = '', defaultLineNumbers = true): CodeMeta {
56
52
  const language =
57
53
  info
58
54
  .trim()
59
55
  .match(/^[^\s{\[]+/)?.[0]
60
- ?.split(":")[0] || "text";
61
- const highlightedLines = new Set<number>();
62
- const ranges = info.match(/\{([\d,\s-]+)\}/)?.[1] || "";
63
- for (const range of ranges.split(",")) {
64
- const match = range.trim().match(/^(\d+)(?:-(\d+))?$/);
65
- if (!match) continue;
66
- const start = Number(match[1]);
67
- const end = Number(match[2] ?? start);
68
- if (start < 1 || end < start || end - start > 10000) continue;
69
- for (let line = start; line <= end; line++) highlightedLines.add(line);
56
+ ?.split(':')[0] || 'text'
57
+ const highlightedLines = new Set<number>()
58
+ const ranges = info.match(/\{([\d,\s-]+)\}/)?.[1] || ''
59
+ for (const range of ranges.split(',')) {
60
+ const match = range.trim().match(/^(\d+)(?:-(\d+))?$/)
61
+ if (!match) continue
62
+ const start = Number(match[1])
63
+ const end = Number(match[2] ?? start)
64
+ if (start < 1 || end < start || end - start > 10000) continue
65
+ for (let line = start; line <= end; line++) highlightedLines.add(line)
70
66
  }
71
67
  return {
72
68
  language,
73
- title: info.match(/\[([^\]]*)\]/)?.[1]?.trim() || "",
69
+ title: info.match(/\[([^\]]*)\]/)?.[1]?.trim() || '',
74
70
  lineNumbers: /:no-line-numbers\b/.test(info)
75
71
  ? false
76
72
  : /:line-numbers\b/.test(info) || defaultLineNumbers,
77
73
  startLine: Math.max(1, Number(info.match(/:line-numbers=(\d+)/)?.[1] || 1)),
78
- highlightedLines: [...highlightedLines].sort((a, b) => a - b),
79
- };
74
+ highlightedLines: [...highlightedLines].sort((a, b) => a - b)
75
+ }
80
76
  }
81
77
 
82
- let singleton: Promise<HighlighterCore> | undefined;
83
- let ready: HighlighterCore | undefined;
84
- const loading = new Map<string, Promise<void>>();
78
+ let singleton: Promise<HighlighterCore> | undefined
79
+ let ready: HighlighterCore | undefined
80
+ const loading = new Map<string, Promise<void>>()
85
81
 
86
82
  export function normalizeCodeLanguage(language: string): string {
87
- const id = language.toLowerCase();
88
- if (id === "plain" || id === "txt" || id === "plaintext" || !id)
89
- return "text";
90
- if (id in bundledLanguages || id in bundledLanguagesAlias) return id;
83
+ const id = language.toLowerCase()
84
+ if (id === 'plain' || id === 'txt' || id === 'plaintext' || !id) return 'text'
85
+ if (id in bundledLanguages || id in bundledLanguagesAlias) return id
91
86
  // Unknown labels retain their caption, but display safely as plain text.
92
- return "text";
87
+ return 'text'
93
88
  }
94
89
 
95
90
  /** One engine and one grammar cache for build-time and browser consumers. */
96
- export async function prepareCodeHighlighter(
97
- languages: string[] = [],
98
- ): Promise<HighlighterCore> {
91
+ export async function prepareCodeHighlighter(languages: string[] = []): Promise<HighlighterCore> {
99
92
  singleton ??= createHighlighterCore({
100
93
  engine: createJavaScriptRegexEngine(),
101
- themes: [
102
- import("shiki/themes/github-light.mjs"),
103
- import("shiki/themes/github-dark.mjs"),
104
- ],
105
- langs: [],
106
- }).then((highlighter) => (ready = highlighter));
107
- const highlighter = await singleton;
94
+ themes: [import('shiki/themes/github-light.mjs'), import('shiki/themes/github-dark.mjs')],
95
+ langs: []
96
+ }).then((highlighter) => (ready = highlighter))
97
+ const highlighter = await singleton
108
98
  await Promise.all(
109
99
  languages.map(async (requested) => {
110
- const language = normalizeCodeLanguage(requested);
111
- if (
112
- language === "text" ||
113
- highlighter.getLoadedLanguages().includes(language)
114
- )
115
- return;
116
- let pending = loading.get(language);
100
+ const language = normalizeCodeLanguage(requested)
101
+ if (language === 'text' || highlighter.getLoadedLanguages().includes(language)) return
102
+ let pending = loading.get(language)
117
103
  if (!pending) {
118
104
  const loader =
119
105
  bundledLanguages[language as keyof typeof bundledLanguages] ??
120
- bundledLanguagesAlias[language as keyof typeof bundledLanguagesAlias];
121
- if (!loader) return;
122
- pending = highlighter.loadLanguage(loader).then(() => undefined);
123
- loading.set(language, pending);
124
- pending.catch(() => loading.delete(language));
106
+ bundledLanguagesAlias[language as keyof typeof bundledLanguagesAlias]
107
+ if (!loader) return
108
+ pending = highlighter.loadLanguage(loader).then(() => undefined)
109
+ loading.set(language, pending)
110
+ pending.catch(() => loading.delete(language))
125
111
  }
126
- await pending;
127
- }),
128
- );
129
- return highlighter;
112
+ await pending
113
+ })
114
+ )
115
+ return highlighter
130
116
  }
131
117
 
132
118
  function transformers(meta: CodeMeta): ShikiTransformer[] {
@@ -134,38 +120,36 @@ function transformers(meta: CodeMeta): ShikiTransformer[] {
134
120
  // notation transformers (`[!code ++]` etc.); Desk has no write path for them.
135
121
  return [
136
122
  {
137
- name: "tnotes-code-meta",
123
+ name: 'tnotes-code-meta',
138
124
  pre(node) {
139
- this.addClassToHast(node, "tn-code-highlight");
140
- node.properties.tabindex = 0;
125
+ this.addClassToHast(node, 'tn-code-highlight')
126
+ node.properties.tabindex = 0
141
127
  },
142
128
  line(node, line) {
143
- node.properties["data-line"] = String(meta.startLine + line - 1);
144
- if (meta.highlightedLines.includes(line))
145
- this.addClassToHast(node, "highlighted");
146
- },
147
- },
148
- ];
129
+ node.properties['data-line'] = String(meta.startLine + line - 1)
130
+ if (meta.highlightedLines.includes(line)) this.addClassToHast(node, 'highlighted')
131
+ }
132
+ }
133
+ ]
149
134
  }
150
135
 
151
136
  /** Markdown renderers prewarm document languages before their synchronous render pass. */
152
- export function highlightCodeSync(code: string, info = ""): string {
153
- if (!ready)
154
- throw new Error("Call prepareCodeHighlighter before rendering Markdown");
155
- const meta = parseCodeMeta(info);
156
- const normalizedLanguage = normalizeCodeLanguage(meta.language);
137
+ export function highlightCodeSync(code: string, info = ''): string {
138
+ if (!ready) throw new Error('Call prepareCodeHighlighter before rendering Markdown')
139
+ const meta = parseCodeMeta(info)
140
+ const normalizedLanguage = normalizeCodeLanguage(meta.language)
157
141
  const language = ready.getLoadedLanguages().includes(normalizedLanguage)
158
142
  ? normalizedLanguage
159
- : "text";
160
- return ready.codeToHtml(code.replace(/\n$/, ""), {
143
+ : 'text'
144
+ return ready.codeToHtml(code.replace(/\n$/, ''), {
161
145
  lang: language,
162
- themes: { light: "github-light", dark: "github-dark" },
146
+ themes: { light: 'github-light', dark: 'github-dark' },
163
147
  defaultColor: false,
164
- transformers: transformers(meta),
165
- });
148
+ transformers: transformers(meta)
149
+ })
166
150
  }
167
151
 
168
- export async function highlightCode(code: string, info = ""): Promise<string> {
169
- await prepareCodeHighlighter([parseCodeMeta(info).language]);
170
- return highlightCodeSync(code, info);
152
+ export async function highlightCode(code: string, info = ''): Promise<string> {
153
+ await prepareCodeHighlighter([parseCodeMeta(info).language])
154
+ return highlightCodeSync(code, info)
171
155
  }
@@ -1,11 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  withDefaults(
3
3
  defineProps<{
4
- text?: string;
5
- type?: "info" | "tip" | "warning" | "danger";
4
+ text?: string
5
+ type?: 'info' | 'tip' | 'warning' | 'danger'
6
6
  }>(),
7
- { text: "", type: "info" },
8
- );
7
+ { text: '', type: 'info' }
8
+ )
9
9
  </script>
10
10
 
11
11
  <template>
@@ -1,73 +1,70 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeUnmount, onServerPrefetch, ref, watch } from "vue";
3
- import { highlightCode, parseCodeMeta } from "../../code/highlight";
4
- import { copyText } from "../../browser/clipboard";
2
+ import { computed, onBeforeUnmount, onServerPrefetch, ref, watch } from 'vue'
3
+ import { highlightCode, parseCodeMeta } from '../../code/highlight'
4
+ import { copyText } from '../../browser/clipboard'
5
5
 
6
6
  const props = withDefaults(
7
7
  defineProps<{
8
- code: string;
9
- info?: string;
10
- highlightedHtml?: string;
11
- lineNumbers?: boolean;
12
- title?: string;
8
+ code: string
9
+ info?: string
10
+ highlightedHtml?: string
11
+ lineNumbers?: boolean
12
+ title?: string
13
13
  }>(),
14
- { info: "", lineNumbers: true },
15
- );
16
- const meta = computed(() => parseCodeMeta(props.info, props.lineNumbers));
17
- const html = ref(props.highlightedHtml || "");
18
- const error = ref("");
19
- const copied = ref(false);
20
- const fullscreen = ref(false);
21
- const trigger = ref<HTMLButtonElement>();
22
- let generation = 0;
23
- let copyTimer: ReturnType<typeof setTimeout> | undefined;
14
+ { info: '', lineNumbers: true }
15
+ )
16
+ const meta = computed(() => parseCodeMeta(props.info, props.lineNumbers))
17
+ const html = ref(props.highlightedHtml || '')
18
+ const error = ref('')
19
+ const copied = ref(false)
20
+ const fullscreen = ref(false)
21
+ const trigger = ref<HTMLButtonElement>()
22
+ let generation = 0
23
+ let copyTimer: ReturnType<typeof setTimeout> | undefined
24
24
 
25
25
  async function renderCode(): Promise<void> {
26
- const version = ++generation;
27
- error.value = "";
26
+ const version = ++generation
27
+ error.value = ''
28
28
  if (props.highlightedHtml !== undefined) {
29
- html.value = props.highlightedHtml;
30
- return;
29
+ html.value = props.highlightedHtml
30
+ return
31
31
  }
32
32
  try {
33
- const output = await highlightCode(props.code, props.info);
34
- if (version === generation) html.value = output;
33
+ const output = await highlightCode(props.code, props.info)
34
+ if (version === generation) html.value = output
35
35
  } catch (cause) {
36
36
  if (version === generation) {
37
- html.value = "";
38
- error.value = `高亮失败:${cause instanceof Error ? cause.message : String(cause)}`;
37
+ html.value = ''
38
+ error.value = `高亮失败:${cause instanceof Error ? cause.message : String(cause)}`
39
39
  }
40
40
  }
41
41
  }
42
42
  watch(() => [props.code, props.info, props.highlightedHtml], renderCode, {
43
- immediate: true,
44
- });
45
- onServerPrefetch(renderCode);
43
+ immediate: true
44
+ })
45
+ onServerPrefetch(renderCode)
46
46
  async function copy(): Promise<void> {
47
47
  try {
48
- await copyText(props.code.replace(/\n$/, ""));
49
- copied.value = true;
50
- clearTimeout(copyTimer);
51
- copyTimer = setTimeout(() => (copied.value = false), 1500);
48
+ await copyText(props.code.replace(/\n$/, ''))
49
+ copied.value = true
50
+ clearTimeout(copyTimer)
51
+ copyTimer = setTimeout(() => (copied.value = false), 1500)
52
52
  } catch {
53
- error.value = "复制失败,请检查剪贴板权限";
53
+ error.value = '复制失败,请检查剪贴板权限'
54
54
  }
55
55
  }
56
56
  function closeFullscreen(): void {
57
- fullscreen.value = false;
58
- trigger.value?.focus({ preventScroll: true });
57
+ fullscreen.value = false
58
+ trigger.value?.focus({ preventScroll: true })
59
59
  }
60
60
  onBeforeUnmount(() => {
61
- generation++;
62
- clearTimeout(copyTimer);
63
- });
61
+ generation++
62
+ clearTimeout(copyTimer)
63
+ })
64
64
  </script>
65
65
 
66
66
  <template>
67
- <section
68
- class="tn-code-block"
69
- :class="{ 'has-line-numbers': meta.lineNumbers }"
70
- >
67
+ <section class="tn-code-block" :class="{ 'has-line-numbers': meta.lineNumbers }">
71
68
  <header class="tn-code-block__header">
72
69
  <span class="tn-code-block__title">{{ title || meta.title }}</span>
73
70
  <slot name="language" :language="meta.language"
@@ -108,9 +105,7 @@ onBeforeUnmount(() => {
108
105
  aria-hidden="true"
109
106
  >
110
107
  <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
111
- <path
112
- d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
113
- />
108
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
114
109
  </svg>
115
110
  </button>
116
111
  <button
@@ -155,14 +150,9 @@ onBeforeUnmount(() => {
155
150
  >
156
151
  <header>
157
152
  <span>{{ title || meta.title || meta.language }}</span
158
- ><button type="button" autofocus @click="closeFullscreen">
159
- 关闭(Esc)
160
- </button>
153
+ ><button type="button" autofocus @click="closeFullscreen">关闭(Esc)</button>
161
154
  </header>
162
- <div
163
- class="tn-code-block"
164
- :class="{ 'has-line-numbers': meta.lineNumbers }"
165
- >
155
+ <div class="tn-code-block" :class="{ 'has-line-numbers': meta.lineNumbers }">
166
156
  <div v-if="html" class="tn-code-block__content" v-html="html" />
167
157
  <pre v-else><code>{{ code }}</code></pre>
168
158
  </div>
@@ -1,48 +1,47 @@
1
1
  <script setup lang="ts">
2
- import { nextTick, onMounted, ref, watch } from "vue";
3
- import CodeBlock from "../CodeBlock/CodeBlock.vue";
4
- import { parseCodeMeta } from "../../code/highlight";
2
+ import { nextTick, onMounted, ref, watch } from 'vue'
3
+ import CodeBlock from '../CodeBlock/CodeBlock.vue'
4
+ import { parseCodeMeta } from '../../code/highlight'
5
5
 
6
6
  export interface CodeGroupItem {
7
- code?: string;
8
- info?: string;
9
- highlightedHtml?: string;
10
- key?: string;
7
+ code?: string
8
+ info?: string
9
+ highlightedHtml?: string
10
+ key?: string
11
11
  }
12
- const props = defineProps<{ items: CodeGroupItem[] }>();
13
- const root = ref<HTMLElement>();
14
- const active = ref(0);
12
+ const props = defineProps<{ items: CodeGroupItem[] }>()
13
+ const root = ref<HTMLElement>()
14
+ const active = ref(0)
15
15
  function syncPanels(): void {
16
16
  const panels =
17
17
  root.value?.querySelectorAll<HTMLElement>(
18
- ":scope > .tn-code-group__panels > .tn-code-group__panel",
19
- ) ?? [];
18
+ ':scope > .tn-code-group__panels > .tn-code-group__panel'
19
+ ) ?? []
20
20
  panels.forEach((panel, index) => {
21
- panel.hidden = index !== active.value;
22
- });
21
+ panel.hidden = index !== active.value
22
+ })
23
23
  }
24
- watch(active, syncPanels, { flush: "post" });
24
+ watch(active, syncPanels, { flush: 'post' })
25
25
  watch(
26
26
  () => props.items.length,
27
27
  (length) => {
28
- active.value = Math.max(0, Math.min(active.value, length - 1));
29
- nextTick(syncPanels);
30
- },
31
- );
32
- onMounted(syncPanels);
28
+ active.value = Math.max(0, Math.min(active.value, length - 1))
29
+ nextTick(syncPanels)
30
+ }
31
+ )
32
+ onMounted(syncPanels)
33
33
  function navigate(event: KeyboardEvent, index: number): void {
34
- let next = index;
35
- if (event.key === "ArrowRight") next = (index + 1) % props.items.length;
36
- else if (event.key === "ArrowLeft")
37
- next = (index - 1 + props.items.length) % props.items.length;
38
- else if (event.key === "Home") next = 0;
39
- else if (event.key === "End") next = props.items.length - 1;
40
- else return;
41
- event.preventDefault();
42
- active.value = next;
43
- (event.currentTarget as HTMLElement).parentElement
44
- ?.querySelectorAll<HTMLButtonElement>("button")
45
- [next]?.focus();
34
+ let next = index
35
+ if (event.key === 'ArrowRight') next = (index + 1) % props.items.length
36
+ else if (event.key === 'ArrowLeft') next = (index - 1 + props.items.length) % props.items.length
37
+ else if (event.key === 'Home') next = 0
38
+ else if (event.key === 'End') next = props.items.length - 1
39
+ else return
40
+ event.preventDefault()
41
+ active.value = next
42
+ ;(event.currentTarget as HTMLElement).parentElement
43
+ ?.querySelectorAll<HTMLButtonElement>('button')
44
+ [next]?.focus()
46
45
  }
47
46
  </script>
48
47
 
@@ -67,9 +66,7 @@ function navigate(event: KeyboardEvent, index: number): void {
67
66
  @keydown="navigate($event, index)"
68
67
  >
69
68
  {{
70
- parseCodeMeta(item.info).title ||
71
- parseCodeMeta(item.info).language ||
72
- `代码 ${index + 1}`
69
+ parseCodeMeta(item.info).title || parseCodeMeta(item.info).language || `代码 ${index + 1}`
73
70
  }}
74
71
  </button>
75
72
  </div>
@@ -1,13 +1,5 @@
1
1
  <script setup lang="ts">
2
- import {
3
- computed,
4
- nextTick,
5
- onBeforeUnmount,
6
- onMounted,
7
- ref,
8
- useSlots,
9
- watch
10
- } from 'vue'
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useSlots, watch } from 'vue'
11
3
 
12
4
  const props = withDefaults(
13
5
  defineProps<{
@@ -141,11 +133,7 @@ watch(
141
133
  <!-- tn-preview-ignore: opt out of Layout ImagePreview so only the in-component modal opens -->
142
134
  <div class="tn-footprints tn-preview-ignore">
143
135
  <div class="tn-footprints__text">
144
- <div
145
- ref="textEl"
146
- class="tn-footprints__text-body"
147
- :class="{ 'is-collapsed': isCollapsed }"
148
- >
136
+ <div ref="textEl" class="tn-footprints__text-body" :class="{ 'is-collapsed': isCollapsed }">
149
137
  <slot name="text-area">
150
138
  <p v-for="(line, i) in paragraphs" :key="i">{{ line }}</p>
151
139
  </slot>
@@ -11,9 +11,7 @@ export interface FootprintsPayload {
11
11
  export function parseFootprintsDatetime(meta: string): number[] {
12
12
  const m = meta
13
13
  .trim()
14
- .match(
15
- /^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/
16
- )
14
+ .match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/)
17
15
  if (!m) return []
18
16
  const parts = [Number(m[1]), Number(m[2]), Number(m[3])]
19
17
  if (m[4] !== undefined) {