@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.
- 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 +24 -8
- 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 +1 -1
- 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/README.md
CHANGED
|
@@ -1,120 +1,53 @@
|
|
|
1
1
|
# @tnotesjs/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TNotes 内置块的 Vue 组件。消费方:`@tnotesjs/ssg`、Desk。需要 `vue` `^3.5`。样式用 `--tn-*`(`src/styles/tokens.css`)。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- TNotes Desk (Electron visual editor)
|
|
7
|
-
|
|
8
|
-
## Package rules
|
|
9
|
-
|
|
10
|
-
- No direct `vitepress` imports in components
|
|
11
|
-
- Styles use `--tn-*` tokens (`src/styles/tokens.css`)
|
|
12
|
-
- Built-ins use short semantic names in markdown (`BilibiliVideo`, `WordList`); third-party components must use a vendor prefix
|
|
13
|
-
- Short one-letter tags (`B` / `E` / `N` / `F`) are **not** supported
|
|
14
|
-
|
|
15
|
-
## Install
|
|
5
|
+
自由绘图尚未做成组件。磁盘真相源约定为知识库 `assets/*.excalidraw`(Desk 与 SSG 将只维护这一套数据)。配套 SVG 是历史派生,组件落地并改引用后再单独迁移删除。
|
|
16
6
|
|
|
17
7
|
```bash
|
|
18
8
|
pnpm add @tnotesjs/ui
|
|
19
9
|
```
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
## Local development
|
|
24
|
-
|
|
25
|
-
Sibling checkouts under `tnotesjs/` (optional `file:` while iterating):
|
|
26
|
-
|
|
27
|
-
```text
|
|
28
|
-
tnotesjs/ui
|
|
29
|
-
tnotesjs/core # production: "@tnotesjs/ui": "^0.1.0"
|
|
30
|
-
tnotesjs/desk # production: "@tnotesjs/ui": "^0.1.0"
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
cd ui && pnpm install
|
|
35
|
-
cd ../core && pnpm install
|
|
36
|
-
cd ../desk && pnpm install
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Published packages should depend on the npm version of `@tnotesjs/ui`, not `file:../ui`.
|
|
11
|
+
### `BilibiliVideo`
|
|
40
12
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`SHARED_CODE_GROUP_CONTRACT` fixture is exercised in both repositories' CI suites so changes to
|
|
47
|
-
fence metadata, line numbering, titles, or generated markup cannot drift silently.
|
|
48
|
-
|
|
49
|
-
| Syntax / capability | Core page | Desk visual edit | Desk read-only | Desk source |
|
|
50
|
-
| --------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------- | --------------------- | ------------------ |
|
|
51
|
-
| Markdown prose (headings, links, lists, tables, quotes) | Shared `prose.css` | Shared typography + editor hit areas | Shared `prose.css` | Canonical Markdown |
|
|
52
|
-
| Fenced code + lazy Shiki language loading | Shared | Shared in code-group previews; CodeMirror while editing | Shared | Byte-preserved |
|
|
53
|
-
| `::: code-group` | Shared `CodeGroup` | Shared atomic preview | Shared | Byte-preserved |
|
|
54
|
-
| `:line-numbers`, `:line-numbers=N`, `:no-line-numbers` | Yes | Yes | Yes | Byte-preserved |
|
|
55
|
-
| `{N}` fence line highlights | Yes | Yes (CodeMirror) | Yes | Byte-preserved |
|
|
56
|
-
| Shiki `[!code]` annotations | No (aligned with Desk) | No | No | Shown as source |
|
|
57
|
-
| Inline `<Badge>` | Shared `Badge` | Shared inline projection | Shared | Byte-preserved |
|
|
58
|
-
| `NotesTable` | Shared component via Core data adapter | Shared component projection | Shared | Byte-preserved |
|
|
59
|
-
| Math | TNotes SSG MathJax pipeline | Milkdown/KaTeX | Milkdown/KaTeX | Byte-preserved |
|
|
60
|
-
| Image lightbox, navigation, zoom, and pan | Shared `ImagePreview` | Shared `ImagePreview` | Shared `ImagePreview` | N/A |
|
|
61
|
-
| Mermaid / Mindmap / Footprints / BilibiliVideo / WordList | Shared | Shared projection | Shared | Byte-preserved |
|
|
62
|
-
|
|
63
|
-
The hosts may add editing handles, navigation offsets, or knowledge-base data adapters. They must
|
|
64
|
-
not redefine the shared color tokens, prose typography, or code rendering shell.
|
|
65
|
-
|
|
66
|
-
### `BilibiliVideo` — `<BilibiliVideo id="BVxxxx" />`
|
|
67
|
-
|
|
68
|
-
| Prop | Default | Notes |
|
|
69
|
-
| ---------- | -------- | ------------------------------------------- |
|
|
70
|
-
| `id` | required | BV id |
|
|
71
|
-
| `autoplay` | `false` | Embed URL sends `autoplay=0` unless enabled |
|
|
72
|
-
| `muted` | `false` | Embed URL sends `muted=0\|1` |
|
|
13
|
+
| Prop | Default |
|
|
14
|
+
| ---------- | -------- |
|
|
15
|
+
| `id` | required |
|
|
16
|
+
| `autoplay` | `false` |
|
|
17
|
+
| `muted` | `false` |
|
|
73
18
|
|
|
74
19
|
```md
|
|
75
20
|
<BilibiliVideo id="BV1QR4y1y7GG" :autoplay="true" :muted="true" />
|
|
76
21
|
```
|
|
77
22
|
|
|
78
|
-
### `WordList`
|
|
23
|
+
### `WordList`
|
|
79
24
|
|
|
80
|
-
| Prop | Default |
|
|
81
|
-
| ----------------- | ------------------------- |
|
|
82
|
-
| `words` | `[]` |
|
|
83
|
-
| `needSort` | `false` |
|
|
84
|
-
| `wordsBaseUrl` | en-words blob URL |
|
|
85
|
-
| `wordsRawBaseUrl` | en-words raw URL |
|
|
86
|
-
| `features` | `WORD_LIST_FEATURES_FULL` |
|
|
25
|
+
| Prop | Default |
|
|
26
|
+
| ----------------- | ------------------------- |
|
|
27
|
+
| `words` | `[]` |
|
|
28
|
+
| `needSort` | `false` |
|
|
29
|
+
| `wordsBaseUrl` | en-words blob URL |
|
|
30
|
+
| `wordsRawBaseUrl` | en-words raw URL |
|
|
31
|
+
| `features` | `WORD_LIST_FEATURES_FULL` |
|
|
87
32
|
|
|
88
33
|
```md
|
|
89
34
|
<WordList :words="['cancel', 'salary']" :needSort="true" />
|
|
90
35
|
```
|
|
91
36
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
| Preset | Cards | Fetch word data | Menu Pin | Menu Auto Show Card |
|
|
95
|
-
| ----------------------------------- | ----- | --------------- | -------- | ------------------- |
|
|
96
|
-
| `WORD_LIST_FEATURES_FULL` (default) | ✓ | ✓ | ✓ | ✓ |
|
|
97
|
-
| `WORD_LIST_FEATURES_STATIC` | — | — | — | — |
|
|
37
|
+
`WORD_LIST_FEATURES_FULL`:卡片 + 拉取词表 + 菜单钉住 / 自动展开。`WORD_LIST_FEATURES_STATIC`:全关。
|
|
98
38
|
|
|
99
39
|
### `Mermaid`
|
|
100
40
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
| Prop | Default |
|
|
104
|
-
| -------------------- | --------- |
|
|
105
|
-
| `source` | `''` |
|
|
106
|
-
| `graph` | `''` |
|
|
107
|
-
| `id` | auto |
|
|
108
|
-
| `center` | `false` |
|
|
109
|
-
| `isDark` | auto |
|
|
110
|
-
| `securityLevel` | `'loose'` |
|
|
111
|
-
| `enableCopy` | `true` |
|
|
112
|
-
| `enableFullscreen` | `true` |
|
|
113
|
-
| `enableCenterToggle` | `true` |
|
|
114
|
-
|
|
115
|
-
Fence:
|
|
116
|
-
|
|
117
|
-
- ` ```mermaid ` → not centered
|
|
118
|
-
- ` ```mermaid center ` → centered
|
|
119
|
-
|
|
120
|
-
Icons for the toggle live in `src/components/Mermaid/icons/` (`icon__center_on.svg` / `icon__center_off.svg`).
|
|
41
|
+
围栏语言 `mermaid`;` ```mermaid center ` 居中。
|
|
42
|
+
|
|
43
|
+
| Prop | Default |
|
|
44
|
+
| -------------------- | --------- |
|
|
45
|
+
| `source` | `''` |
|
|
46
|
+
| `graph` | `''` |
|
|
47
|
+
| `id` | auto |
|
|
48
|
+
| `center` | `false` |
|
|
49
|
+
| `isDark` | auto |
|
|
50
|
+
| `securityLevel` | `'loose'` |
|
|
51
|
+
| `enableCopy` | `true` |
|
|
52
|
+
| `enableFullscreen` | `true` |
|
|
53
|
+
| `enableCenterToggle` | `true` |
|
package/dist/code/highlight.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// src/code/highlight.ts
|
|
2
|
-
import {
|
|
3
|
-
createHighlighterCore
|
|
4
|
-
} from "shiki/core";
|
|
2
|
+
import { createHighlighterCore } from "shiki/core";
|
|
5
3
|
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
|
6
4
|
import { bundledLanguages, bundledLanguagesAlias } from "shiki/langs";
|
|
7
5
|
var SHARED_CODE_GROUP_CONTRACT = {
|
|
@@ -62,26 +60,21 @@ var ready;
|
|
|
62
60
|
var loading = /* @__PURE__ */ new Map();
|
|
63
61
|
function normalizeCodeLanguage(language) {
|
|
64
62
|
const id = language.toLowerCase();
|
|
65
|
-
if (id === "plain" || id === "txt" || id === "plaintext" || !id)
|
|
66
|
-
return "text";
|
|
63
|
+
if (id === "plain" || id === "txt" || id === "plaintext" || !id) return "text";
|
|
67
64
|
if (id in bundledLanguages || id in bundledLanguagesAlias) return id;
|
|
68
65
|
return "text";
|
|
69
66
|
}
|
|
70
67
|
async function prepareCodeHighlighter(languages = []) {
|
|
71
68
|
singleton ??= createHighlighterCore({
|
|
72
69
|
engine: createJavaScriptRegexEngine(),
|
|
73
|
-
themes: [
|
|
74
|
-
import("shiki/themes/github-light.mjs"),
|
|
75
|
-
import("shiki/themes/github-dark.mjs")
|
|
76
|
-
],
|
|
70
|
+
themes: [import("shiki/themes/github-light.mjs"), import("shiki/themes/github-dark.mjs")],
|
|
77
71
|
langs: []
|
|
78
72
|
}).then((highlighter2) => ready = highlighter2);
|
|
79
73
|
const highlighter = await singleton;
|
|
80
74
|
await Promise.all(
|
|
81
75
|
languages.map(async (requested) => {
|
|
82
76
|
const language = normalizeCodeLanguage(requested);
|
|
83
|
-
if (language === "text" || highlighter.getLoadedLanguages().includes(language))
|
|
84
|
-
return;
|
|
77
|
+
if (language === "text" || highlighter.getLoadedLanguages().includes(language)) return;
|
|
85
78
|
let pending = loading.get(language);
|
|
86
79
|
if (!pending) {
|
|
87
80
|
const loader = bundledLanguages[language] ?? bundledLanguagesAlias[language];
|
|
@@ -105,15 +98,13 @@ function transformers(meta) {
|
|
|
105
98
|
},
|
|
106
99
|
line(node, line) {
|
|
107
100
|
node.properties["data-line"] = String(meta.startLine + line - 1);
|
|
108
|
-
if (meta.highlightedLines.includes(line))
|
|
109
|
-
this.addClassToHast(node, "highlighted");
|
|
101
|
+
if (meta.highlightedLines.includes(line)) this.addClassToHast(node, "highlighted");
|
|
110
102
|
}
|
|
111
103
|
}
|
|
112
104
|
];
|
|
113
105
|
}
|
|
114
106
|
function highlightCodeSync(code, info = "") {
|
|
115
|
-
if (!ready)
|
|
116
|
-
throw new Error("Call prepareCodeHighlighter before rendering Markdown");
|
|
107
|
+
if (!ready) throw new Error("Call prepareCodeHighlighter before rendering Markdown");
|
|
117
108
|
const meta = parseCodeMeta(info);
|
|
118
109
|
const normalizedLanguage = normalizeCodeLanguage(meta.language);
|
|
119
110
|
const language = ready.getLoadedLanguages().includes(normalizedLanguage) ? normalizedLanguage : "text";
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// src/components/Footprints/parse.ts
|
|
2
2
|
function parseFootprintsDatetime(meta) {
|
|
3
|
-
const m = meta.trim().match(
|
|
4
|
-
/^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/
|
|
5
|
-
);
|
|
3
|
+
const m = meta.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/);
|
|
6
4
|
if (!m) return [];
|
|
7
5
|
const parts = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
8
6
|
if (m[4] !== void 0) {
|
|
@@ -2,14 +2,8 @@ interface MindmapFenceOptions {
|
|
|
2
2
|
title?: string;
|
|
3
3
|
initialExpandLevel?: number;
|
|
4
4
|
}
|
|
5
|
-
interface MindmapReference {
|
|
6
|
-
path: string;
|
|
7
|
-
title?: string;
|
|
8
|
-
}
|
|
9
5
|
/** Parse the canonical `mindmap [title] 2` fence metadata. */
|
|
10
6
|
declare function parseMindmapFence(openLine: string): MindmapFenceOptions | null;
|
|
11
|
-
/** @deprecated Mindmap fences no longer resolve `<<<` includes; kept for body-include parsers / tests. */
|
|
12
|
-
declare function parseMindmapReference(line: string): MindmapReference | null;
|
|
13
7
|
interface NormalizeMindmapOptions {
|
|
14
8
|
title?: string;
|
|
15
9
|
defaultTitle?: string;
|
|
@@ -17,4 +11,4 @@ interface NormalizeMindmapOptions {
|
|
|
17
11
|
/** Ensure canonical mindmap Markdown has exactly one H1 root title. */
|
|
18
12
|
declare function normalizeMindmapMarkdown(source: string, options?: NormalizeMindmapOptions): string;
|
|
19
13
|
|
|
20
|
-
export { type MindmapFenceOptions, type
|
|
14
|
+
export { type MindmapFenceOptions, type NormalizeMindmapOptions, normalizeMindmapMarkdown, parseMindmapFence };
|
|
@@ -19,19 +19,6 @@ function parseMindmapFence(openLine) {
|
|
|
19
19
|
}
|
|
20
20
|
return options;
|
|
21
21
|
}
|
|
22
|
-
function parseMindmapReference(line) {
|
|
23
|
-
const match = line.trim().match(/^<<<\s+(.+?)\s*$/);
|
|
24
|
-
if (!match) return null;
|
|
25
|
-
let rest = match[1].trim();
|
|
26
|
-
let title;
|
|
27
|
-
const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/);
|
|
28
|
-
if (titleMatch) {
|
|
29
|
-
title = cleanHeadingText(titleMatch[1]) || void 0;
|
|
30
|
-
rest = rest.slice(0, titleMatch.index).trim();
|
|
31
|
-
}
|
|
32
|
-
const path = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, "$1$2").trim();
|
|
33
|
-
return path ? { path, title } : null;
|
|
34
|
-
}
|
|
35
22
|
function normalizeMindmapMarkdown(source, options = {}) {
|
|
36
23
|
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
37
24
|
let existingTitle = "";
|
|
@@ -55,6 +42,5 @@ ${body.join("\n")}
|
|
|
55
42
|
}
|
|
56
43
|
export {
|
|
57
44
|
normalizeMindmapMarkdown,
|
|
58
|
-
parseMindmapFence
|
|
59
|
-
parseMindmapReference
|
|
45
|
+
parseMindmapFence
|
|
60
46
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tnotesjs/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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": {
|
|
@@ -50,7 +50,17 @@
|
|
|
50
50
|
"./mermaid": "./src/entries/mermaid.ts",
|
|
51
51
|
"./mindmap": "./src/entries/mindmap.ts",
|
|
52
52
|
"./notes-table": "./src/entries/notes-table.ts",
|
|
53
|
-
"./word-list": "./src/entries/word-list.ts"
|
|
53
|
+
"./word-list": "./src/entries/word-list.ts",
|
|
54
|
+
"./excalidraw-view": {
|
|
55
|
+
"types": "./src/entries/excalidraw-view.ts",
|
|
56
|
+
"import": "./src/entries/excalidraw-view.ts",
|
|
57
|
+
"default": "./src/entries/excalidraw-view.ts"
|
|
58
|
+
},
|
|
59
|
+
"./excalidraw-editor": {
|
|
60
|
+
"types": "./src/entries/excalidraw-editor.ts",
|
|
61
|
+
"import": "./src/entries/excalidraw-editor.ts",
|
|
62
|
+
"default": "./src/entries/excalidraw-editor.ts"
|
|
63
|
+
}
|
|
54
64
|
},
|
|
55
65
|
"files": [
|
|
56
66
|
"dist",
|
|
@@ -65,15 +75,20 @@
|
|
|
65
75
|
"vue": "^3.5.0"
|
|
66
76
|
},
|
|
67
77
|
"dependencies": {
|
|
68
|
-
"@
|
|
78
|
+
"@excalidraw/excalidraw": "0.18.1",
|
|
69
79
|
"marked": "^15.0.11",
|
|
70
80
|
"mermaid": "^11.17.0",
|
|
71
|
-
"
|
|
81
|
+
"react": "18.3.1",
|
|
82
|
+
"react-dom": "18.3.1",
|
|
83
|
+
"sass": "^1.90.0",
|
|
84
|
+
"shiki": "4.4.2",
|
|
85
|
+
"@tnotesjs/mindmap-core": "^0.2.2"
|
|
72
86
|
},
|
|
73
87
|
"devDependencies": {
|
|
88
|
+
"@types/react": "^18.3.31",
|
|
89
|
+
"@types/react-dom": "^18.0.0",
|
|
74
90
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
75
91
|
"happy-dom": "^20.12.0",
|
|
76
|
-
"sass": "^1.90.0",
|
|
77
92
|
"tsup": "8.5.1",
|
|
78
93
|
"typescript": "^5.9.3",
|
|
79
94
|
"vitest": "^4.1.11",
|
|
@@ -88,11 +103,12 @@
|
|
|
88
103
|
},
|
|
89
104
|
"repository": {
|
|
90
105
|
"type": "git",
|
|
91
|
-
"url": "git+https://github.com/tnotesjs/
|
|
106
|
+
"url": "git+https://github.com/tnotesjs/tnotesjs.git",
|
|
107
|
+
"directory": "packages/ui"
|
|
92
108
|
},
|
|
93
|
-
"homepage": "https://github.com/tnotesjs/ui#readme",
|
|
109
|
+
"homepage": "https://github.com/tnotesjs/tnotesjs/tree/main/packages/ui#readme",
|
|
94
110
|
"bugs": {
|
|
95
|
-
"url": "https://github.com/tnotesjs/
|
|
111
|
+
"url": "https://github.com/tnotesjs/tnotesjs/issues"
|
|
96
112
|
},
|
|
97
113
|
"keywords": [
|
|
98
114
|
"tnotesjs",
|
package/src/browser/clipboard.ts
CHANGED
|
@@ -2,31 +2,28 @@
|
|
|
2
2
|
export async function copyText(text: string): Promise<void> {
|
|
3
3
|
try {
|
|
4
4
|
if (navigator.clipboard?.writeText) {
|
|
5
|
-
await navigator.clipboard.writeText(text)
|
|
6
|
-
return
|
|
5
|
+
await navigator.clipboard.writeText(text)
|
|
6
|
+
return
|
|
7
7
|
}
|
|
8
8
|
} catch {
|
|
9
9
|
/* use the synchronous user-gesture fallback */
|
|
10
10
|
}
|
|
11
|
-
const selection = document.getSelection()
|
|
11
|
+
const selection = document.getSelection()
|
|
12
12
|
const ranges = selection
|
|
13
|
-
? Array.from({ length: selection.rangeCount }, (_, i) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
field
|
|
20
|
-
field.style.cssText = "position:fixed;left:-9999px;opacity:0";
|
|
21
|
-
document.body.append(field);
|
|
13
|
+
? Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i))
|
|
14
|
+
: []
|
|
15
|
+
const active = document.activeElement
|
|
16
|
+
const field = document.createElement('textarea')
|
|
17
|
+
field.value = text
|
|
18
|
+
field.style.cssText = 'position:fixed;left:-9999px;opacity:0'
|
|
19
|
+
document.body.append(field)
|
|
22
20
|
try {
|
|
23
|
-
field.select()
|
|
24
|
-
if (!document.execCommand(
|
|
25
|
-
throw new Error("Clipboard access denied");
|
|
21
|
+
field.select()
|
|
22
|
+
if (!document.execCommand('copy')) throw new Error('Clipboard access denied')
|
|
26
23
|
} finally {
|
|
27
|
-
field.remove()
|
|
28
|
-
if (active instanceof HTMLElement) active.focus({ preventScroll: true })
|
|
29
|
-
selection?.removeAllRanges()
|
|
30
|
-
ranges.forEach((range) => selection?.addRange(range))
|
|
24
|
+
field.remove()
|
|
25
|
+
if (active instanceof HTMLElement) active.focus({ preventScroll: true })
|
|
26
|
+
selection?.removeAllRanges()
|
|
27
|
+
ranges.forEach((range) => selection?.addRange(range))
|
|
31
28
|
}
|
|
32
29
|
}
|
package/src/browser/theme.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
export type ThemePreference =
|
|
1
|
+
export type ThemePreference = 'light' | 'dark' | 'system'
|
|
2
2
|
|
|
3
3
|
/** Explicit data-theme wins; safe during static/SSR rendering. */
|
|
4
4
|
export function isDarkTheme(root?: HTMLElement): boolean {
|
|
5
|
-
root ??=
|
|
6
|
-
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
return root.dataset.theme === "dark" || root.classList.contains("dark");
|
|
5
|
+
root ??= typeof document === 'undefined' ? undefined : document.documentElement
|
|
6
|
+
if (!root) return false
|
|
7
|
+
if (root.dataset.theme === 'light') return false
|
|
8
|
+
return root.dataset.theme === 'dark' || root.classList.contains('dark')
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
export function applyTheme(
|
|
13
|
-
preference: ThemePreference,
|
|
14
|
-
root = document.documentElement,
|
|
15
|
-
): void {
|
|
11
|
+
export function applyTheme(preference: ThemePreference, root = document.documentElement): void {
|
|
16
12
|
const dark =
|
|
17
|
-
preference ===
|
|
18
|
-
(preference ===
|
|
19
|
-
|
|
20
|
-
root.
|
|
21
|
-
root.classList.toggle("dark", dark);
|
|
13
|
+
preference === 'dark' ||
|
|
14
|
+
(preference === 'system' && matchMedia('(prefers-color-scheme: dark)').matches)
|
|
15
|
+
root.dataset.theme = dark ? 'dark' : 'light'
|
|
16
|
+
root.classList.toggle('dark', dark)
|
|
22
17
|
}
|
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
highlightCode,
|
|
4
|
+
highlightCodeSync,
|
|
5
|
+
parseCodeMeta,
|
|
6
|
+
prepareCodeHighlighter
|
|
7
|
+
} from './highlight'
|
|
3
8
|
|
|
4
9
|
describe('shared code rendering', () => {
|
|
5
10
|
it('parses titles, line offsets, highlight ranges and per-block overrides', () => {
|
|
6
11
|
expect(parseCodeMeta('ts:line-numbers=12 {1,3-5} [demo.ts]')).toEqual({
|
|
7
|
-
language: 'ts',
|
|
12
|
+
language: 'ts',
|
|
13
|
+
title: 'demo.ts',
|
|
14
|
+
startLine: 12,
|
|
15
|
+
lineNumbers: true,
|
|
8
16
|
highlightedLines: [1, 3, 4, 5]
|
|
9
17
|
})
|
|
10
18
|
expect(parseCodeMeta('js {1-2} [setup.js]')).toEqual({
|
|
11
|
-
language: 'js',
|
|
19
|
+
language: 'js',
|
|
20
|
+
title: 'setup.js',
|
|
21
|
+
startLine: 1,
|
|
22
|
+
lineNumbers: true,
|
|
12
23
|
highlightedLines: [1, 2]
|
|
13
24
|
})
|
|
14
25
|
expect(parseCodeMeta('txt {3}')).toEqual({
|
|
15
|
-
language: 'txt',
|
|
26
|
+
language: 'txt',
|
|
27
|
+
title: '',
|
|
28
|
+
startLine: 1,
|
|
29
|
+
lineNumbers: true,
|
|
16
30
|
highlightedLines: [3]
|
|
17
31
|
})
|
|
18
32
|
expect(parseCodeMeta('text:no-line-numbers').lineNumbers).toBe(false)
|
|
19
33
|
expect(parseCodeMeta('text {0,-1,8-3,1-9999999}').highlightedLines).toEqual([])
|
|
20
34
|
})
|
|
21
35
|
it('shares one lazy engine and matches build-time/browser output byte for byte', async () => {
|
|
22
|
-
const [a, b] = await Promise.all([
|
|
36
|
+
const [a, b] = await Promise.all([
|
|
37
|
+
prepareCodeHighlighter(['js']),
|
|
38
|
+
prepareCodeHighlighter(['typescript'])
|
|
39
|
+
])
|
|
23
40
|
expect(a).toBe(b)
|
|
24
41
|
const source = 'const answer = 42\nconsole.log(answer)\n'
|
|
25
42
|
const runtime = await highlightCode(source, 'js {2}')
|
|
@@ -42,7 +59,7 @@ describe('shared code rendering', () => {
|
|
|
42
59
|
it('applies fence line highlights and offsets without Shiki notation', async () => {
|
|
43
60
|
const withNotation = await highlightCode(
|
|
44
61
|
'const x = 1 // [!code ++]\nconst y = 2 // [!code focus]',
|
|
45
|
-
'js:line-numbers=5 {2}'
|
|
62
|
+
'js:line-numbers=5 {2}'
|
|
46
63
|
)
|
|
47
64
|
// Magic comments stay as source text; Desk has no annotation write path.
|
|
48
65
|
expect(withNotation).toContain('[!code ++]')
|