quasar-ui-danx 0.5.0 → 0.5.2

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 (81) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/dist/danx.es.js +16119 -10641
  3. package/dist/danx.es.js.map +1 -1
  4. package/dist/danx.umd.js +202 -123
  5. package/dist/danx.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +8 -1
  8. package/src/components/Utility/Buttons/ActionButton.vue +15 -5
  9. package/src/components/Utility/Code/CodeViewer.vue +41 -16
  10. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  11. package/src/components/Utility/Code/CodeViewerFooter.vue +3 -1
  12. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  13. package/src/components/Utility/Code/MarkdownContent.vue +31 -163
  14. package/src/components/Utility/Code/index.ts +3 -0
  15. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  16. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  17. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  18. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  19. package/src/components/Utility/Markdown/MarkdownEditor.vue +233 -0
  20. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +296 -0
  21. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  22. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  23. package/src/components/Utility/Markdown/index.ts +11 -0
  24. package/src/components/Utility/Markdown/types.ts +27 -0
  25. package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
  26. package/src/components/Utility/index.ts +1 -0
  27. package/src/composables/index.ts +1 -0
  28. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  29. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  30. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  31. package/src/composables/markdown/features/useCodeBlocks.spec.ts +805 -0
  32. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  33. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  34. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  35. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  36. package/src/composables/markdown/features/useHeadings.ts +290 -0
  37. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  38. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  39. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  40. package/src/composables/markdown/features/useLinks.spec.ts +388 -0
  41. package/src/composables/markdown/features/useLinks.ts +374 -0
  42. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  43. package/src/composables/markdown/features/useLists.ts +747 -0
  44. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  45. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  46. package/src/composables/markdown/features/useTables.ts +1107 -0
  47. package/src/composables/markdown/index.ts +16 -0
  48. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  49. package/src/composables/markdown/useMarkdownEditor.ts +1077 -0
  50. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  51. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  52. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  53. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  54. package/src/composables/useCodeFormat.ts +17 -10
  55. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  56. package/src/composables/useCodeViewerEditor.ts +174 -20
  57. package/src/helpers/formats/highlightCSS.ts +236 -0
  58. package/src/helpers/formats/highlightHTML.ts +483 -0
  59. package/src/helpers/formats/highlightJavaScript.ts +346 -0
  60. package/src/helpers/formats/highlightSyntax.ts +15 -4
  61. package/src/helpers/formats/index.ts +3 -0
  62. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  63. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  64. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +425 -0
  65. package/src/helpers/formats/markdown/index.ts +7 -0
  66. package/src/helpers/formats/markdown/linePatterns.spec.ts +498 -0
  67. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  68. package/src/styles/danx.scss +3 -3
  69. package/src/styles/index.scss +5 -5
  70. package/src/styles/themes/danx/code.scss +257 -1
  71. package/src/styles/themes/danx/index.scss +10 -10
  72. package/src/styles/themes/danx/markdown.scss +59 -0
  73. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  74. package/src/test/helpers/editorTestUtils.ts +253 -0
  75. package/src/test/helpers/index.ts +1 -0
  76. package/src/test/highlighters.test.ts +153 -0
  77. package/src/test/setup.test.ts +12 -0
  78. package/src/test/setup.ts +12 -0
  79. package/src/types/widgets.d.ts +2 -2
  80. package/vite.config.js +5 -1
  81. package/vitest.config.ts +19 -0
@@ -9,29 +9,17 @@
9
9
  />
10
10
 
11
11
  <!-- Code blocks with syntax highlighting -->
12
- <div
12
+ <CodeViewer
13
13
  v-else-if="token.type === 'code_block'"
