quasar-ui-danx 0.4.99 → 0.5.1

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 (90) hide show
  1. package/dist/danx.es.js +17884 -12732
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +192 -118
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +11 -2
  7. package/scripts/publish.sh +76 -0
  8. package/src/components/Utility/Code/CodeViewer.vue +31 -14
  9. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  10. package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
  11. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  12. package/src/components/Utility/Code/MarkdownContent.vue +160 -6
  13. package/src/components/Utility/Code/index.ts +3 -0
  14. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  15. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  16. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  17. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  18. package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
  19. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
  20. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  21. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  22. package/src/components/Utility/Markdown/index.ts +11 -0
  23. package/src/components/Utility/Markdown/types.ts +27 -0
  24. package/src/components/Utility/index.ts +1 -0
  25. package/src/composables/index.ts +1 -0
  26. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  27. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  28. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  29. package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
  30. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  31. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  32. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  33. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  34. package/src/composables/markdown/features/useHeadings.ts +290 -0
  35. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  36. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  37. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  38. package/src/composables/markdown/features/useLinks.spec.ts +369 -0
  39. package/src/composables/markdown/features/useLinks.ts +374 -0
  40. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  41. package/src/composables/markdown/features/useLists.ts +747 -0
  42. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  43. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  44. package/src/composables/markdown/features/useTables.ts +1107 -0
  45. package/src/composables/markdown/index.ts +16 -0
  46. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  47. package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
  48. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  49. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  50. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  51. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  52. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  53. package/src/composables/useCodeViewerEditor.ts +174 -20
  54. package/src/helpers/formats/index.ts +1 -1
  55. package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
  56. package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
  57. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  58. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  59. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
  60. package/src/helpers/formats/markdown/index.ts +92 -0
  61. package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
  62. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  63. package/src/helpers/formats/markdown/parseInline.ts +124 -0
  64. package/src/helpers/formats/markdown/render/index.ts +92 -0
  65. package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
  66. package/src/helpers/formats/markdown/render/renderList.ts +69 -0
  67. package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
  68. package/src/helpers/formats/markdown/state.ts +58 -0
  69. package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
  70. package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
  71. package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
  72. package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
  73. package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
  74. package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
  75. package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
  76. package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
  77. package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
  78. package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
  79. package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
  80. package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
  81. package/src/helpers/formats/markdown/types.ts +63 -0
  82. package/src/styles/danx.scss +1 -0
  83. package/src/styles/themes/danx/markdown.scss +96 -0
  84. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  85. package/src/test/helpers/editorTestUtils.ts +253 -0
  86. package/src/test/helpers/index.ts +1 -0
  87. package/src/test/setup.test.ts +12 -0
  88. package/src/test/setup.ts +12 -0
  89. package/vitest.config.ts +19 -0
  90. package/src/helpers/formats/renderMarkdown.ts +0 -338
@@ -44,8 +44,29 @@
44
44
  <li
45
45
  v-for="(item, itemIndex) in token.items"
46
46
  :key="itemIndex"
47
- v-html="parseInlineContent(item)"
48
- />
47
+ >
48
+ <span v-html="parseInlineContent(item.content)" />
49
+ <template v-if="item.children && item.children.length > 0">
50
+ <template v-for="(child, childIndex) in item.children" :key="'child-' + childIndex">
51
+ <!-- Nested unordered list -->
52
+ <ul v-if="child.type === 'ul'">
53
+ <li
54
+ v-for="(nestedItem, nestedIndex) in child.items"
55
+ :key="nestedIndex"
56
+ v-html="renderListItem(nestedItem)"
57
+ />
58
+ </ul>
59
+ <!-- Nested ordered list -->
60
+ <ol v-else-if="child.type === 'ol'" :start="child.start">
61
+ <li
62
+ v-for="(nestedItem, nestedIndex) in child.items"
63
+ :key="nestedIndex"
64
+ v-html="renderListItem(nestedItem)"
65
+ />
66
+ </ol>
67
+ </template>
68
+ </template>
69
+ </li>
49
70
  </ul>
50
71
 
51
72
  <!-- Ordered lists -->
@@ -56,10 +77,86 @@
56
77
  <li
57
78
  v-for="(item, itemIndex) in token.items"
58
79
  :key="itemIndex"
