@tnotesjs/ui 0.1.4 → 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 +28 -13
  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 +2 -2
  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
@@ -24,10 +24,9 @@
24
24
  --tn-c-default-soft: #e8e8e9;
25
25
  --tn-shadow-2: 0 4px 12px rgba(0, 0, 0, 0.12);
26
26
  --tn-font-sans:
27
- "Punctuation SC", Inter, ui-sans-serif, system-ui, sans-serif,
28
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
29
- --tn-font-mono:
30
- "SFMono-Regular", "Cascadia Code", "Roboto Mono", Consolas, monospace;
27
+ 'Punctuation SC', Inter, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
28
+ 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
29
+ --tn-font-mono: 'SFMono-Regular', 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
31
30
  --tn-prose-size: 16px;
32
31
  --tn-prose-line-height: 28px;
33
32
  --tn-heading-weight: 600;
@@ -46,8 +45,8 @@
46
45
  color-scheme: light;
47
46
  }
48
47
 
49
- :root.dark:not([data-theme="light"]),
50
- :root[data-theme="dark"] {
48
+ :root.dark:not([data-theme='light']),
49
+ :root[data-theme='dark'] {
51
50
  --tn-c-brand-strong: #a8b1ff;
52
51
  --tn-c-text: #dfdfd6;
53
52
  --tn-c-text-2: #98989f;
@@ -1,11 +1,11 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
- import { describe, expect, it } from "vitest";
3
+ import { describe, expect, it } from 'vitest'
4
4
 
5
- import { hydrateTnSwipers } from "./hydrate";
5
+ import { hydrateTnSwipers } from './hydrate'
6
6
 
7
7
  function swiperShell(titles: string[]): HTMLElement {
8
- const root = document.createElement("div");
8
+ const root = document.createElement('div')
9
9
  root.innerHTML = `
10
10
  <div class="tn-swiper">
11
11
  <div class="tn-swiper-tabs"></div>
@@ -14,52 +14,48 @@ function swiperShell(titles: string[]): HTMLElement {
14
14
  ${titles
15
15
  .map(
16
16
  (title) =>
17
- `<div class="swiper-slide" data-title="${title}"><img alt="${title}" src="/${title}.svg"></div>`,
17
+ `<div class="swiper-slide" data-title="${title}"><img alt="${title}" src="/${title}.svg"></div>`
18
18
  )
19
- .join("")}
19
+ .join('')}
20
20
  </div>
21
21
  </div>
22
22
  </div>
23
- `;
24
- document.body.append(root);
25
- return root;
23
+ `
24
+ document.body.append(root)
25
+ return root
26
26
  }
27
27
 
28
- describe("hydrateTnSwipers", () => {
29
- it("builds tabs and shows only the active slide", () => {
30
- const root = swiperShell(["A", "B", "C"]);
31
- expect(hydrateTnSwipers(root)).toBe(1);
28
+ describe('hydrateTnSwipers', () => {
29
+ it('builds tabs and shows only the active slide', () => {
30
+ const root = swiperShell(['A', 'B', 'C'])
31
+ expect(hydrateTnSwipers(root)).toBe(1)
32
32
 
33
- const host = root.querySelector(".tn-swiper")!;
34
- const tabs = [...host.querySelectorAll(".tn-tab")].map(
35
- (tab) => tab.textContent,
36
- );
37
- expect(tabs).toEqual(["A", "B", "C"]);
38
- expect(host.querySelectorAll(".tn-tab-nav")).toHaveLength(3); // < / >
33
+ const host = root.querySelector('.tn-swiper')!
34
+ const tabs = [...host.querySelectorAll('.tn-tab')].map((tab) => tab.textContent)
35
+ expect(tabs).toEqual(['A', 'B', 'C'])
36
+ expect(host.querySelectorAll('.tn-tab-nav')).toHaveLength(3) // < / >
39
37
 
40
- const slides = [...host.querySelectorAll(".swiper-slide")] as HTMLElement[];
41
- expect(slides[0]!.classList.contains("is-active")).toBe(true);
42
- expect(slides[0]!.hidden).toBe(false);
43
- expect(slides[1]!.hidden).toBe(true);
44
- expect(slides[2]!.hidden).toBe(true);
38
+ const slides = [...host.querySelectorAll('.swiper-slide')] as HTMLElement[]
39
+ expect(slides[0]!.classList.contains('is-active')).toBe(true)
40
+ expect(slides[0]!.hidden).toBe(false)
41
+ expect(slides[1]!.hidden).toBe(true)
42
+ expect(slides[2]!.hidden).toBe(true)
45
43
 
46
- ;(host.querySelectorAll(".tn-tab")[1] as HTMLButtonElement).click();
47
- expect(slides[0]!.hidden).toBe(true);
48
- expect(slides[1]!.hidden).toBe(false);
49
- expect(slides[1]!.classList.contains("is-active")).toBe(true);
44
+ ;(host.querySelectorAll('.tn-tab')[1] as HTMLButtonElement).click()
45
+ expect(slides[0]!.hidden).toBe(true)
46
+ expect(slides[1]!.hidden).toBe(false)
47
+ expect(slides[1]!.classList.contains('is-active')).toBe(true)
50
48
 
51
49
  // Idempotent
52
- expect(hydrateTnSwipers(root)).toBe(0);
53
- root.remove();
54
- });
55
-
56
- it("omits the tab bar for a single slide", () => {
57
- const root = swiperShell(["solo"]);
58
- hydrateTnSwipers(root);
59
- expect(root.querySelector(".tn-swiper-tabs")).toBeNull();
60
- expect(root.querySelector(".swiper-slide")!.classList.contains("is-active")).toBe(
61
- true,
62
- );
63
- root.remove();
64
- });
65
- });
50
+ expect(hydrateTnSwipers(root)).toBe(0)
51
+ root.remove()
52
+ })
53
+
54
+ it('omits the tab bar for a single slide', () => {
55
+ const root = swiperShell(['solo'])
56
+ hydrateTnSwipers(root)
57
+ expect(root.querySelector('.tn-swiper-tabs')).toBeNull()
58
+ expect(root.querySelector('.swiper-slide')!.classList.contains('is-active')).toBe(true)
59
+ root.remove()
60
+ })
61
+ })
@@ -5,56 +5,49 @@
5
5
  */
6
6
 
7
7
  export function applySwiperTabsPadding(tabs: HTMLElement, hasNav: boolean): void {
8
- tabs.style.padding = hasNav ? "0 0.8rem 0 3rem" : "0 0.8rem";
8
+ tabs.style.padding = hasNav ? '0 0.8rem 0 3rem' : '0 0.8rem'
9
9
  }
10
10
 
11
- export function createSwiperTabNav(handlers: {
12
- onPrev: () => void;
13
- onNext: () => void;
14
- }): {
15
- prev: HTMLButtonElement;
16
- line: HTMLSpanElement;
17
- next: HTMLButtonElement;
11
+ export function createSwiperTabNav(handlers: { onPrev: () => void; onNext: () => void }): {
12
+ prev: HTMLButtonElement
13
+ line: HTMLSpanElement
14
+ next: HTMLButtonElement
18
15
  } {
19
- const prev = document.createElement("button");
20
- prev.type = "button";
21
- prev.className = "tn-tab-nav tn-tab-prev";
22
- prev.textContent = "<";
23
- prev.title = "上一页";
24
- prev.addEventListener("click", (event) => {
25
- event.preventDefault();
26
- event.stopPropagation();
27
- handlers.onPrev();
28
- });
29
-
30
- const line = document.createElement("span");
31
- line.className = "tn-tab-nav tab-tab-line";
32
- line.textContent = "/";
33
-
34
- const next = document.createElement("button");
35
- next.type = "button";
36
- next.className = "tn-tab-nav tn-tab-next";
37
- next.textContent = ">";
38
- next.title = "下一页";
39
- next.addEventListener("click", (event) => {
40
- event.preventDefault();
41
- event.stopPropagation();
42
- handlers.onNext();
43
- });
44
-
45
- return { prev, line, next };
16
+ const prev = document.createElement('button')
17
+ prev.type = 'button'
18
+ prev.className = 'tn-tab-nav tn-tab-prev'
19
+ prev.textContent = '<'
20
+ prev.title = '上一页'
21
+ prev.addEventListener('click', (event) => {
22
+ event.preventDefault()
23
+ event.stopPropagation()
24
+ handlers.onPrev()
25
+ })
26
+
27
+ const line = document.createElement('span')
28
+ line.className = 'tn-tab-nav tab-tab-line'
29
+ line.textContent = '/'
30
+
31
+ const next = document.createElement('button')
32
+ next.type = 'button'
33
+ next.className = 'tn-tab-nav tn-tab-next'
34
+ next.textContent = '>'
35
+ next.title = '下一页'
36
+ next.addEventListener('click', (event) => {
37
+ event.preventDefault()
38
+ event.stopPropagation()
39
+ handlers.onNext()
40
+ })
41
+
42
+ return { prev, line, next }
46
43
  }
47
44
 
48
- export function wrapSlideIndex(
49
- index: number,
50
- length: number,
51
- delta: -1 | 1,
52
- ): number {
53
- if (length <= 0) return 0;
54
- return (index + delta + length) % length;
45
+ export function wrapSlideIndex(index: number, length: number, delta: -1 | 1): number {
46
+ if (length <= 0) return 0
47
+ return (index + delta + length) % length
55
48
  }
56
49
 
57
- const READY = "tnSwiperReady";
50
+ const READY = 'tnSwiperReady'
58
51
 
59
52
  /**
60
53
  * Hydrate every `.tn-swiper` under `root` (or `document`). Idempotent:
@@ -62,103 +55,99 @@ const READY = "tnSwiperReady";
62
55
  * When `root` itself is a `.tn-swiper`, it is included.
63
56
  */
64
57
  export function hydrateTnSwipers(root: ParentNode = document): number {
65
- const hosts: HTMLElement[] = [];
66
- if (root instanceof HTMLElement && root.classList.contains("tn-swiper")) {
67
- hosts.push(root);
58
+ const hosts: HTMLElement[] = []
59
+ if (root instanceof HTMLElement && root.classList.contains('tn-swiper')) {
60
+ hosts.push(root)
68
61
  }
69
- hosts.push(...root.querySelectorAll<HTMLElement>(".tn-swiper"));
62
+ hosts.push(...root.querySelectorAll<HTMLElement>('.tn-swiper'))
70
63
 
71
- let count = 0;
72
- const seen = new Set<HTMLElement>();
64
+ let count = 0
65
+ const seen = new Set<HTMLElement>()
73
66
  for (const host of hosts) {
74
- if (seen.has(host)) continue;
75
- seen.add(host);
76
- if (host.dataset[READY] === "true") continue;
67
+ if (seen.has(host)) continue
68
+ seen.add(host)
69
+ if (host.dataset[READY] === 'true') continue
77
70
  if (hydrateOne(host)) {
78
- host.dataset[READY] = "true";
79
- count += 1;
71
+ host.dataset[READY] = 'true'
72
+ count += 1
80
73
  }
81
74
  }
82
- return count;
75
+ return count
83
76
  }
84
77
 
85
78
  function hydrateOne(host: HTMLElement): boolean {
86
- const tabs = host.querySelector<HTMLElement>(".tn-swiper-tabs");
87
- const wrapper = host.querySelector<HTMLElement>(".swiper-wrapper");
88
- if (!wrapper) return false;
79
+ const tabs = host.querySelector<HTMLElement>('.tn-swiper-tabs')
80
+ const wrapper = host.querySelector<HTMLElement>('.swiper-wrapper')
81
+ if (!wrapper) return false
89
82
 
90
- const slides = [
91
- ...wrapper.querySelectorAll<HTMLElement>(":scope > .swiper-slide"),
92
- ];
83
+ const slides = [...wrapper.querySelectorAll<HTMLElement>(':scope > .swiper-slide')]
93
84
  if (slides.length === 0) {
94
85
  // Empty shell — still mark ready so we don't loop.
95
- return true;
86
+ return true
96
87
  }
97
88
 
98
89
  // Ensure tabs host exists (site always emits it; Desk may omit for 1 slide).
99
- let tabsEl = tabs;
90
+ let tabsEl = tabs
100
91
  if (!tabsEl) {
101
- tabsEl = document.createElement("div");
102
- tabsEl.className = "tn-swiper-tabs";
103
- host.prepend(tabsEl);
92
+ tabsEl = document.createElement('div')
93
+ tabsEl.className = 'tn-swiper-tabs'
94
+ host.prepend(tabsEl)
104
95
  }
105
96
 
106
- const useTabs = slides.length > 1;
107
- tabsEl.replaceChildren();
108
- applySwiperTabsPadding(tabsEl, useTabs);
97
+ const useTabs = slides.length > 1
98
+ tabsEl.replaceChildren()
99
+ applySwiperTabsPadding(tabsEl, useTabs)
109
100
 
110
- const tabButtons: HTMLButtonElement[] = [];
101
+ const tabButtons: HTMLButtonElement[] = []
111
102
 
112
103
  const activate = (index: number): void => {
113
104
  tabButtons.forEach((button, buttonIndex) =>
114
- button.classList.toggle("active", buttonIndex === index),
115
- );
105
+ button.classList.toggle('active', buttonIndex === index)
106
+ )
116
107
  slides.forEach((slide, slideIndex) => {
117
- const on = slideIndex === index;
118
- slide.classList.toggle("is-active", on);
119
- slide.hidden = !on;
120
- });
121
- };
108
+ const on = slideIndex === index
109
+ slide.classList.toggle('is-active', on)
110
+ slide.hidden = !on
111
+ })
112
+ }
122
113
 
123
114
  const activeIndex = (): number => {
124
- const found = tabButtons.findIndex((button) =>
125
- button.classList.contains("active"),
126
- );
127
- return found >= 0 ? found : 0;
128
- };
115
+ const found = tabButtons.findIndex((button) => button.classList.contains('active'))
116
+ return found >= 0 ? found : 0
117
+ }
129
118
 
130
119
  if (useTabs) {
131
120
  const nav = createSwiperTabNav({
132
121
  onPrev: () => activate(wrapSlideIndex(activeIndex(), slides.length, -1)),
133
- onNext: () => activate(wrapSlideIndex(activeIndex(), slides.length, 1)),
134
- });
135
- tabsEl.append(nav.prev, nav.line);
122
+ onNext: () => activate(wrapSlideIndex(activeIndex(), slides.length, 1))
123
+ })
124
+ tabsEl.append(nav.prev, nav.line)
136
125
 
137
126
  slides.forEach((slide, index) => {
138
127
  const title =
139
128
  slide.dataset.title?.trim() ||
140
- slide.querySelector("img")?.getAttribute("alt")?.trim() ||
141
- "img";
142
- slide.dataset.title = title;
143
- const tab = document.createElement("button");
144
- tab.type = "button";
145
- tab.className = "tn-tab";
146
- tab.textContent = title;
147
- tab.addEventListener("click", () => activate(index));
148
- tabButtons.push(tab);
149
- tabsEl!.append(tab);
150
- });
151
-
152
- tabsEl.append(nav.next);
129
+ slide.querySelector('img')?.getAttribute('alt')?.trim() ||
130
+ 'img'
131
+ slide.dataset.title = title
132
+ const tab = document.createElement('button')
133
+ tab.type = 'button'
134
+ tab.className = 'tn-tab'
135
+ tab.textContent = title
136
+ tab.addEventListener('click', () => activate(index))
137
+ tabButtons.push(tab)
138
+ tabsEl!.append(tab)
139
+ })
140
+
141
+ tabsEl.append(nav.next)
153
142
  // Multi-slide: tabs stay above the container.
154
143
  if (tabsEl.parentElement !== host || host.firstElementChild !== tabsEl) {
155
- host.prepend(tabsEl);
144
+ host.prepend(tabsEl)
156
145
  }
157
146
  } else {
158
147
  // Single slide: drop empty tab bar (Desk parity).
159
- tabsEl.remove();
148
+ tabsEl.remove()
160
149
  }
161
150
 
162
- activate(0);
163
- return true;
151
+ activate(0)
152
+ return true
164
153
  }