@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
package/README.md CHANGED
@@ -1,120 +1,53 @@
1
1
  # @tnotesjs/ui
2
2
 
3
- Shared Vue UI for TNotes built-in blocks. Consumed by:
3
+ TNotes 内置块的 Vue 组件。消费方:`@tnotesjs/ssg`、Desk。需要 `vue` `^3.5`。样式用 `--tn-*`(`src/styles/tokens.css`)。
4
4
 
5
- - `@tnotesjs/core` VitePress theme (`tn:dev`)
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
- Requires `vue` `^3.5` (peer).
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
- ## Components
42
-
43
- ### Shared rendering contract
44
-
45
- Core and Desk import the same token, prose, code-block, and code-group implementation. The
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` — `<WordList :words="[…]" />`
23
+ ### `WordList`
79
24
 
80
- | Prop | Default | Notes |
81
- | ----------------- | ------------------------- | ---------------------------- |
82
- | `words` | `[]` | Word strings |
83
- | `needSort` | `false` | Sort A→Z by first letter |
84
- | `wordsBaseUrl` | en-words blob URL | Optional host override |
85
- | `wordsRawBaseUrl` | en-words raw URL | Optional host override |
86
- | `features` | `WORD_LIST_FEATURES_FULL` | Capability flags (see below) |
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
- **Features presets** (no consumer/`host` field — capability-based):
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
- Shared diagram preview for core (`tn:dev`) and Desk. Markdown fence language: `mermaid`.
102
-
103
- | Prop | Default | Notes |
104
- | -------------------- | --------- | ------------------------------------------------ |
105
- | `source` | `''` | Plain Mermaid text (Desk) |
106
- | `graph` | `''` | URI-encoded source (VitePress fence) |
107
- | `id` | auto | Mermaid render id |
108
- | `center` | `false` | From fence keyword `center`; omit → not centered |
109
- | `isDark` | auto | Or pass explicitly |
110
- | `securityLevel` | `'loose'` | Same default for Desk and site preview |
111
- | `enableCopy` | `true` | Hover copy action |
112
- | `enableFullscreen` | `true` | Hover fullscreen action |
113
- | `enableCenterToggle` | `true` | Hover center toggle (left of fullscreen) |
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` |
@@ -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 MindmapReference, type NormalizeMindmapOptions, normalizeMindmapMarkdown, parseMindmapFence, parseMindmapReference };
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.1.5",
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
- "@tnotesjs/mindmap-core": "^0.2.2",
78
+ "@excalidraw/excalidraw": "0.18.1",
69
79
  "marked": "^15.0.11",
70
80
  "mermaid": "^11.17.0",
71
- "shiki": "4.4.2"
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/ui.git"
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/ui/issues"
111
+ "url": "https://github.com/tnotesjs/tnotesjs/issues"
96
112
  },
97
113
  "keywords": [
98
114
  "tnotesjs",
@@ -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
- selection.getRangeAt(i),
15
- )
16
- : [];
17
- const active = document.activeElement;
18
- const field = document.createElement("textarea");
19
- field.value = text;
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("copy"))
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
  }
@@ -1,22 +1,17 @@
1
- export type ThemePreference = "light" | "dark" | "system";
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
- typeof document === "undefined" ? undefined : document.documentElement;
7
- if (!root) return false;
8
- if (root.dataset.theme === "light") return false;
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 === "dark" ||
18
- (preference === "system" &&
19
- matchMedia("(prefers-color-scheme: dark)").matches);
20
- root.dataset.theme = dark ? "dark" : "light";
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 { highlightCode, highlightCodeSync, parseCodeMeta, prepareCodeHighlighter } from './highlight'
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', title: 'demo.ts', startLine: 12, lineNumbers: true,
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', title: 'setup.js', startLine: 1, lineNumbers: true,
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', title: '', startLine: 1, lineNumbers: true,
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([prepareCodeHighlighter(['js']), prepareCodeHighlighter(['typescript'])])
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 ++]')