59
- v-html="parseInlineContent(item)"
60
- />
80
+ >
81
+ <span v-html="parseInlineContent(item.content)" />
82
+ <template v-if="item.children && item.children.length > 0">
83
+ <template v-for="(child, childIndex) in item.children" :key="'child-' + childIndex">
84
+ <!-- Nested unordered list -->
85
+ <ul v-if="child.type === 'ul'">
86
+ <li
87
+ v-for="(nestedItem, nestedIndex) in child.items"
88
+ :key="nestedIndex"
89
+ v-html="renderListItem(nestedItem)"
90
+ />
91
+ </ul>
92
+ <!-- Nested ordered list -->
93
+ <ol v-else-if="child.type === 'ol'" :start="child.start">
94
+ <li
95
+ v-for="(nestedItem, nestedIndex) in child.items"
96
+ :key="nestedIndex"
97
+ v-html="renderListItem(nestedItem)"
98
+ />
99
+ </ol>
100
+ </template>
101
+ </template>
102
+ </li>
61
103
  </ol>
62
104
 
105
+ <!-- Task lists -->
106
+ <ul
107
+ v-else-if="token.type === 'task_list'"
108
+ class="task-list"
109
+ >
110
+ <li
111
+ v-for="(item, itemIndex) in token.items"
112
+ :key="itemIndex"
113
+ class="task-list-item"
114
+ >
115
+ <input
116
+ type="checkbox"
117
+ :checked="item.checked"
118
+ disabled
119
+ />
120
+ <span v-html="parseInlineContent(item.content)" />
121
+ </li>
122
+ </ul>
123
+
124
+ <!-- Tables -->
125
+ <table v-else-if="token.type === 'table'">
126
+ <thead>
127
+ <tr>
128
+ <th
129
+ v-for="(header, hIndex) in token.headers"
130
+ :key="hIndex"
131
+ :style="token.alignments[hIndex] ? { textAlign: token.alignments[hIndex] } : {}"
132
+ v-html="parseInlineContent(header)"
133
+ />
134
+ </tr>
135
+ </thead>
136
+ <tbody>
137
+ <tr v-for="(row, rIndex) in token.rows" :key="rIndex">
138
+ <td
139
+ v-for="(cell, cIndex) in row"
140
+ :key="cIndex"
141
+ :style="token.alignments[cIndex] ? { textAlign: token.alignments[cIndex] } : {}"
142
+ v-html="parseInlineContent(cell)"
143
+ />
144
+ </tr>
145
+ </tbody>
146
+ </table>
147
+
148
+ <!-- Definition lists -->
149
+ <dl v-else-if="token.type === 'dl'">
150
+ <template v-for="(item, itemIndex) in token.items" :key="itemIndex">
151
+ <dt v-html="parseInlineContent(item.term)" />
152
+ <dd
153
+ v-for="(def, defIndex) in item.definitions"
154
+ :key="'def-' + defIndex"
155
+ v-html="parseInlineContent(def)"
156
+ />
157
+ </template>
158
+ </dl>
159
+
63
160
  <!-- Horizontal rules -->
64
161
  <hr v-else-if="token.type === 'hr'" />
65
162
 
@@ -69,13 +166,30 @@
69
166
  v-html="parseInlineContent(token.content).replace(/\n/g, '<br />')"
70
167
  />
71
168
  </template>
169
+
170
+ <!-- Footnotes section -->
171
+ <section v-if="hasFootnotes" class="footnotes">
172
+ <hr />
173
+ <ol class="footnote-list">
174
+ <li
175
+ v-for="fn in sortedFootnotes"
176
+ :key="fn.id"
177
+ :id="'fn-' + fn.id"
178
+ class="footnote-item"
179
+ >
180
+ <span v-html="parseInlineContent(fn.content)" />
181
+ <a :href="'#fnref-' + fn.id" class="footnote-backref">&#8617;</a>
182
+ </li>
183
+ </ol>
184
+ </section>
72
185
  </div>
73
186
  </template>
74
187
 
75
188
  <script setup lang="ts">
76
189
  import { computed, reactive } from "vue";
77
190
  import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
78
- import { tokenizeBlocks, parseInline, renderMarkdown, BlockToken } from "../../../helpers/formats/renderMarkdown";
191
+ import { tokenizeBlocks, parseInline, renderMarkdown, getFootnotes, resetParserState } from "../../../helpers/formats/markdown";
192
+ import type { BlockToken, ListItem } from "../../../helpers/formats/markdown";
79
193
  import { highlightJSON, highlightYAML } from "../../../helpers/formats/highlightSyntax";
80
194
  import LanguageBadge from "./LanguageBadge.vue";
81
195
 
@@ -96,9 +210,30 @@ const convertedContent = reactive<Record<number, string>>({});
96
210
  // Tokenize the markdown content
