@tnotesjs/ui 0.1.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +68 -0
  4. package/src/components/BilibiliVideo/BilibiliVideo.vue +55 -0
  5. package/src/components/Footprints/Footprints.vue +339 -0
  6. package/src/components/Footprints/parse.ts +122 -0
  7. package/src/components/Mermaid/Mermaid.vue +553 -0
  8. package/src/components/Mermaid/icons/icon__center_off.svg +1 -0
  9. package/src/components/Mermaid/icons/icon__center_on.svg +1 -0
  10. package/src/components/Mermaid/icons/icon__check.svg +3 -0
  11. package/src/components/Mermaid/icons/icon__clipboard.svg +8 -0
  12. package/src/components/Mermaid/icons/icon__fullscreen.svg +1 -0
  13. package/src/components/Mermaid/icons/icon__fullscreen_exit.svg +1 -0
  14. package/src/components/Mindmap/FocusBreadcrumbs.test.ts +265 -0
  15. package/src/components/Mindmap/FocusBreadcrumbs.vue +436 -0
  16. package/src/components/Mindmap/InlineRuns.ts +25 -0
  17. package/src/components/Mindmap/Mindmap.vue +1210 -0
  18. package/src/components/Mindmap/MindmapOutlineNode.vue +62 -0
  19. package/src/components/Mindmap/MindmapViewIcon.vue +41 -0
  20. package/src/components/Mindmap/editor/AppIcon.vue +107 -0
  21. package/src/components/Mindmap/editor/CanvasContextMenu.vue +157 -0
  22. package/src/components/Mindmap/editor/LinkPopover.vue +83 -0
  23. package/src/components/Mindmap/editor/MarkdownView.vue +163 -0
  24. package/src/components/Mindmap/editor/MindmapView.vue +253 -0
  25. package/src/components/Mindmap/editor/OutlineView.vue +2494 -0
  26. package/src/components/Mindmap/editor/RichInlineEditor.vue +393 -0
  27. package/src/components/Mindmap/editor/SelectionToolbar.vue +191 -0
  28. package/src/components/Mindmap/editor/canvasClipboard.ts +6 -0
  29. package/src/components/Mindmap/editor/imagePaste.ts +32 -0
  30. package/src/components/Mindmap/editor/mindmapClipboard.ts +93 -0
  31. package/src/components/Mindmap/editor/outlineDrag.ts +29 -0
  32. package/src/components/Mindmap/editor/platform.ts +18 -0
  33. package/src/components/Mindmap/expandLevel.ts +28 -0
  34. package/src/components/Mindmap/icons/icon__fullscreen.svg +1 -0
  35. package/src/components/Mindmap/icons/icon__fullscreen_exit.svg +1 -0
  36. package/src/components/Mindmap/icons/icon__zoom_fit.svg +1 -0
  37. package/src/components/Mindmap/markdown.ts +83 -0
  38. package/src/components/Mindmap/wheelInteraction.ts +7 -0
  39. package/src/components/NotesTable/NotesTable.vue +119 -0
  40. package/src/components/NotesTable/types.ts +6 -0
  41. package/src/components/WordList/RightClickMenu.vue +106 -0
  42. package/src/components/WordList/WordList.vue +692 -0
  43. package/src/components/WordList/wordListFeatures.ts +38 -0
  44. package/src/index.ts +30 -0
  45. package/src/styles/tokens.css +35 -0
