quasar-ui-danx 0.5.0 → 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.
- package/dist/danx.es.js +12797 -8181
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +192 -120
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +8 -1
- package/src/components/Utility/Code/CodeViewer.vue +31 -14
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/index.ts +3 -0
- package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
- package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
- package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
- package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
- package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
- package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
- package/src/components/Utility/Markdown/TablePopover.vue +420 -0
- package/src/components/Utility/Markdown/index.ts +11 -0
- package/src/components/Utility/Markdown/types.ts +27 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
- package/src/composables/markdown/features/useBlockquotes.ts +248 -0
- package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
- package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
- package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
- package/src/composables/markdown/features/useContextMenu.ts +444 -0
- package/src/composables/markdown/features/useFocusTracking.ts +116 -0
- package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
- package/src/composables/markdown/features/useHeadings.ts +290 -0
- package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
- package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
- package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
- package/src/composables/markdown/features/useLinks.spec.ts +369 -0
- package/src/composables/markdown/features/useLinks.ts +374 -0
- package/src/composables/markdown/features/useLists.spec.ts +834 -0
- package/src/composables/markdown/features/useLists.ts +747 -0
- package/src/composables/markdown/features/usePopoverManager.ts +181 -0
- package/src/composables/markdown/features/useTables.spec.ts +1601 -0
- package/src/composables/markdown/features/useTables.ts +1107 -0
- package/src/composables/markdown/index.ts +16 -0
- package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
- package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
- package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
- package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
- package/src/composables/markdown/useMarkdownSelection.ts +219 -0
- package/src/composables/markdown/useMarkdownSync.ts +549 -0
- package/src/composables/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
- package/src/helpers/formats/markdown/index.ts +7 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/test/helpers/editorTestUtils.spec.ts +296 -0
- package/src/test/helpers/editorTestUtils.ts +253 -0
- package/src/test/helpers/index.ts +1 -0
- package/src/test/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/vitest.config.ts +19 -0
|
@@ -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">▸</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>
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="overlayRef"
|
|
4
|
+
class="dx-hotkey-help-overlay"
|
|
5
|
+
tabindex="-1"
|
|
6
|
+
@click.self="$emit('close')"
|
|
7
|
+
@keydown.escape="$emit('close')"
|
|
8
|
+
>
|
|
9
|
+
<div class="dx-hotkey-help-popover">
|
|
10
|
+
<div class="popover-header">
|
|
11
|
+
<h3>Keyboard Shortcuts</h3>
|
|
12
|
+
<button
|
|
13
|
+
class="close-btn"
|
|
14
|
+
type="button"
|
|
15
|
+
aria-label="Close"
|
|
16
|
+
@click="$emit('close')"
|
|
17
|
+
>
|
|
18
|
+
<CloseIcon class="w-4 h-4" />
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="popover-content">
|
|
23
|
+
<div class="hotkey-groups-grid">
|
|
24
|
+
<div
|
|
25
|
+
v-for="group in groupedHotkeys"
|
|
26
|
+
:key="group.name"
|
|
27
|
+
class="hotkey-group"
|
|
28
|
+
>
|
|
29
|
+
<h4>{{ group.label }}</h4>
|
|
30
|
+
<div class="hotkey-list">
|
|
31
|
+
<div
|
|
32
|
+
v-for="hotkey in group.hotkeys"
|
|
33
|
+
:key="hotkey.key"
|
|
34
|
+
class="hotkey-item"
|
|
35
|
+
>
|
|
36
|
+
<span class="hotkey-description">{{ hotkey.description }}</span>
|
|
37
|
+
<kbd class="hotkey-key">{{ formatKey(hotkey.key) }}</kbd>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup lang="ts">
|
|
48
|
+
import { FaSolidXmark as CloseIcon } from "danx-icon";
|
|
49
|
+
import { computed, onMounted, ref } from "vue";
|
|
50
|
+
import { HotkeyDefinition, HotkeyGroup } from "../../../composables/markdown/useMarkdownHotkeys";
|
|
51
|
+
|
|
52
|
+
const overlayRef = ref<HTMLDivElement | null>(null);
|
|
53
|
+
|
|
54
|
+
onMounted(() => {
|
|
55
|
+
// Focus the overlay so it can receive keyboard events
|
|
56
|
+
overlayRef.value?.focus();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export interface HotkeyHelpPopoverProps {
|
|
60
|
+
hotkeys: HotkeyDefinition[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface HotkeyGroupDisplay {
|
|
64
|
+
name: HotkeyGroup;
|
|
65
|
+
label: string;
|
|
66
|
+
hotkeys: HotkeyDefinition[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const GROUP_LABELS: Record<HotkeyGroup, string> = {
|
|
70
|
+
headings: "Headings",
|
|
71
|
+
formatting: "Formatting",
|
|
72
|
+
lists: "Lists",
|
|
73
|
+
blocks: "Blocks",
|
|
74
|
+
tables: "Tables",
|
|
75
|
+
other: "Other"
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const GROUP_ORDER: HotkeyGroup[] = ["headings", "formatting", "lists", "blocks", "tables", "other"];
|
|
79
|
+
|
|
80
|
+
const props = defineProps<HotkeyHelpPopoverProps>();
|
|
81
|
+
|
|
82
|
+
defineEmits<{
|
|
83
|
+
close: [];
|
|
84
|
+
}>();
|
|
85
|
+
|
|
86
|
+
const groupedHotkeys = computed<HotkeyGroupDisplay[]>(() => {
|
|
87
|
+
const groups = new Map<HotkeyGroup, HotkeyDefinition[]>();
|
|
88
|
+
|
|
89
|
+
// Initialize groups
|
|
90
|
+
for (const group of GROUP_ORDER) {
|
|
91
|
+
groups.set(group, []);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Distribute hotkeys into groups
|
|
95
|
+
for (const hotkey of props.hotkeys) {
|
|
96
|
+
const group = groups.get(hotkey.group);
|
|
97
|
+
if (group) {
|
|
98
|
+
group.push(hotkey);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Convert to display format, filtering empty groups
|
|
103
|
+
return GROUP_ORDER
|
|
104
|
+
.filter(name => (groups.get(name)?.length || 0) > 0)
|
|
105
|
+
.map(name => ({
|
|
106
|
+
name,
|
|
107
|
+
label: GROUP_LABELS[name],
|
|
108
|
+
hotkeys: groups.get(name) || []
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Format a key combination for display
|
|
114
|
+
* Converts 'ctrl+1' to 'Ctrl + 1'
|
|
115
|
+
*/
|
|
116
|
+
function formatKey(key: string): string {
|
|
117
|
+
return key
|
|
118
|
+
.split("+")
|
|
119
|
+
.map(part => {
|
|
120
|
+
const lower = part.toLowerCase();
|
|
121
|
+
switch (lower) {
|
|
122
|
+
case "ctrl":
|
|
123
|
+
case "control":
|
|
124
|
+
return "Ctrl";
|
|
125
|
+
case "shift":
|
|
126
|
+
return "Shift";
|
|
127
|
+
case "alt":
|
|
128
|
+
case "option":
|
|
129
|
+
return "Alt";
|
|
130
|
+
case "meta":
|
|
131
|
+
case "cmd":
|
|
132
|
+
case "command":
|
|
133
|
+
return "Cmd";
|
|
134
|
+
default:
|
|
135
|
+
return part.toUpperCase();
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
.join(" + ");
|
|
139
|
+
}
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<style lang="scss">
|
|
143
|
+
.dx-hotkey-help-overlay {
|
|
144
|
+
position: fixed;
|
|
145
|
+
inset: 0;
|
|
146
|
+
z-index: 1000;
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
background: rgba(0, 0, 0, 0.5);
|
|
151
|
+
backdrop-filter: blur(2px);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.dx-hotkey-help-popover {
|
|
155
|
+
background: #2d2d2d;
|
|
156
|
+
border: 1px solid #404040;
|
|
157
|
+
border-radius: 0.5rem;
|
|
158
|
+
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
|
159
|
+
min-width: 320px;
|
|
160
|
+
max-width: 90vw;
|
|
161
|
+
max-height: 80vh;
|
|
162
|
+
overflow: hidden;
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
|
|
166
|
+
.popover-header {
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: space-between;
|
|
170
|
+
padding: 1rem 1.25rem;
|
|
171
|
+
border-bottom: 1px solid #404040;
|
|
172
|
+
|
|
173
|
+
h3 {
|
|
174
|
+
margin: 0;
|
|
175
|
+
font-size: 1rem;
|
|
176
|
+
font-weight: 600;
|
|
177
|
+
color: #f3f4f6;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.close-btn {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
width: 1.75rem;
|
|
185
|
+
height: 1.75rem;
|
|
186
|
+
padding: 0;
|
|
187
|
+
background: transparent;
|
|
188
|
+
border: none;
|
|
189
|
+
border-radius: 0.25rem;
|
|
190
|
+
color: #9ca3af;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
transition: all 0.15s ease;
|
|
193
|
+
|
|
194
|
+
&:hover {
|
|
195
|
+
background: rgba(255, 255, 255, 0.1);
|
|
196
|
+
color: #f3f4f6;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.popover-content {
|
|
202
|
+
padding: 1rem 1.25rem;
|
|
203
|
+
overflow-y: auto;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.hotkey-groups-grid {
|
|
207
|
+
display: grid;
|
|
208
|
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
209
|
+
gap: 1.5rem 2rem;
|
|
210
|
+
|
|
211
|
+
// For wider screens, limit to 3 columns max
|
|
212
|
+
@media (min-width: 800px) {
|
|
213
|
+
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.hotkey-group {
|
|
218
|
+
h4 {
|
|
219
|
+
margin: 0 0 0.75rem;
|
|
220
|
+
font-size: 0.75rem;
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
text-transform: uppercase;
|
|
223
|
+
letter-spacing: 0.05em;
|
|
224
|
+
color: #9ca3af;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.hotkey-list {
|
|
229
|
+
display: flex;
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
gap: 0.5rem;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.hotkey-item {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
justify-content: space-between;
|
|
238
|
+
gap: 1rem;
|
|
239
|
+
|
|
240
|
+
.hotkey-description {
|
|
241
|
+
flex: 1;
|
|
242
|
+
color: #d4d4d4;
|
|
243
|
+
font-size: 0.875rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.hotkey-key {
|
|
247
|
+
flex-shrink: 0;
|
|
248
|
+
padding: 0.25rem 0.5rem;
|
|
249
|
+
background: #1e1e1e;
|
|
250
|
+
border: 1px solid #404040;
|
|
251
|
+
border-radius: 0.25rem;
|
|
252
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
253
|
+
font-size: 0.75rem;
|
|
254
|
+
color: #9ca3af;
|
|
255
|
+
white-space: nowrap;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
</style>
|