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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.99",
3
+ "version": "0.5.1",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -15,7 +15,12 @@
15
15
  "build:bundle": "vite build",
16
16
  "build": "yarn clean && yarn build:types && yarn build:bundle",
17
17
  "preview": "vite preview",
18
- "postversion": "yarn build && npm publish && cd .. && git add ui && git commit -m \"v$npm_package_version\" && git tag \"v$npm_package_version\" && git push"
18
+ "test": "vitest",
19
+ "test:run": "vitest run",
20
+ "test:coverage": "vitest run --coverage",
21
+ "publish:patch": "./scripts/publish.sh patch",
22
+ "publish:minor": "./scripts/publish.sh minor",
23
+ "publish:major": "./scripts/publish.sh major"
19
24
  },
20
25
  "repository": {
21
26
  "type": "git",
@@ -32,6 +37,8 @@
32
37
  "@typescript-eslint/eslint-plugin": "^7.6.0",
33
38
  "@typescript-eslint/parser": "^7.6.0",
34
39
  "@vitejs/plugin-vue": "^5.0.4",
40
+ "@vitest/coverage-v8": "^4.0.16",
41
+ "@vue/test-utils": "^2.4.6",
35
42
  "autoprefixer": "^10.4.19",
36
43
  "chalk": "^4.1.0",
37
44
  "core-js": "^3.0.0",
@@ -40,6 +47,7 @@
40
47
  "eslint-plugin-import": "^2.29.1",
41
48
  "eslint-plugin-vue": "^9.24.1",
42
49
  "fs-extra": "^8.1.0",
50
+ "jsdom": "^27.4.0",
43
51
  "postcss": "^8.4.38",
44
52
  "quasar": "^2.0.0",
45
53
  "sass": "^1.33.0",
@@ -49,6 +57,7 @@
49
57
  "vite": "^5.2.8",
50
58
  "vite-plugin-dts": "^3.8.1",
51
59
  "vite-svg-loader": "^5.1.0",
60
+ "vitest": "^4.0.16",
52
61
  "vue": "^3.4.21",
53
62
  "vue-eslint-parser": "^9.4.2",
54
63
  "vue-router": "^4.3.2"
@@ -0,0 +1,76 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # NOTE: npm now requires granular tokens with 2FA, max 90-day lifetime
5
+ # Classic tokens have been revoked. If auth fails, you'll need to:
6
+ # 1. Run `npm login` to create a new granular token
7
+ # 2. Complete 2FA verification
8
+ # See: https://gh.io/all-npm-classic-tokens-revoked
9
+
10
+ # Colors for output
11
+ RED='\033[0;31m'
12
+ GREEN='\033[0;32m'
13
+ YELLOW='\033[1;33m'
14
+ NC='\033[0m' # No Color
15
+
16
+ echo -e "${YELLOW}Checking npm authentication...${NC}"
17
+
18
+ # Check if logged in by trying to get current user
19
+ if ! npm whoami &>/dev/null; then
20
+ echo -e "${RED}Not logged in to npm or token expired.${NC}"
21
+ echo -e "${YELLOW}Please log in to npm:${NC}"
22
+ npm login
23
+
24
+ # Verify login succeeded
25
+ if ! npm whoami &>/dev/null; then
26
+ echo -e "${RED}Login failed. Aborting.${NC}"
27
+ exit 1
28
+ fi
29
+ fi
30
+
31
+ CURRENT_USER=$(npm whoami)
32
+ echo -e "${GREEN}Logged in as: ${CURRENT_USER}${NC}"
33
+
34
+ # Get the version bump type (default to patch)
35
+ BUMP_TYPE=${1:-patch}
36
+
37
+ if [[ ! "$BUMP_TYPE" =~ ^(patch|minor|major)$ ]]; then
38
+ echo -e "${RED}Invalid version bump type: ${BUMP_TYPE}${NC}"
39
+ echo "Usage: ./scripts/publish.sh [patch|minor|major]"
40
+ exit 1
41
+ fi
42
+
43
+ echo -e "${YELLOW}Bumping version (${BUMP_TYPE})...${NC}"
44
+
45
+ # Build first to catch any errors before version bump
46
+ echo -e "${YELLOW}Building...${NC}"
47
+ yarn build
48
+
49
+ # Bump version (this updates package.json)
50
+ npm version $BUMP_TYPE --no-git-tag-version
51
+
52
+ # Get the new version
53
+ NEW_VERSION=$(node -p "require('./package.json').version")
54
+ echo -e "${GREEN}New version: ${NEW_VERSION}${NC}"
55
+
56
+ # Publish to npm
57
+ echo -e "${YELLOW}Publishing to npm...${NC}"
58
+ if npm publish; then
59
+ echo -e "${GREEN}Published successfully!${NC}"
60
+
61
+ # Git operations
62
+ echo -e "${YELLOW}Committing and tagging...${NC}"
63
+ cd ..
64
+ git add ui
65
+ git commit -m "v${NEW_VERSION}"
66
+ git tag "v${NEW_VERSION}"
67
+ git push
68
+ git push --tags
69
+
70
+ echo -e "${GREEN}Done! Published v${NEW_VERSION}${NC}"
71
+ else
72
+ echo -e "${RED}Publish failed!${NC}"
73
+ echo -e "${YELLOW}Rolling back version in package.json...${NC}"
74
+ git checkout package.json
75
+ exit 1
76
+ fi
@@ -15,18 +15,25 @@
15
15
  :preview="collapsedPreview"
16
16
  :format="currentFormat"
17
17
  :available-formats="currentFormat === 'json' || currentFormat === 'yaml' ? ['json', 'yaml'] : currentFormat === 'text' || currentFormat === 'markdown' ? ['text', 'markdown'] : []"
18
+ :allow-any-language="allowAnyLanguage"
18
19
  @expand="toggleCollapse"
19
20
  @format-change="onFormatChange"
20
21
  />
21
22
 
22
23
  <!-- Expanded view - full code viewer -->
23
24
  <template v-else>
24
- <div class="code-wrapper relative flex flex-col flex-1 min-h-0">
25
+ <div
26
+ class="code-wrapper relative flex flex-col flex-1 min-h-0"
27
+ tabindex="0"
28
+ @keydown="editor.onKeyDown"
29
+ >
25
30
  <!-- Language badge - shows popout format options on hover -->
26
31
  <LanguageBadge
32
+ ref="languageBadgeRef"
27
33
  :format="currentFormat"
28
34
  :available-formats="currentFormat === 'json' || currentFormat === 'yaml' ? ['json', 'yaml'] : currentFormat === 'text' || currentFormat === 'markdown' ? ['text', 'markdown'] : []"
29
35
  :toggleable="true"
36
+ :allow-any-language="allowAnyLanguage"
30
37
  @click.stop
31
38
  @change="onFormatChange"
32
39
  />
@@ -110,6 +117,7 @@ export interface CodeViewerProps {
110
117
  collapsible?: boolean;
111
118
  defaultCollapsed?: boolean;
112
119
  defaultCodeFormat?: "json" | "yaml";
120
+ allowAnyLanguage?: boolean;
113
121
  }
114
122
 
115
123
  const props = withDefaults(defineProps<CodeViewerProps>(), {
@@ -127,6 +135,8 @@ const emit = defineEmits<{
127
135
  "update:modelValue": [value: object | string | null];
128
136
  "update:format": [format: CodeFormat];
129
137
  "update:editable": [editable: boolean];
138
+ "exit": [];
139
+ "delete": [];
130
140
  }>();
131
141
 
132
142
  // Initialize composable with current props
@@ -138,6 +148,7 @@ const codeFormat = useCodeFormat({
138
148
  // Local state
139
149
  const currentFormat = ref<CodeFormat>(props.format);
140
150
  const codeRef = ref<HTMLPreElement | null>(null);
151
+ const languageBadgeRef = ref<InstanceType<typeof LanguageBadge> | null>(null);
141
152
 
142
153
  // Collapsed state (for collapsible mode)
143
154
  const isCollapsed = ref(props.collapsible && props.defaultCollapsed);
@@ -154,9 +165,26 @@ function toggleCollapse() {
154
165
  isCollapsed.value = !isCollapsed.value;
155
166
  }
156
167
 
157
- // Sync composable format with current format
168
+ // Initialize editor composable
169
+ const editor = useCodeViewerEditor({
170
+ codeRef,
171
+ codeFormat,
172
+ currentFormat,
173
+ canEdit: toRef(props, "canEdit"),
174
+ editable: toRef(props, "editable"),
175
+ onEmitModelValue: (value) => emit("update:modelValue", value),
176
+ onEmitEditable: (editable) => emit("update:editable", editable),
177
+ onEmitFormat: (format) => onFormatChange(format),
178
+ onExit: () => emit("exit"),
179
+ onDelete: () => emit("delete"),
180
+ onOpenLanguageSearch: () => languageBadgeRef.value?.openSearchPanel()
181
+ });
182
+
183
+ // Sync composable format with current format and update editor content
158
184
  watch(currentFormat, (newFormat) => {
159
185
  codeFormat.setFormat(newFormat);
186
+ // Update editor content when format changes (needed for syntax re-highlighting)
187
+ editor.updateEditingContentOnFormatChange();
160
188
  });
161
189
 
162
190
  // Watch for external format changes
@@ -170,17 +198,6 @@ watch(() => props.modelValue, () => {
170
198
  editor.syncEditingContentFromValue();
171
199
  });
172
200
 
173
- // Initialize editor composable
174
- const editor = useCodeViewerEditor({
175
- codeRef,
176
- codeFormat,
177
- currentFormat,
178
- canEdit: toRef(props, "canEdit"),
179
- editable: toRef(props, "editable"),
180
- onEmitModelValue: (value) => emit("update:modelValue", value),
181
- onEmitEditable: (editable) => emit("update:editable", editable)
182
- });
183
-
184
201
  // Sync internal editable state with prop
185
202
  watch(() => props.editable, (newValue) => {
186
203
  editor.syncEditableFromProp(newValue);
@@ -198,7 +215,7 @@ const { collapsedPreview } = useCodeViewerCollapse({
198
215
  function onFormatChange(newFormat: CodeFormat) {
199
216
  currentFormat.value = newFormat;
200
217
  emit("update:format", newFormat);
201
- editor.updateEditingContentOnFormatChange();
218
+ // Note: editor.updateEditingContentOnFormatChange() is called by the currentFormat watcher
202
219
  }
203
220
 
204
221
  // Get the raw markdown content for MarkdownContent component
@@ -11,6 +11,7 @@
11
11
  :format="format"
12
12
  :available-formats="availableFormats"
13
13
  :toggleable="availableFormats.length > 1"
14
+ :allow-any-language="allowAnyLanguage"
14
15
  @click.stop
15
16
  @change="(fmt) => $emit('format-change', fmt)"
16
17
  />
@@ -25,6 +26,7 @@ defineProps<{
25
26
  preview: string;
26
27
  format: string;
27
28
  availableFormats?: string[];
29
+ allowAnyLanguage?: boolean;
28
30
  }>();
29
31
 
30
32
  defineEmits<{
@@ -26,13 +26,13 @@
26
26
  @click="$emit('toggle-edit')"
27
27
  >
28
28
  <EditIcon class="w-3.5 h-3.5" />
29
- <QTooltip>{{ isEditing ? 'Exit edit mode' : 'Edit content' }}</QTooltip>
30
29
  </QBtn>
31
30
  </div>
32
31
  </template>
33
32
 
34
33
  <script setup lang="ts">
35
34
  import { FaSolidPencil as EditIcon } from "danx-icon";
35
+ import { QBtn } from "quasar";
36
36
  import { computed } from "vue";
37
37
  import { ValidationError } from "../../../composables/useCodeFormat";
38
38
 
@@ -3,8 +3,21 @@
3
3
  class="dx-language-badge-container"
4
4
  :class="{ 'is-toggleable': toggleable && availableFormats.length > 1 }"
5
5
  @mouseenter="showOptions = true"
6
- @mouseleave="showOptions = false"
6
+ @mouseleave="onMouseLeave"
7
7
  >
8
+ <!-- Search icon (when allowAnyLanguage is true) - placed first/leftmost -->
9
+ <transition name="slide-left">
10
+ <div
11
+ v-if="showOptions && allowAnyLanguage"
12
+ class="dx-language-option dx-language-search-trigger"
13
+ @click.stop="openSearchPanel"
14
+ >
15
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
16
+ <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
17
+ </svg>
18
+ </div>
19
+ </transition>
20
+
8
21
  <!-- Other format options (slide out to the left) -->
9
22
  <transition name="slide-left">
10
23
  <div
@@ -23,36 +36,197 @@
23
36
  </transition>
24
37
 
25
38
  <!-- Current format badge (stays in place) -->
26
- <div class="dx-language-badge" :class="{ 'is-active': showOptions && otherFormats.length > 0 }">
39
+ <div class="dx-language-badge" :class="{ 'is-active': showOptions && (otherFormats.length > 0 || allowAnyLanguage) }">
27
40
  {{ format.toUpperCase() }}
28
41
  </div>
42
+
43
+ <!-- Search dropdown panel -->
44
+ <transition name="fade">
45
+ <div
46
+ v-if="showSearchPanel"
47
+ class="dx-language-search-panel"
48
+ @click.stop
49
+ >
50
+ <input
51
+ ref="searchInputRef"
52
+ v-model="searchQuery"
53
+ type="text"
54
+ class="dx-language-search-input"
55
+ placeholder="Search languages..."
56
+ @input="onSearchQueryChange"
57
+ @keydown.down.prevent="navigateDown"
58
+ @keydown.up.prevent="navigateUp"
59
+ @keydown.enter.prevent="selectCurrentItem"
60
+ @keydown.escape="closeSearchPanel"
61
+ />
62
+ <div class="dx-language-search-list">
63
+ <div
64
+ v-for="(lang, index) in filteredLanguages"
65
+ :key="lang"
66
+ class="dx-language-search-item"
67
+ :class="{ 'is-selected': index === selectedIndex }"
68
+ @click="selectLanguage(lang)"
69
+ @mouseenter="selectedIndex = index"
70
+ >
71
+ {{ lang.toUpperCase() }}
72
+ </div>
73
+ <div v-if="filteredLanguages.length === 0" class="dx-language-search-empty">
74
+ No languages found
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </transition>
29
79
  </div>
30
80
  </template>
31
81
 
32
82
  <script setup lang="ts">
33
- import { computed, ref } from "vue";
83
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
84
+
85
+ // All supported languages (sorted alphabetically)
86
+ const ALL_LANGUAGES = [
87
+ "bash",
88
+ "c",
89
+ "cpp",
90
+ "css",
91
+ "dockerfile",
92
+ "go",
93
+ "graphql",
94
+ "html",
95
+ "java",
96
+ "javascript",
97
+ "json",
98
+ "kotlin",
99
+ "markdown",
100
+ "php",
101
+ "python",
102
+ "ruby",
103
+ "rust",
104
+ "scss",
105
+ "sql",
106
+ "swift",
107
+ "text",
108
+ "typescript",
109
+ "xml",
110
+ "yaml"
111
+ ];
34
112
 
35
113
  export interface LanguageBadgeProps {
36
114
  format: string;
37
115
  availableFormats?: string[];
38
116
  toggleable?: boolean;
117
+ allowAnyLanguage?: boolean;
39
118
  }
40
119
 
41
120
  const props = withDefaults(defineProps<LanguageBadgeProps>(), {
42
121
  availableFormats: () => [],
43
- toggleable: true
122
+ toggleable: true,
123
+ allowAnyLanguage: false
44
124
  });
45
125
 
46
- defineEmits<{
126
+ const emit = defineEmits<{
47
127
  change: [format: string];
48
128
  }>();
49
129
 
50
130
  const showOptions = ref(false);
131
+ const showSearchPanel = ref(false);
132
+ const searchQuery = ref("");
133
+ const searchInputRef = ref<HTMLInputElement | null>(null);
134
+ const selectedIndex = ref(0);
51
135
 
52
136
  // Get formats other than the current one
53
137
  const otherFormats = computed(() => {
54
138
  return props.availableFormats.filter(f => f !== props.format);
55
139
  });
140
+
141
+ // Filter languages based on search query
142
+ const filteredLanguages = computed(() => {
143
+ if (!searchQuery.value) {
144
+ return ALL_LANGUAGES;
145
+ }
146
+ const query = searchQuery.value.toLowerCase();
147
+ return ALL_LANGUAGES.filter(lang => lang.toLowerCase().includes(query));
148
+ });
149
+
150
+ // Reset selectedIndex when search query changes
151
+ function onSearchQueryChange() {
152
+ selectedIndex.value = 0;
153
+ }
154
+
155
+ // Keyboard navigation functions
156
+ function navigateDown() {
157
+ if (filteredLanguages.value.length === 0) return;
158
+ selectedIndex.value = (selectedIndex.value + 1) % filteredLanguages.value.length;
159
+ scrollSelectedIntoView();
160
+ }
161
+
162
+ function navigateUp() {
163
+ if (filteredLanguages.value.length === 0) return;
164
+ selectedIndex.value = selectedIndex.value === 0
165
+ ? filteredLanguages.value.length - 1
166
+ : selectedIndex.value - 1;
167
+ scrollSelectedIntoView();
168
+ }
169
+
170
+ function selectCurrentItem() {
171
+ if (filteredLanguages.value.length > 0) {
172
+ selectLanguage(filteredLanguages.value[selectedIndex.value]);
173
+ }
174
+ }
175
+
176
+ function scrollSelectedIntoView() {
177
+ nextTick(() => {
178
+ const list = document.querySelector(".dx-language-search-list");
179
+ const selected = list?.querySelector(".is-selected");
180
+ selected?.scrollIntoView({ block: "nearest" });
181
+ });
182
+ }
183
+
184
+ function openSearchPanel() {
185
+ showSearchPanel.value = true;
186
+ searchQuery.value = "";
187
+ selectedIndex.value = 0;
188
+ nextTick(() => {
189
+ searchInputRef.value?.focus();
190
+ });
191
+ }
192
+
193
+ function closeSearchPanel() {
194
+ showSearchPanel.value = false;
195
+ searchQuery.value = "";
196
+ }
197
+
198
+ function selectLanguage(lang: string) {
199
+ emit("change", lang);
200
+ closeSearchPanel();
201
+ }
202
+
203
+ function onMouseLeave() {
204
+ showOptions.value = false;
205
+ // Don't close search panel on mouse leave - let click outside handle it
206
+ }
207
+
208
+ function handleClickOutside(event: MouseEvent) {
209
+ if (showSearchPanel.value) {
210
+ const target = event.target as HTMLElement;
211
+ const panel = document.querySelector(".dx-language-search-panel");
212
+ if (panel && !panel.contains(target)) {
213
+ closeSearchPanel();
214
+ }
215
+ }
216
+ }
217
+
218
+ onMounted(() => {
219
+ document.addEventListener("mousedown", handleClickOutside);
220
+ });
221
+
222
+ onBeforeUnmount(() => {
223
+ document.removeEventListener("mousedown", handleClickOutside);
224
+ });
225
+
226
+ // Expose openSearchPanel for external callers (e.g., keyboard shortcut from CodeViewer)
227
+ defineExpose({
228
+ openSearchPanel
229
+ });
56
230
  </script>
57
231
 
58
232
  <style lang="scss">
@@ -94,6 +268,19 @@ const otherFormats = computed(() => {
94
268
  }
95
269
  }
96
270
 
271
+ // Search trigger inherits from .dx-language-option, only adds flex centering for the icon
272
+ .dx-language-search-trigger {
273
+ display: flex;
274
+ align-items: center;
275
+ justify-content: center;
276
+ padding: 2px 6px;
277
+
278
+ svg {
279
+ width: 12px;
280
+ height: 12px;
281
+ }
282
+ }
283
+
97
284
  .dx-language-badge {
98
285
  padding: 2px 8px;
99
286
  font-size: 0.7em;
@@ -108,6 +295,81 @@ const otherFormats = computed(() => {
108
295
  }
109
296
  }
110
297
 
298
+ .dx-language-search-panel {
299
+ position: absolute;
300
+ top: 100%;
301
+ right: 0;
302
+ margin-top: 4px;
303
+ background: rgba(0, 0, 0, 0.9);
304
+ border-radius: 6px;
305
+ border: 1px solid rgba(255, 255, 255, 0.1);
306
+ min-width: 160px;
307
+ overflow: hidden;
308
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
309
+ }
310
+
311
+ .dx-language-search-input {
312
+ width: 100%;
313
+ padding: 8px 12px;
314
+ font-size: 0.8em;
315
+ background: rgba(255, 255, 255, 0.05);
316
+ border: none;
317
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
318
+ color: rgba(255, 255, 255, 0.9);
319
+ outline: none;
320
+
321
+ &::placeholder {
322
+ color: rgba(255, 255, 255, 0.4);
323
+ }
324
+
325
+ &:focus {
326
+ background: rgba(255, 255, 255, 0.08);
327
+ }
328
+ }
329
+
330
+ .dx-language-search-list {
331
+ max-height: 200px;
332
+ overflow-y: auto;
333
+
334
+ &::-webkit-scrollbar {
335
+ width: 6px;
336
+ }
337
+
338
+ &::-webkit-scrollbar-track {
339
+ background: rgba(0, 0, 0, 0.2);
340
+ }
341
+
342
+ &::-webkit-scrollbar-thumb {
343
+ background: rgba(255, 255, 255, 0.2);
344
+ border-radius: 3px;
345
+
346
+ &:hover {
347
+ background: rgba(255, 255, 255, 0.3);
348
+ }
349
+ }
350
+ }
351
+
352
+ .dx-language-search-item {
353
+ padding: 6px 12px;
354
+ font-size: 0.75em;
355
+ color: rgba(255, 255, 255, 0.7);
356
+ cursor: pointer;
357
+ transition: all 0.15s;
358
+
359
+ &:hover,
360
+ &.is-selected {
361
+ background: rgba(255, 255, 255, 0.15);
362
+ color: rgba(255, 255, 255, 0.95);
363
+ }
364
+ }
365
+
366
+ .dx-language-search-empty {
367
+ padding: 12px;
368
+ font-size: 0.75em;
369
+ color: rgba(255, 255, 255, 0.4);
370
+ text-align: center;
371
+ }
372
+
111
373
  // Slide animation for options
112
374
  .slide-left-enter-active,
113
375
  .slide-left-leave-active {
@@ -119,4 +381,15 @@ const otherFormats = computed(() => {
119
381
  opacity: 0;
120
382
  transform: translateX(10px);
121
383
  }
384
+
385
+ // Fade animation for search panel
386
+ .fade-enter-active,
387
+ .fade-leave-active {
388
+ transition: opacity 0.15s ease;
389
+ }
390
+
391
+ .fade-enter-from,
392
+ .fade-leave-to {
393
+ opacity: 0;
394
+ }
122
395
  </style>