@@ -0,0 +1,93 @@
1
+ import { cloneSubtree, parseMarkdown } from '@tnotesjs/mindmap-core'
2
+ import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
3
+
4
+ const LIST_LINE_RE = /^\s*[-*+]\s+/
5
+
6
+ /** In-app fallback when Electron denies async Clipboard API (common in Desk). */
7
+ let mindmapClipboardBuffer = ''
8
+
9
+ function clipboardNodes(text: string): MindmapNode[] {
10
+ const fragment = text
11
+ .split(/\r?\n/)
12
+ .filter((line) => line.trim() !== '')
13
+ .map((line) => {
14
+ if (LIST_LINE_RE.test(line)) return line
15
+ const match = /^(\s*)(.*)$/.exec(line)!
16
+ return `${match[1]}- ${match[2]}`
17
+ })
18
+ .join('\n')
19
+ if (!fragment) return []
20
+ const parsed = parseMarkdown(`# _\n\n${fragment}\n`)
21
+ if (!parsed.valid) return []
22
+ return parsed.doc.root.children.map((node) => cloneSubtree(node))
23
+ }
24
+
25
+ /** Sync write that survives Electron's missing clipboard permission. */
26
+ export function writeMindmapClipboard(text: string, event?: ClipboardEvent | null): void {
27
+ mindmapClipboardBuffer = text
28
+ if (event?.clipboardData) {
29
+ event.clipboardData.setData('text/plain', text)
30
+ return
31
+ }
32
+ try {
33
+ const textarea = document.createElement('textarea')
34
+ textarea.value = text
35
+ textarea.setAttribute('readonly', '')
36
+ textarea.style.position = 'fixed'
37
+ textarea.style.left = '-9999px'
38
+ textarea.style.top = '0'
39
+ textarea.style.opacity = '0'
40
+ document.body.append(textarea)
41
+ textarea.focus()
42
+ textarea.select()
43
+ document.execCommand('copy')
44
+ textarea.remove()
45
+ } catch {
46
+ // fall through to async API
47
+ }
48
+ void navigator.clipboard?.writeText(text).catch(() => {
49
+ // Buffer above still enables in-app Cmd+V.
50
+ })
51
+ }
52
+
53
+ export async function readMindmapClipboard(event?: ClipboardEvent | null): Promise<string> {
54
+ const fromEvent = event?.clipboardData?.getData('text/plain') ?? ''
55
+ if (fromEvent.trim()) {
56
+ mindmapClipboardBuffer = fromEvent
57
+ return fromEvent
58
+ }
59
+ try {
60
+ const fromApi = (await navigator.clipboard?.readText?.()) ?? ''
61
+ if (fromApi.trim()) {
62
+ mindmapClipboardBuffer = fromApi
63
+ return fromApi
64
+ }
65
+ } catch {
66
+ // Electron often denies readText without an explicit permission grant.
67
+ }
68
+ return mindmapClipboardBuffer
69
+ }
70
+
71
+ /** 把剪贴板中的普通文字 / Markdown 列表作为当前主题后的同级子树插入。 */
72
+ export function pasteCanvasOutline(session: MindmapSession, anchorId: string, text: string): string[] {
73
+ const nodes = clipboardNodes(text)
74
+ const anchor = session.document.find(anchorId)
75
+ if (!anchor || nodes.length === 0) return []
76
+ const insertedIds: string[] = []
77
+ session.transact((doc) => {
78
+ const parent = anchor === session.focusRootNode ? anchor : (anchor.parent ?? session.focusRootNode)
79
+ let index = anchor === session.focusRootNode ? parent.children.length : parent.children.indexOf(anchor) + 1
80
+ for (const source of nodes) {
81
+ const inserted = doc.addNode(
82
+ parent,
83
+ { ...source.content, image: source.content.image ? { ...source.content.image } : null },
84
+ index++,
85
+ )
86
+ inserted.collapsed = source.collapsed
87
+ for (const child of [...source.children]) doc.move(child, inserted, inserted.children.length)
88
+ insertedIds.push(inserted.id)
89
+ }
90
+ })
91
+ session.selectMany(insertedIds, insertedIds[insertedIds.length - 1] ?? null, insertedIds[0] ?? null)
92
+ return insertedIds
93
+ }
@@ -0,0 +1,29 @@
1
+ import type { MindmapNode } from '@tnotesjs/mindmap-core'
2
+
3
+ export interface DropLevel {
4
+ node: MindmapNode
5
+ depth: number
6
+ }
7
+
8
+ /**
9
+ * 根据横向指针位置解析“插到哪个祖先之后”。
10
+ * 每越过一条缩进线就提升一级;是否为末子不影响提升,否则普通兄弟无法拖回顶层。
11
+ */
12
+ export function resolveAfterDropLevel(
13
+ anchor: MindmapNode,
14
+ focusRoot: MindmapNode,
15
+ clientX: number,
16
+ indentEdge: number,
17
+ depth: number,
18
+ indent: number,
19
+ ): DropLevel {
20
+ let node = anchor
21
+ let nextDepth = depth
22
+ let edge = indentEdge
23
+ while (node.parent && node.parent !== focusRoot && clientX <= edge && nextDepth > 0) {
24
+ node = node.parent
25
+ nextDepth -= 1
26
+ edge -= indent
27
+ }
28
+ return { node, depth: nextDepth }
29
+ }
@@ -0,0 +1,18 @@
1
+ function platformName(): string {
2
+ if (typeof navigator === 'undefined') return ''
3
+ const data = navigator as Navigator & { userAgentData?: { platform?: string } }
4
+ return `${data.userAgentData?.platform ?? ''} ${navigator.platform ?? ''} ${navigator.userAgent ?? ''}`.toLowerCase()
5
+ }
6
+
7
+ export function isApplePlatform(): boolean {
8
+ return /mac|iphone|ipad|ipod/.test(platformName())
9
+ }
10
+
11
+ export function primaryShortcut(key: string, options: { shift?: boolean } = {}): string {
12
+ if (isApplePlatform()) return `${options.shift ? '⇧' : ''}⌘${key}`
13
+ return `Ctrl+${options.shift ? 'Shift+' : ''}${key}`
14
+ }
15
+
16
+ export function altShortcut(key: string): string {
17
+ return isApplePlatform() ? `⌥${key}` : `Alt+${key}`
18
+ }
@@ -0,0 +1,28 @@
1
+ import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
2
+
3
+ export function normalizeExpandLevel(value: number): number {
4
+ return Math.max(1, Math.trunc(Number(value) || 1))
5
+ }
6
+
7
+ function childLevel(node: MindmapNode, root: MindmapNode): number {
8
+ let level = 0
9
+ let current: MindmapNode | null = node
10
+ while (current && current !== root) {
11
+ level += 1
12
+ current = current.parent
13
+ }
14
+ return level
15
+ }
16
+
17
+ /**
18
+ * Level 1 renders root + direct children; level 2 additionally renders
19
+ * grandchildren. Nodes at the last visible level are collapsed.
20
+ */
21
+ export function applyInitialExpandLevel(session: MindmapSession, value: number): void {
22
+ const visibleLevel = normalizeExpandLevel(value)
23
+ const root = session.document.root
24
+ session.document.traverse((node) => {
25
+ if (node === root || node.children.length === 0) return
26
+ session.setCollapsed(node.id, childLevel(node, root) >= visibleLevel)
27
+ })
28
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none"><path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="#646cff" d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586zM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#646cff" d="m10 15.4l-5.9 5.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7L8.6 14H5q-.425 0-.712-.288T4 13t.288-.712T5 12h6q.425 0 .713.288T12 13v6q0 .425-.288.713T11 20t-.712-.288T10 19zm5.4-5.4H19q.425 0 .713.288T20 11t-.288.713T19 12h-6q-.425 0-.712-.288T12 11V5q0-.425.288-.712T13 4t.713.288T14 5v3.6l5.9-5.9q.275-.275.7-.275t.7.275t.275.7t-.275.7z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="#646cff" d="M17 4h3c1.1 0 2 .9 2 2v2h-2V6h-3zM4 8V6h3V4H4c-1.1 0-2 .9-2 2v2zm16 8v2h-3v2h3c1.1 0 2-.9 2-2v-2zM7 18H4v-2H2v2c0 1.1.9 2 2 2h3zm9-8v4H8v-4zm2-2H6v8h12z"/></svg>
@@ -0,0 +1,83 @@
1
+ export interface MindmapFenceOptions {
2
+ title?: string
3
+ initialExpandLevel?: number
4
+ }
5
+
6
+ export interface MindmapReference {
7
+ path: string
8
+ title?: string
9
+ }
10
+
11
+ function cleanHeadingText(value: string): string {
12
+ return value.trim().replace(/\s+#+\s*$/, '').trim()
13
+ }
14
+
15
+ /** Parse the canonical `mindmap [title] 2` fence metadata. */
16
+ export function parseMindmapFence(openLine: string): MindmapFenceOptions | null {
17
+ const fenceBody = openLine.trim().replace(/^`+\s*/, '')
18
+ const nameMatch = fenceBody.match(/^mindmap(?=\s|\[|$)/)
19
+ if (!nameMatch) return null
20
+
21
+ let rest = fenceBody.slice(nameMatch[0].length).trim()
22
+ const options: MindmapFenceOptions = {}
23
+ const titleMatch = rest.match(/\[([^\]]+)\]/)
24
+ if (titleMatch) {
25
+ options.title = cleanHeadingText(titleMatch[1]) || undefined
26
+ rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
27
+ }
28
+
29
+ if (rest && !/^\d+$/.test(rest)) return null
30
+ if (rest) {
31
+ options.initialExpandLevel = Math.max(1, Number(rest))
32
+ }
33
+ return options
34
+ }
35
+
36
+ /** @deprecated Mindmap fences no longer resolve `<<<` includes; kept for body-include parsers / tests. */
37
+ export function parseMindmapReference(line: string): MindmapReference | null {
38
+ const match = line.trim().match(/^<<<\s+(.+?)\s*$/)
39
+ if (!match) return null
40
+
41
+ let rest = match[1].trim()
42
+ let title: string | undefined
43
+ const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/)
44
+ if (titleMatch) {
45
+ title = cleanHeadingText(titleMatch[1]) || undefined
46
+ rest = rest.slice(0, titleMatch.index).trim()
47
+ }
48
+
49
+ const path = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2').trim()
50
+ return path ? { path, title } : null
51
+ }
52
+
53
+ export interface NormalizeMindmapOptions {
54
+ title?: string
55
+ defaultTitle?: string
56
+ }
57
+
58
+ /** Ensure canonical mindmap Markdown has exactly one H1 root title. */
59
+ export function normalizeMindmapMarkdown(
60
+ source: string,
61
+ options: NormalizeMindmapOptions = {},
62
+ ): string {
63
+ const lines = source.replace(/\r\n?/g, '\n').split('\n')
64
+ let existingTitle = ''
65
+ let rootIndex = -1
66
+
67
+ for (let index = 0; index < lines.length; index++) {
68
+ const match = lines[index].match(/^\s{0,3}#(?!#)\s+(.+?)\s*$/)
69
+ if (!match) continue
70
+ existingTitle = cleanHeadingText(match[1])
71
+ rootIndex = index
72
+ break
73
+ }
74
+
75
+ const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || 'root') || 'root'
76
+ const body = lines.filter((_, index) => index !== rootIndex)
77
+ while (body[0]?.trim() === '') body.shift()
78
+ while (body[body.length - 1]?.trim() === '') body.pop()
79
+
80
+ return body.length > 0
81
+ ? `# ${rootTitle}\n\n${body.join('\n')}\n`
82
+ : `# ${rootTitle}\n`
83
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * CanvasViewer 会在自身 wheel 监听器中阻止默认事件并移动画布。
3
+ * 未激活时先在捕获阶段终止分发,但不 preventDefault,让滚轮继续交给文档页面。
4
+ */
5
+ export function gateMindmapWheel(event: WheelEvent, active: boolean): void {
6
+ if (!active) event.stopImmediatePropagation()
7
+ }
@@ -0,0 +1,119 @@
1
+ <script setup lang="ts">
2
+ import type { NotesTableRow } from './types'
3
+
4
+ defineProps<{
5
+ notes?: NotesTableRow[]
6
+ missingIds?: string[]
7
+ error?: string | null
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <div v-if="error" class="tn-notes-table__error">
13
+ {{ error }}
14
+ </div>
15
+
16
+ <div v-else-if="(missingIds?.length ?? 0) > 0" class="tn-notes-table__warning">
17
+ 以下笔记 ID 未找到配置: {{ missingIds!.join(', ') }}
18
+ </div>
19
+
20
+ <table v-if="(notes?.length ?? 0) > 0" class="tn-notes-table">
21
+ <thead>
22
+ <tr>
23
+ <th>笔记</th>
24
+ <th>简介</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <tr v-for="note in notes" :key="note.id">
29
+ <td>
30
+ <a :href="note.url" class="tn-notes-table__link">
31
+ <span class="tn-notes-table__id">{{ note.id }}.</span>
32
+ <span>{{ note.title }}</span>
33
+ </a>
34
+ </td>
35
+ <td>
36
+ <span class="tn-notes-table__desc" :class="{ 'is-empty': !note.description }">
37
+ {{ note.description || '暂无简介' }}
38
+ </span>
39
+ </td>
40
+ </tr>
41
+ </tbody>
42
+ </table>
43
+ </template>
44
+
45
+ <style scoped lang="scss">
46
+ .tn-notes-table {
47
+ width: 100%;
48
+ margin: 1.5rem 0;
49
+ border-collapse: collapse;
50
+ font-size: 0.95rem;
51
+
52
+ th,
53
+ td {
54
+ padding: 0.75rem 1rem;
55
+ text-align: left;
56
+ border: 1px solid var(--tn-c-divider);
57
+ }
58
+
59
+ th {
60
+ background-color: var(--tn-c-bg-soft);
61
+ font-weight: 600;
62
+ color: var(--tn-c-text);
63
+ }
64
+
65
+ tbody tr {
66
+ transition: background-color 0.2s;
67
+
68
+ &:hover {
69
+ background-color: var(--tn-c-bg-soft);
70
+ }
71
+ }
72
+
73
+ td {
74
+ color: var(--tn-c-text-2);
75
+ }
76
+ }
77
+
78
+ .tn-notes-table__link {
79
+ color: var(--tn-c-brand);
80
+ text-decoration: none;
81
+ font-weight: 500;
82
+
83
+ &:hover {
84
+ text-decoration: underline;
85
+ }
86
+ }
87
+
88
+ .tn-notes-table__id {
89
+ margin-right: 0.5rem;
90
+ font-family: var(--tn-font-mono);
91
+ font-size: 0.9em;
92
+ color: var(--tn-c-text-2);
93
+ }
94
+
95
+ .tn-notes-table__desc {
96
+ line-height: 1.6;
97
+
98
+ &.is-empty {
99
+ color: var(--tn-c-text-2);
100
+ font-style: italic;
101
+ }
102
+ }
103
+
104
+ .tn-notes-table__error,
105
+ .tn-notes-table__warning {
106
+ margin: 1rem 0;
107
+ padding: 1rem;
108
+ border-radius: 4px;
109
+ border-left: 4px solid var(--tn-c-danger);
110
+ background-color: var(--tn-c-danger-soft);
111
+ color: var(--tn-c-danger);
112
+ }
113
+
114
+ .tn-notes-table__warning {
115
+ border-left-color: #f9b44e;
116
+ background-color: color-mix(in srgb, #f9b44e 16%, transparent);
117
+ color: #f9b44e;
118
+ }
119
+ </style>
@@ -0,0 +1,6 @@
1
+ export interface NotesTableRow {
2
+ id: string
3
+ title: string
4
+ description: string
5
+ url: string
6
+ }
@@ -0,0 +1,106 @@
1
+ <template>
2
+ <div
3
+ v-if="show"
4
+ class="rightClickMenu"
5
+ :style="{ left: x + 'px', top: y + 'px' }"
6
+ >
7
+ <div v-if="showPin" class="menuItem" @click="handlePin">📌 Pin</div>
8
+ <div class="menuItem" @click="(e) => handlePronounce(e, 'en-GB')">
9
+ 📢 Pronounce(英)
10
+ </div>
11
+ <div class="menuItem" @click="(e) => handlePronounce(e, 'en-US')">
12
+ 📢 Pronounce(美)
13
+ </div>
14
+ <div
15
+ class="menuItem"
16
+ @click="(e) => handlePronounceAll(e, 'en-GB')"
17
+ >
18
+ 📢 Pronounce All(英)
19
+ </div>
20
+ <div
21
+ class="menuItem"
22
+ @click="(e) => handlePronounceAll(e, 'en-US')"
23
+ >
24
+ 📢 Pronounce All(美)
25
+ </div>
26
+ <div
27
+ v-if="showAutoShowCard"
28
+ class="menuItem"
29
+ @click="handleAutoShowCard"
30
+ >
31
+ 🔍 Auto Show Card({{ isAutoShowCard ? '关' : '开' }})
32
+ </div>
33
+ <div class="menuItem" @click="handleCheckAll">✅ Check All</div>
34
+ <div class="menuItem" @click="handleReset">❌ Reset</div>
35
+ </div>
36
+ </template>
37
+
38
+ <script setup>
39
+ defineProps({
40
+ show: Boolean,
41
+ isAutoShowCard: Boolean,
42
+ x: Number,
43
+ y: Number,
44
+ showPin: {
45
+ type: Boolean,
46
+ default: true,
47
+ },
48
+ showAutoShowCard: {
49
+ type: Boolean,
50
+ default: true,
51
+ },
52
+ })
53
+
54
+ const emit = defineEmits([
55
+ 'pin',
56
+ 'pronounce',
57
+ 'pronounceAll',
58
+ 'autoShowCard',
59
+ 'checkAll',
60
+ 'reset',
61
+ ])
62
+ const handlePin = (e) => {
63
+ emit('pin')
64
+ e.preventDefault()
65
+ }
66
+ const handleAutoShowCard = () => {
67
+ emit('autoShowCard')
68
+ }
69
+ const handlePronounce = (e, lang) => {
70
+ emit('pronounce', lang)
71
+ e.preventDefault()
72
+ }
73
+ const handlePronounceAll = (e, lang) => {
74
+ emit('pronounceAll', lang)
75
+ e.preventDefault()
76
+ }
77
+ const handleCheckAll = () => {
78
+ emit('checkAll')
79
+ }
80
+ const handleReset = () => {
81
+ emit('reset')
82
+ }
83
+ </script>
84
+
85
+ <style scoped lang="scss">
86
+ .rightClickMenu {
87
+ position: fixed;
88
+ z-index: 99999;
89
+ background: #2c2c2c;
90
+ border: 1px solid #444;
91
+ border-radius: 6px;
92
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
93
+ font-size: 13px;
94
+ color: #eee;
95
+ cursor: pointer;
96
+ user-select: none;
97
+ }
98
+
99
+ .menuItem {
100
+ padding: 8px 16px;
101
+
102
+ &:hover {
103
+ background-color: #444;
104
+ }
105
+ }
106
+ </style>