14
- class="dx-markdown-code-block"
15
- >
16
- <!-- Language toggle badge (only for json/yaml) -->
17
- <LanguageBadge
18
- v-if="isToggleableLanguage(token.language)"
19
- :format="getCodeBlockFormat(index, token.language)"
20
- :available-formats="['json', 'yaml']"
21
- :toggleable="true"
22
- @change="(fmt) => setCodeBlockFormat(index, fmt)"
23
- />
24
- <LanguageBadge
25
- v-else-if="token.language"
26
- :format="token.language"
27
- :available-formats="[]"
28
- :toggleable="false"
29
- />
30
- <pre><code
31
- :class="'language-' + getCodeBlockFormat(index, token.language)"
32
- v-html="highlightCodeBlock(index, token.content, token.language)"
33
- ></code></pre>
34
- </div>
14
+ :model-value="token.content"
15
+ :format="normalizeLanguage(token.language)"
16
+ :default-code-format="defaultCodeFormat"
17
+ :can-edit="false"
18
+ :collapsible="false"
19
+ hide-footer
20
+ allow-any-language
21
+ class="markdown-code-block"
22
+ />
35
23
 
36
24
  <!-- Blockquotes (recursive) -->
37
25
  <blockquote
@@ -186,12 +174,10 @@
186
174
  </template>
187
175
 
188
176
  <script setup lang="ts">
189
- import { computed, reactive } from "vue";
190
- import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
177
+ import { computed } from "vue";
191
178
  import { tokenizeBlocks, parseInline, renderMarkdown, getFootnotes, resetParserState } from "../../../helpers/formats/markdown";
192
179
  import type { BlockToken, ListItem } from "../../../helpers/formats/markdown";
193
- import { highlightJSON, highlightYAML } from "../../../helpers/formats/highlightSyntax";
194
- import LanguageBadge from "./LanguageBadge.vue";
180
+ import CodeViewer from "./CodeViewer.vue";
195
181
 
