@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.
- package/README.md +30 -97
- package/dist/code/highlight.js +6 -15
- package/dist/components/Footprints/parse.js +1 -3
- package/dist/components/Mindmap/markdown.d.ts +1 -7
- package/dist/components/Mindmap/markdown.js +1 -15
- package/package.json +28 -13
- package/src/browser/clipboard.ts +16 -19
- package/src/browser/theme.ts +10 -15
- package/src/code/highlight.test.ts +23 -6
- package/src/code/highlight.ts +89 -105
- package/src/components/Badge/Badge.vue +4 -4
- package/src/components/CodeBlock/CodeBlock.vue +43 -53
- package/src/components/CodeGroup/CodeGroup.vue +32 -35
- package/src/components/Footprints/Footprints.vue +2 -14
- package/src/components/Footprints/parse.ts +1 -3
- package/src/components/ImagePreview/ImagePreview.vue +101 -126
- package/src/components/Mermaid/Mermaid.vue +9 -12
- package/src/components/Mindmap/FocusBreadcrumbs.test.ts +40 -30
- package/src/components/Mindmap/FocusBreadcrumbs.vue +31 -28
- package/src/components/Mindmap/InlineRuns.ts +25 -16
- package/src/components/Mindmap/Mindmap.vue +147 -81
- package/src/components/Mindmap/MindmapOutlineNode.vue +4 -1
- package/src/components/Mindmap/editor/AppIcon.vue +32 -11
- package/src/components/Mindmap/editor/CanvasContextMenu.vue +126 -49
- package/src/components/Mindmap/editor/LinkPopover.vue +101 -23
- package/src/components/Mindmap/editor/MarkdownView.vue +7 -4
- package/src/components/Mindmap/editor/MindmapView.vue +11 -3
- package/src/components/Mindmap/editor/OutlineView.vue +144 -58
- package/src/components/Mindmap/editor/RichInlineEditor.vue +93 -44
- package/src/components/Mindmap/editor/SelectionToolbar.vue +133 -41
- package/src/components/Mindmap/editor/canvasClipboard.ts +1 -5
- package/src/components/Mindmap/editor/imagePaste.ts +1 -1
- package/src/components/Mindmap/editor/mindmapClipboard.ts +17 -5
- package/src/components/Mindmap/editor/outlineDrag.ts +1 -1
- package/src/components/Mindmap/markdown.ts +10 -29
- package/src/components/WordList/RightClickMenu.vue +10 -35
- package/src/components/WordList/WordList.vue +19 -42
- package/src/components/sharedMedia.test.ts +94 -112
- package/src/entries/bilibili-video.ts +1 -1
- package/src/entries/excalidraw-editor.ts +20 -0
- package/src/entries/excalidraw-view.ts +15 -0
- package/src/entries/footprints.ts +1 -1
- package/src/entries/mermaid.ts +1 -1
- package/src/entries/mindmap.ts +1 -1
- package/src/entries/notes-table.ts +2 -2
- package/src/entries/word-list.ts +1 -1
- package/src/excalidraw/ExcalidrawSvg.vue +125 -0
- package/src/excalidraw/editorHost.ts +110 -0
- package/src/excalidraw/editorSession.test.ts +205 -0
- package/src/excalidraw/editorSession.ts +224 -0
- package/src/excalidraw/exporter.ts +50 -0
- package/src/excalidraw/focus.test.ts +60 -0
- package/src/excalidraw/focus.ts +33 -0
- package/src/excalidraw/fonts.test.ts +66 -0
- package/src/excalidraw/fonts.ts +92 -0
- package/src/excalidraw/renderer.test.ts +148 -0
- package/src/excalidraw/renderer.ts +105 -0
- package/src/excalidraw/scene.ts +93 -0
- package/src/index.ts +25 -33
- package/src/markdown/image.test.ts +1 -3
- package/src/markdown/image.ts +4 -13
- package/src/styles/code.css +5 -5
- package/src/styles/prose.css +2 -2
- package/src/styles/swiper.css +1 -1
- package/src/styles/tokens.css +5 -6
- package/src/swiper/hydrate.test.ts +37 -41
- package/src/swiper/hydrate.ts +91 -102
package/src/styles/tokens.css
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
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=
|
|
50
|
-
:root[data-theme=
|
|
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
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import { hydrateTnSwipers } from
|
|
5
|
+
import { hydrateTnSwipers } from './hydrate'
|
|
6
6
|
|
|
7
7
|
function swiperShell(titles: string[]): HTMLElement {
|
|
8
|
-
const root = document.createElement(
|
|
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(
|
|
29
|
-
it(
|
|
30
|
-
const root = swiperShell([
|
|
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(
|
|
34
|
-
const tabs = [...host.querySelectorAll(
|
|
35
|
-
|
|
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(
|
|
41
|
-
expect(slides[0]!.classList.contains(
|
|
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(
|
|
47
|
-
expect(slides[0]!.hidden).toBe(true)
|
|
48
|
-
expect(slides[1]!.hidden).toBe(false)
|
|
49
|
-
expect(slides[1]!.classList.contains(
|
|
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(
|
|
57
|
-
const root = swiperShell([
|
|
58
|
-
hydrateTnSwipers(root)
|
|
59
|
-
expect(root.querySelector(
|
|
60
|
-
expect(root.querySelector(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
})
|
package/src/swiper/hydrate.ts
CHANGED
|
@@ -5,56 +5,49 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export function applySwiperTabsPadding(tabs: HTMLElement, hasNav: boolean): void {
|
|
8
|
-
tabs.style.padding = hasNav ?
|
|
8
|
+
tabs.style.padding = hasNav ? '0 0.8rem 0 3rem' : '0 0.8rem'
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function createSwiperTabNav(handlers: {
|
|
12
|
-
|
|
13
|
-
|
|
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(
|
|
20
|
-
prev.type =
|
|
21
|
-
prev.className =
|
|
22
|
-
prev.textContent =
|
|
23
|
-
prev.title =
|
|
24
|
-
prev.addEventListener(
|
|
25
|
-
event.preventDefault()
|
|
26
|
-
event.stopPropagation()
|
|
27
|
-
handlers.onPrev()
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
const line = document.createElement(
|
|
31
|
-
line.className =
|
|
32
|
-
line.textContent =
|
|
33
|
-
|
|
34
|
-
const next = document.createElement(
|
|
35
|
-
next.type =
|
|
36
|
-
next.className =
|
|
37
|
-
next.textContent =
|
|
38
|
-
next.title =
|
|
39
|
-
next.addEventListener(
|
|
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
|
-
|
|
50
|
-
length
|
|
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 =
|
|
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(
|
|
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>(
|
|
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] ===
|
|
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] =
|
|
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>(
|
|
87
|
-
const wrapper = host.querySelector<HTMLElement>(
|
|
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(
|
|
102
|
-
tabsEl.className =
|
|
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(
|
|
115
|
-
)
|
|
105
|
+
button.classList.toggle('active', buttonIndex === index)
|
|
106
|
+
)
|
|
116
107
|
slides.forEach((slide, slideIndex) => {
|
|
117
|
-
const on = slideIndex === index
|
|
118
|
-
slide.classList.toggle(
|
|
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
|
-
|
|
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(
|
|
141
|
-
|
|
142
|
-
slide.dataset.title = title
|
|
143
|
-
const tab = document.createElement(
|
|
144
|
-
tab.type =
|
|
145
|
-
tab.className =
|
|
146
|
-
tab.textContent = title
|
|
147
|
-
tab.addEventListener(
|
|
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
|
}
|