97
211
  const tokens = computed<BlockToken[]>(() => {
98
212
  if (!props.content) return [];
213
+ // Reset parser state before tokenizing to clear refs from previous content
214
+ // This ensures link refs and footnotes are freshly parsed for this content
215
+ resetParserState();
99
216
  return tokenizeBlocks(props.content);
100
217
  });
101
218
 
219
+ // Computed properties for footnotes
220
+ // IMPORTANT: Access tokens.value to ensure tokenizeBlocks runs first,
221
+ // which populates currentFootnotes and currentLinkRefs
222
+ const footnotes = computed(() => {
223
+ // Force dependency on tokens - this ensures tokenizeBlocks has run
224
+ // and populated the module-level currentFootnotes before we read it
225
+ tokens.value;
226
+ return getFootnotes();
227
+ });
228
+
229
+ const hasFootnotes = computed(() => Object.keys(footnotes.value).length > 0);
230
+
231
+ const sortedFootnotes = computed(() => {
232
+ return Object.entries(footnotes.value)
233
+ .sort((a, b) => a[1].index - b[1].index)
234
+ .map(([id, fn]) => ({ id, content: fn.content, index: fn.index }));
235
+ });
236
+
102
237
  // Check if a language is toggleable (json or yaml)
103
238
  function isToggleableLanguage(language: string): boolean {
104
239
  if (!language) return false;
@@ -222,9 +357,28 @@ function parseInlineContent(text: string): string {
222
357
  return parseInline(text, true);
223
358
  }
224
359
 
360
+ // Render a list item with potential nested children
361
+ function renderListItem(item: ListItem): string {
362
+ let html = parseInline(item.content, true);
363
+ if (item.children && item.children.length > 0) {
364
+ for (const child of item.children) {
365
+ if (child.type === "ul") {
366
+ const items = child.items.map((i) => `<li>${renderListItem(i)}</li>`).join("");
367
+ html += `<ul>${items}</ul>`;
368
+ } else if (child.type === "ol") {
369
+ const items = child.items.map((i) => `<li>${renderListItem(i)}</li>`).join("");
370
+ const startAttr = child.start !== 1 ? ` start="${child.start}"` : "";
371
+ html += `<ol${startAttr}>${items}</ol>`;
372
+ }
373
+ }
374
+ }
375
+ return html;
376
+ }
377
+
225
378
  // Render blockquote content (can contain nested markdown)
379
+ // Use preserveState to keep link refs and footnotes from parent document
226
380
  function renderBlockquote(content: string): string {
227
- return renderMarkdown(content);
381
+ return renderMarkdown(content, { preserveState: true });
228
382
  }
229
383
  </script>
230
384
 
@@ -3,3 +3,6 @@ export { default as CodeViewerCollapsed } from "./CodeViewerCollapsed.vue";
3
3
  export { default as CodeViewerFooter } from "./CodeViewerFooter.vue";
4
4
  export { default as LanguageBadge } from "./LanguageBadge.vue";
5
5
  export { default as MarkdownContent } from "./MarkdownContent.vue";
6
+
7
+ // Re-export Markdown editor components
8
+ export * from "../Markdown";
@@ -0,0 +1,314 @@
1
+ <template>
2
+ <div
3
+ class="dx-context-menu-overlay"
4
+ @click.self="onClose"
5
+ @keydown.escape="onClose"
6
+ >
7
+ <div
8
+ ref="menuRef"
9
+ class="dx-context-menu"
10
+ :style="menuStyle"
11
+ >
12
+ <template v-for="(item, itemIndex) in items" :key="item.id">
13
+ <!-- Divider -->
14
+ <div v-if="item.divider" class="context-menu-divider" />
15
+
16
+ <!-- Regular menu item or submenu trigger -->
17
+ <template v-else>
18
+ <div
19
+ class="context-menu-item-wrapper"
20
+ @mouseenter="handleItemHover(item, itemIndex)"
21
+ @mouseleave="handleItemLeave"
22
+ >
23
+ <button
24
+ class="context-menu-item"
25
+ :class="{ disabled: item.disabled, 'has-children': item.children?.length }"
26
+ type="button"
27
+ :disabled="item.disabled"
28
+ @click="onItemClick(item)"
29
+ >
30
+ <span class="item-label">{{ item.label }}</span>
31
+ <span v-if="item.shortcut && !item.children" class="item-shortcut">{{ item.shortcut }}</span>
32
+ <span v-if="item.children?.length" class="item-chevron">&#9656;</span>
33
+ </button>
34
+
35
+ <!-- Nested submenu -->
36
+ <div
37
+ v-if="item.children?.length && activeSubmenuId === item.id"
38
+ ref="submenuRefs"
39
+ class="dx-context-submenu"
40
+ :class="{ 'open-left': submenuOpenLeft }"
41
+ :data-item-id="item.id"
42
+ @mouseenter="handleSubmenuEnter"
43
+ @mouseleave="handleSubmenuLeave"
44
+ >
45
+ <template v-for="child in item.children" :key="child.id">
46
+ <!-- Child divider -->
47
+ <div v-if="child.divider" class="context-menu-divider" />
48
+
49
+ <!-- Child item -->
50
+ <button
51
+ v-else
52
+ class="context-menu-item"
53
+ :class="{ disabled: child.disabled }"
54
+ type="button"
55
+ :disabled="child.disabled"
56
+ @click="onItemClick(child)"
57
+ >
58
+ <span class="item-label">{{ child.label }}</span>
59
+ <span v-if="child.shortcut" class="item-shortcut">{{ child.shortcut }}</span>
60
+ </button>
61
+ </template>
62
+ </div>
63
+ </div>
64
+ </template>
65
+ </template>
66
+ </div>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import { computed, onMounted, onUnmounted, ref } from "vue";
72
+ import type { ContextMenuItem } from "./types";
73
+ import type { PopoverPosition } from "@/composables/markdown";
74
+
75
+ export interface ContextMenuProps {
76
+ position: PopoverPosition;
77
+ items: ContextMenuItem[];
78
+ }
79
+
80
+ const props = defineProps<ContextMenuProps>();
81
+
82
+ const emit = defineEmits<{
83
+ close: [];
84
+ action: [item: ContextMenuItem];
85
+ }>();
86
+
87
+ const menuRef = ref<HTMLElement | null>(null);
88
+ const activeSubmenuId = ref<string | null>(null);
89
+ const submenuOpenLeft = ref(false);
90
+ let hoverTimeout: ReturnType<typeof setTimeout> | null = null;
91
+
92
+ // Calculate menu position with viewport boundary detection
93
+ const menuStyle = computed(() => {
94
+ const menuHeight = 400; // Approximate max height for nested menus
95
+ const menuWidth = 320; // Match CSS max-width
96
+ const padding = 10;
97
+
98
+ let top = props.position.y;
99
+ let left = props.position.x;
100
+
101
+ // Check if menu would extend below viewport
102
+ if (top + menuHeight > window.innerHeight - padding) {
103
+ // Position above the cursor
104
+ top = Math.max(padding, props.position.y - menuHeight);
105
+ }
106
+
107
+ // Ensure menu doesn't go off left edge
108
+ if (left < padding) {
109
+ left = padding;
110
+ }
111
+
112
+ // Ensure menu doesn't go off right edge
113
+ if (left + menuWidth > window.innerWidth - padding) {
114
+ left = window.innerWidth - menuWidth - padding;
115
+ }
116
+
117
+ // Determine if submenus should open to the left
118
+ // (if menu is positioned near right edge, submenus should open left)
119
+ submenuOpenLeft.value = left + menuWidth + menuWidth > window.innerWidth - padding;
120
+
121
+ return {
122
+ top: `${top}px`,
123
+ left: `${left}px`
124
+ };
125
+ });
126
+
127
+ function handleItemHover(item: ContextMenuItem, _index: number): void {
128
+ // Clear any pending timeout
129
+ if (hoverTimeout) {
130
+ clearTimeout(hoverTimeout);
131
+ hoverTimeout = null;
132
+ }
133
+
134
+ // If item has children, show submenu after a small delay
135
+ if (item.children?.length) {
136
+ hoverTimeout = setTimeout(() => {
137
+ activeSubmenuId.value = item.id;
138
+ }, 100);
139
+ } else {
140
+ // Immediately hide submenu for non-parent items
141
+ activeSubmenuId.value = null;
142
+ }
143
+ }
144
+
145
+ function handleItemLeave(): void {
146
+ // Clear pending timeout
147
+ if (hoverTimeout) {
148
+ clearTimeout(hoverTimeout);
149
+ hoverTimeout = null;
150
+ }
151
+
152
+ // Set a timeout to close submenu (will be cancelled if mouse enters submenu)
153
+ hoverTimeout = setTimeout(() => {
154
+ activeSubmenuId.value = null;
155
+ }, 150);
156
+ }
157
+
158
+ function handleSubmenuEnter(): void {
159
+ // Cancel any pending close timeout when entering the submenu
160
+ if (hoverTimeout) {
161
+ clearTimeout(hoverTimeout);
162
+ hoverTimeout = null;
163
+ }
164
+ }
165
+
166
+ function handleSubmenuLeave(): void {
167
+ // Start timeout to close submenu when leaving
168
+ hoverTimeout = setTimeout(() => {
169
+ activeSubmenuId.value = null;
170
+ }, 150);
171
+ }
172
+
173
+ function onItemClick(item: ContextMenuItem): void {
174
+ if (item.disabled) return;
175
+
176
+ // If item has children, don't close - just toggle submenu
177
+ if (item.children?.length) {
178
+ activeSubmenuId.value = activeSubmenuId.value === item.id ? null : item.id;
179
+ return;
180
+ }
181
+
182
+ // Execute action if available
183
+ if (item.action) {
184
+ emit("action", item);
185
+ item.action();
186
+ }
187
+ emit("close");
188
+ }
189
+
190
+ function onClose(): void {
191
+ emit("close");
192
+ }
193
+
194
+ // Handle Escape key at document level
195
+ function handleDocumentKeydown(event: KeyboardEvent): void {
196
+ if (event.key === "Escape") {
197
+ onClose();
198
+ }
199
+ }
200
+
201
+ onMounted(() => {
202
+ document.addEventListener("keydown", handleDocumentKeydown);
203
+ });
204
+
205
+ onUnmounted(() => {
206
+ document.removeEventListener("keydown", handleDocumentKeydown);
207
+ if (hoverTimeout) {
208
+ clearTimeout(hoverTimeout);
209
+ }
210
+ });
211
+ </script>
212
+
213
+ <style lang="scss">
214
+ .dx-context-menu-overlay {
215
+ position: fixed;
216
+ inset: 0;
217
+ z-index: 1000;
218
+ // Transparent overlay - no visual background
219
+ }
220
+
221
+ .dx-context-menu {
222
+ position: fixed;
223
+ background: #2d2d2d;
224
+ border: 1px solid #404040;
225
+ border-radius: 0.375rem;
226
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
227
+ min-width: 200px;
228
+ max-width: 320px;
229
+ overflow: visible;
230
+ padding: 0.25rem 0;
231
+
232
+ .context-menu-divider {
233
+ height: 1px;
234
+ background: #404040;
235
+ margin: 0.25rem 0;
236
+ }
237
+
238
+ .context-menu-item-wrapper {
239
+ position: relative;
240
+ }
241
+
242
+ .context-menu-item {
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: space-between;
246
+ width: 100%;
247
+ padding: 0.5rem 0.75rem;
248
+ background: transparent;
249
+ border: none;
250
+ color: #d4d4d4;
251
+ font-size: 0.875rem;
252
+ text-align: left;
253
+ cursor: pointer;
254
+ transition: background-color 0.15s ease;
255
+
256
+ &:hover:not(.disabled) {
257
+ background: rgba(255, 255, 255, 0.1);
258
+ }
259
+
260
+ &.disabled {
261
+ color: #6b7280;
262
+ cursor: not-allowed;
263
+ }
264
+
265
+ &.has-children {
266
+ padding-right: 0.5rem;
267
+ }
268
+
269
+ .item-label {
270
+ flex: 1;
271
+ white-space: nowrap;
272
+ }
273
+
274
+ .item-shortcut {
275
+ font-size: 0.75rem;
276
+ color: #6b7280;
277
+ font-family: 'Consolas', 'Monaco', monospace;
278
+ margin-left: 1rem;
279
+ white-space: nowrap;
280
+ }
281
+
282
+ .item-chevron {
283
+ font-size: 0.75rem;
284
+ color: #6b7280;
285
+ margin-left: 0.5rem;
286
+ }
287
+ }
288
+
289
+ // Submenu styling
290
+ .dx-context-submenu {
291
+ position: absolute;
292
+ top: 0;
293
+ left: 100%;
294
+ margin-left: 2px;
295
+ background: #2d2d2d;
296
+ border: 1px solid #404040;
297
+ border-radius: 0.375rem;
298
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
299
+ min-width: 280px;
300
+ max-width: 360px;
301
+ overflow: hidden;
302
+ padding: 0.25rem 0;
303
+ z-index: 1001;
304
+
305
+ // Open to the left when near right viewport edge
306
+ &.open-left {
307
+ left: auto;
308
+ right: 100%;
309
+ margin-left: 0;
310
+ margin-right: 2px;
311
+ }
312
+ }
313
+ }
314
+ </style>