196
182
  export interface MarkdownContentProps {
197
183
  content: string;
@@ -202,10 +188,21 @@ const props = withDefaults(defineProps<MarkdownContentProps>(), {
202
188
  content: ""
203
189
  });
204
190
 
205
- // Track format overrides for each code block (for toggling json<->yaml)
206
- const codeBlockFormats = reactive<Record<number, string>>({});
207
- // Cache converted content for each code block
208
- const convertedContent = reactive<Record<number, string>>({});
191
+ // Normalize language aliases to standard names
192
+ function normalizeLanguage(lang?: string): string {
193
+ if (!lang) return "text";
194
+ const aliases: Record<string, string> = {
195
+ js: "javascript",
196
+ ts: "typescript",
197
+ py: "python",
198
+ rb: "ruby",
199
+ yml: "yaml",
200
+ md: "markdown",
201
+ sh: "bash",
202
+ shell: "bash"
203
+ };
204
+ return aliases[lang.toLowerCase()] || lang.toLowerCase();
205
+ }
209
206
 
210
207
  // Tokenize the markdown content
211
208
  const tokens = computed<BlockToken[]>(() => {
@@ -234,124 +231,6 @@ const sortedFootnotes = computed(() => {
234
231
  .map(([id, fn]) => ({ id, content: fn.content, index: fn.index }));
235
232
  });
236
233
 
237
- // Check if a language is toggleable (json or yaml)
238
- function isToggleableLanguage(language: string): boolean {
239
- if (!language) return false;
240
- const lang = language.toLowerCase();
241
- return lang === "json" || lang === "yaml";
242
- }
243
-
244
- // Get the current format for a code block (respecting user toggle, then default override, then original)
245
- function getCodeBlockFormat(index: number, originalLanguage: string): string {
246
- // If user has toggled this block, use their choice
247
- if (codeBlockFormats[index]) {
248
- return codeBlockFormats[index];
249
- }
250
-
251
- // If a default is set and this is a toggleable language, use the default
252
- const lang = originalLanguage?.toLowerCase();
253
- if (props.defaultCodeFormat && (lang === "json" || lang === "yaml")) {
254
- return props.defaultCodeFormat;
255
- }
256
-
257
- // Otherwise use the original language
258
- return lang || "text";
259
- }
260
-
261
- // Get converted content for a code block (handles initial conversion for defaultCodeFormat)
262
- function getConvertedContent(index: number, originalContent: string, originalLang: string): string {
263
- const format = getCodeBlockFormat(index, originalLang);
264
-
265
- // If format matches original, no conversion needed
266
- if (format === originalLang || !isToggleableLanguage(originalLang)) {
267
- return originalContent;
268
- }
269
-
270
- // Convert from original to target format
271
- try {
272
- let parsed: unknown;
273
-
274
- if (originalLang === "json") {
275
- parsed = JSON.parse(originalContent);
276
- } else if (originalLang === "yaml") {
277
- parsed = parseYAML(originalContent);
278
- }
279
-
280
- if (parsed !== undefined) {
281
- if (format === "json") {
282
- return JSON.stringify(parsed, null, 2);
283
- } else if (format === "yaml") {
284
- return stringifyYAML(parsed as object);
285
- }
286
- }
287
- } catch {
288
- // Conversion failed, return original
289
- }
290
-
291
- return originalContent;
292
- }
293
-
294
- // Set format for a code block (converts content to the new format)
295
- function setCodeBlockFormat(index: number, newFormat: string) {
296
- const token = tokens.value[index];
297
- if (token?.type !== "code_block") return;
298
-
299
- const originalLang = token.language?.toLowerCase() || "json";
300
- const current = getCodeBlockFormat(index, originalLang);
301
-
302
- // No change needed if already in target format
303
- if (current === newFormat) return;
304
-
305
- // Convert the content
306
- try {
307
- // Use the currently displayed content (which may already be converted due to defaultCodeFormat)
308
- const sourceContent = convertedContent[index] || getConvertedContent(index, token.content, originalLang);
309
- let parsed: unknown;
310
-
311
- // Parse from current format
312
- if (current === "json") {
313
- parsed = JSON.parse(sourceContent);
314
- } else {
315
- parsed = parseYAML(sourceContent);
316
- }
317
-
318
- // Convert to new format
319
- if (newFormat === "json") {
320
- convertedContent[index] = JSON.stringify(parsed, null, 2);
321
- } else {
322
- convertedContent[index] = stringifyYAML(parsed as object);
323
- }
324
-
325
- codeBlockFormats[index] = newFormat;
326
- } catch {
327
- // If conversion fails, just set the format without converting
328
- codeBlockFormats[index] = newFormat;
329
- }
330
- }
331
-
332
- // Highlight code block content based on format
333
- function highlightCodeBlock(index: number, originalContent: string, originalLanguage: string): string {
334
- const format = getCodeBlockFormat(index, originalLanguage);
335
- const originalLang = originalLanguage?.toLowerCase() || "text";
336
-
337
- // Get the content (converted if needed, or from cache if user toggled)
338
- const content = convertedContent[index] || getConvertedContent(index, originalContent, originalLang);
339
-
340
- // Apply syntax highlighting
341
- switch (format) {
342
- case "json":
343
- return highlightJSON(content);
344
- case "yaml":
345
- return highlightYAML(content);
346
- default:
347
- // For other languages, just escape HTML
348
- return content
349
- .replace(/&/g, "&amp;")
350
- .replace(/</g, "&lt;")
351
- .replace(/>/g, "&gt;");
352
- }
353
- }
354
-
355
234
  // Parse inline markdown (bold, italic, links, etc.)
356
235
  function parseInlineContent(text: string): string {
357
236
  return parseInline(text, true);
@@ -383,23 +262,12 @@ function renderBlockquote(content: string): string {
383
262
  </script>
384
263
 
385
264
  <style lang="scss">
386
- .dx-markdown-code-block {
387
- position: relative;
265
+ .markdown-code-block {
388
266
  margin: 1em 0;
389
267
 
390
- pre {
391
- margin: 0;
392
- background: rgba(0, 0, 0, 0.3);
393
- padding: 1em;
394
- border-radius: 6px;
395
- overflow-x: auto;
396
-
397
- code {
398
- background: transparent;
399
- padding: 0;
400
- font-size: 0.875em;
401
- font-family: 'Fira Code', 'Monaco', monospace;
402
- }
268
+ // Ensure auto-height instead of 100%
269
+ &.dx-code-viewer {
270
+ height: auto;
403
271
  }
404
272
  }
405
273
  </style>
@@ -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>