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.
Files changed (63) hide show
  1. package/dist/danx.es.js +12797 -8181
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +192 -120
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +8 -1
  7. package/src/components/Utility/Code/CodeViewer.vue +31 -14
  8. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  9. package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
  10. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  11. package/src/components/Utility/Code/index.ts +3 -0
  12. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  13. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  14. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  15. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  16. package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
  17. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
  18. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  19. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  20. package/src/components/Utility/Markdown/index.ts +11 -0
  21. package/src/components/Utility/Markdown/types.ts +27 -0
  22. package/src/components/Utility/index.ts +1 -0
  23. package/src/composables/index.ts +1 -0
  24. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  25. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  26. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  27. package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
  28. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  29. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  30. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  31. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  32. package/src/composables/markdown/features/useHeadings.ts +290 -0
  33. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  34. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  35. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  36. package/src/composables/markdown/features/useLinks.spec.ts +369 -0
  37. package/src/composables/markdown/features/useLinks.ts +374 -0
  38. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  39. package/src/composables/markdown/features/useLists.ts +747 -0
  40. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  41. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  42. package/src/composables/markdown/features/useTables.ts +1107 -0
  43. package/src/composables/markdown/index.ts +16 -0
  44. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  45. package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
  46. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  47. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  48. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  49. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  50. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  51. package/src/composables/useCodeViewerEditor.ts +174 -20
  52. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  53. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  54. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
  55. package/src/helpers/formats/markdown/index.ts +7 -0
  56. package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
  57. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  58. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  59. package/src/test/helpers/editorTestUtils.ts +253 -0
  60. package/src/test/helpers/index.ts +1 -0
  61. package/src/test/setup.test.ts +12 -0
  62. package/src/test/setup.ts +12 -0
  63. 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">&#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>
@@ -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>