quasar-ui-danx 0.4.95 → 0.5.0

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 (55) hide show
  1. package/dist/danx.es.js +25284 -23176
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +133 -120
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +4 -2
  7. package/scripts/publish.sh +76 -0
  8. package/src/components/Utility/Buttons/ActionButton.vue +11 -3
  9. package/src/components/Utility/Code/CodeViewer.vue +219 -0
  10. package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
  11. package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
  12. package/src/components/Utility/Code/LanguageBadge.vue +122 -0
  13. package/src/components/Utility/Code/MarkdownContent.vue +405 -0
  14. package/src/components/Utility/Code/index.ts +5 -0
  15. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
  16. package/src/components/Utility/Files/CarouselHeader.vue +24 -0
  17. package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
  18. package/src/components/Utility/Files/FilePreview.vue +118 -166
  19. package/src/components/Utility/Files/index.ts +1 -0
  20. package/src/components/Utility/index.ts +1 -0
  21. package/src/composables/index.ts +5 -0
  22. package/src/composables/useCodeFormat.ts +199 -0
  23. package/src/composables/useCodeViewerCollapse.ts +125 -0
  24. package/src/composables/useCodeViewerEditor.ts +420 -0
  25. package/src/composables/useFilePreview.ts +119 -0
  26. package/src/composables/useTranscodeLoader.ts +68 -0
  27. package/src/helpers/formats/highlightSyntax.ts +327 -0
  28. package/src/helpers/formats/index.ts +3 -1
  29. package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
  30. package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
  31. package/src/helpers/formats/markdown/index.ts +85 -0
  32. package/src/helpers/formats/markdown/parseInline.ts +124 -0
  33. package/src/helpers/formats/markdown/render/index.ts +92 -0
  34. package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
  35. package/src/helpers/formats/markdown/render/renderList.ts +69 -0
  36. package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
  37. package/src/helpers/formats/markdown/state.ts +58 -0
  38. package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
  39. package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
  40. package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
  41. package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
  42. package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
  43. package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
  44. package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
  45. package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
  46. package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
  47. package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
  48. package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
  49. package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
  50. package/src/helpers/formats/markdown/types.ts +63 -0
  51. package/src/styles/danx.scss +4 -0
  52. package/src/styles/themes/danx/code.scss +158 -0
  53. package/src/styles/themes/danx/index.scss +2 -0
  54. package/src/styles/themes/danx/markdown.scss +241 -0
  55. package/src/styles/themes/danx/scrollbar.scss +125 -0
@@ -0,0 +1,405 @@
1
+ <template>
2
+ <div class="dx-markdown-content">
3
+ <template v-for="(token, index) in tokens" :key="index">
4
+ <!-- Headings -->
5
+ <component
6
+ v-if="token.type === 'heading'"
7
+ :is="'h' + token.level"
8
+ v-html="parseInlineContent(token.content)"
9
+ />
10
+
11
+ <!-- Code blocks with syntax highlighting -->
12
+ <div
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>
35
+
36
+ <!-- Blockquotes (recursive) -->
37
+ <blockquote
38
+ v-else-if="token.type === 'blockquote'"
39
+ v-html="renderBlockquote(token.content)"
40
+ />
41
+
42
+ <!-- Unordered lists -->
43
+ <ul v-else-if="token.type === 'ul'">
44
+ <li
45
+ v-for="(item, itemIndex) in token.items"
46
+ :key="itemIndex"
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>
70
+ </ul>
71
+
72
+ <!-- Ordered lists -->
73
+ <ol
74
+ v-else-if="token.type === 'ol'"
75
+ :start="token.start"
76
+ >
77
+ <li
78
+ v-for="(item, itemIndex) in token.items"
79
+ :key="itemIndex"
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>
103
+ </ol>
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
+
160
+ <!-- Horizontal rules -->
161
+ <hr v-else-if="token.type === 'hr'" />
162
+
163
+ <!-- Paragraphs -->
164
+ <p
165
+ v-else-if="token.type === 'paragraph'"
166
+ v-html="parseInlineContent(token.content).replace(/\n/g, '<br />')"
167
+ />
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>
185
+ </div>
186
+ </template>
187
+
188
+ <script setup lang="ts">
189
+ import { computed, reactive } from "vue";
190
+ import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
191
+ import { tokenizeBlocks, parseInline, renderMarkdown, getFootnotes, resetParserState } from "../../../helpers/formats/markdown";
192
+ import type { BlockToken, ListItem } from "../../../helpers/formats/markdown";
193
+ import { highlightJSON, highlightYAML } from "../../../helpers/formats/highlightSyntax";
194
+ import LanguageBadge from "./LanguageBadge.vue";
195
+
196
+ export interface MarkdownContentProps {
197
+ content: string;
198
+ defaultCodeFormat?: "json" | "yaml";
199
+ }
200
+
201
+ const props = withDefaults(defineProps<MarkdownContentProps>(), {
202
+ content: ""
203
+ });
204
+
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>>({});
209
+
210
+ // Tokenize the markdown content
211
+ const tokens = computed<BlockToken[]>(() => {
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();
216
+ return tokenizeBlocks(props.content);
217
+ });
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
+
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
+ // Parse inline markdown (bold, italic, links, etc.)
356
+ function parseInlineContent(text: string): string {
357
+ return parseInline(text, true);
358
+ }
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
+
378
+ // Render blockquote content (can contain nested markdown)
379
+ // Use preserveState to keep link refs and footnotes from parent document
380
+ function renderBlockquote(content: string): string {
381
+ return renderMarkdown(content, { preserveState: true });
382
+ }
383
+ </script>
384
+
385
+ <style lang="scss">
386
+ .dx-markdown-code-block {
387
+ position: relative;
388
+ margin: 1em 0;
389
+
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
+ }
403
+ }
404
+ }
405
+ </style>
@@ -0,0 +1,5 @@
1
+ export { default as CodeViewer } from "./CodeViewer.vue";
2
+ export { default as CodeViewerCollapsed } from "./CodeViewerCollapsed.vue";
3
+ export { default as CodeViewerFooter } from "./CodeViewerFooter.vue";
4
+ export { default as LanguageBadge } from "./LanguageBadge.vue";
5
+ export { default as MarkdownContent } from "./MarkdownContent.vue";
@@ -10,58 +10,105 @@
10
10
  <!-- Header with filename and navigation -->
11
11
  <CarouselHeader
12
12
  v-if="currentFile"
13
- :filename="currentFile.filename || currentFile.name"
13
+ :filename="filename"
14
14
  :show-back-button="hasParent"
15
+ :show-metadata-button="hasMetadata"
16
+ :metadata-count="metadataKeyCount"
15
17
  :show-transcodes-button="!!(currentFile.transcodes && currentFile.transcodes.length > 0)"
16
18
  :transcodes-count="currentFile.transcodes?.length || 0"
17
19
  @back="navigateToParent"
20
+ @metadata="onMetadataClick"
18
21
  @transcodes="showTranscodeNav = true"
19
22
  />
20
23
 
21
- <!-- Carousel -->
22
- <div class="flex-grow relative">
23
- <div class="absolute inset-0">
24
+ <!-- Content Area with optional split panel -->
25
+ <div class="flex-grow flex relative min-h-0">
26
+ <!-- Carousel Section -->
27
+ <div class="flex-grow relative">
28
+ <div class="absolute inset-0">
29
+ <div
30
+ v-for="slide in visibleSlides"
31
+ :key="slide.file.id"
32
+ :class="[
33
+ 'absolute inset-0 flex items-center justify-center transition-opacity duration-300',
34
+ slide.isActive ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
35
+ ]"
36
+ >
37
+ <FileRenderer
38
+ :file="slide.file"
39
+ :autoplay="slide.isActive"
40
+ />
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Navigation Arrows -->
24
45
  <div
25
- v-for="slide in visibleSlides"
26
- :key="slide.file.id"
27
- :class="[
28
- 'absolute inset-0 flex items-center justify-center transition-opacity duration-300',
29
- slide.isActive ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
30
- ]"
46
+ v-if="canNavigatePrevious"
47
+ class="absolute left-4 top-1/2 -translate-y-1/2 z-20"
31
48
  >
32
- <FileRenderer
33
- :file="slide.file"
34
- :autoplay="slide.isActive"
35
- />
49
+ <QBtn
50
+ round
51
+ size="lg"
52
+ class="bg-slate-800 text-white opacity-70 hover:opacity-100"
53
+ @click="navigatePrevious"
54
+ >
55
+ <ChevronLeftIcon class="w-8" />
56
+ </QBtn>
36
57
  </div>
37
- </div>
38
-
39
- <!-- Navigation Arrows -->
40
- <div
41
- v-if="canNavigatePrevious"
42
- class="absolute left-4 top-1/2 -translate-y-1/2 z-20"
43
- >
44
- <QBtn
45
- round
46
- size="lg"
47
- class="bg-slate-800 text-white opacity-70 hover:opacity-100"
48
- @click="navigatePrevious"
58
+ <div
59
+ v-if="canNavigateNext"
60
+ class="absolute right-4 top-1/2 -translate-y-1/2 z-20"
49
61
  >
50
- <ChevronLeftIcon class="w-8" />
51
- </QBtn>
62
+ <QBtn
63
+ round
64
+ size="lg"
65
+ class="bg-slate-800 text-white opacity-70 hover:opacity-100"
66
+ @click="navigateNext"
67
+ >
68
+ <ChevronRightIcon class="w-8" />
69
+ </QBtn>
70
+ </div>
52
71
  </div>
72
+
73
+ <!-- Split Metadata Panel -->
53
74
  <div
54
- v-if="canNavigateNext"
55
- class="absolute right-4 top-1/2 -translate-y-1/2 z-20"
75
+ v-if="metadataSplitMode && showMetadataDialog && hasMetadata"
76
+ class="w-[40%] max-w-[600px] min-w-[300px] bg-slate-900 border-l border-slate-700 flex flex-col"
56
77
  >
57
- <QBtn
58
- round
59
- size="lg"
60
- class="bg-slate-800 text-white opacity-70 hover:opacity-100"
61
- @click="navigateNext"
62
- >
63
- <ChevronRightIcon class="w-8" />
64
- </QBtn>
78
+ <div class="px-4 py-3 bg-slate-800 border-b border-slate-700 flex items-center justify-between flex-shrink-0">
79
+ <h3 class="text-slate-200 font-medium">Metadata</h3>
80
+ <div class="flex items-center gap-1">
81
+ <QBtn
82
+ flat
83
+ dense
84
+ round
85
+ size="sm"
86
+ class="text-slate-400 hover:text-white hover:bg-slate-700"
87
+ @click="undockToModal"
88
+ >
89
+ <UndockIcon class="w-4 h-4" />
90
+ <QTooltip>Undock to modal</QTooltip>
91
+ </QBtn>
92
+ <QBtn
93
+ flat
94
+ dense
95
+ round
96
+ size="sm"
97
+ class="text-slate-400 hover:text-white hover:bg-slate-700"
98
+ @click="showMetadataDialog = false"
99
+ >
100
+ <CloseIcon class="w-4 h-4" />
101
+ <QTooltip>Close</QTooltip>
102
+ </QBtn>
103
+ </div>
104
+ </div>
105
+ <div class="flex-1 min-h-0 p-4">
106
+ <CodeViewer
107
+ :model-value="filteredMetadata"
108
+ :readonly="true"
109
+ format="yaml"
110
+ />
111
+ </div>
65
112
  </div>
66
113
  </div>
67
114
 
@@ -88,19 +135,35 @@
88
135
  :transcodes="currentFile.transcodes"
89
136
  @select="onSelectTranscode"
90
137
  />
138
+
139
+ <!-- Metadata Dialog (only in modal mode) -->
140
+ <FileMetadataDialog
141
+ v-if="showMetadataDialog && !metadataSplitMode"
142
+ :filename="filename"
143
+ :mime-type="mimeType"
144
+ :metadata="filteredMetadata"
145
+ :show-dock-button="true"
146
+ @close="showMetadataDialog = false"
147
+ @dock="dockToSplit"
148
+ />
91
149
  </div>
92
150
  </QDialog>
93
151
  </template>
94
152
 
95
153
  <script setup lang="ts">
96
154
  import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/outline";
155
+ import { FaSolidWindowMaximize as UndockIcon } from "danx-icon";
97
156
  import { ref } from "vue";
98
157
  import { useFileNavigation } from "../../../composables/useFileNavigation";
158
+ import { useFilePreview } from "../../../composables/useFilePreview";
99
159
  import { useKeyboardNavigation } from "../../../composables/useKeyboardNavigation";
100
160
  import { useVirtualCarousel } from "../../../composables/useVirtualCarousel";
161
+ import { getItem, setItem } from "../../../helpers";
101
162
  import { XIcon as CloseIcon } from "../../../svg";
102
163
  import { UploadedFile } from "../../../types";
164
+ import { CodeViewer } from "../Code";
103
165
  import CarouselHeader from "../Files/CarouselHeader.vue";
166
+ import FileMetadataDialog from "../Files/FileMetadataDialog.vue";
104
167
  import FileRenderer from "../Files/FileRenderer.vue";
105
168
  import ThumbnailStrip from "../Files/ThumbnailStrip.vue";
106
169
  import TranscodeNavigator from "../Files/TranscodeNavigator.vue";
@@ -155,4 +218,37 @@ function onSelectTranscode(transcode: UploadedFile, index: number) {
155
218
  diveInto(transcode, currentFile.value.transcodes);
156
219
  }
157
220
  }
221
+
222
+ // Metadata navigation
223
+ const showMetadataDialog = ref(false);
224
+ const METADATA_MODE_KEY = "danx-file-preview-metadata-mode";
225
+ const metadataSplitMode = ref(getItem(METADATA_MODE_KEY, false));
226
+
227
+ function onMetadataClick() {
228
+ // Toggle metadata visibility - mode determines whether it shows as split or modal
229
+ showMetadataDialog.value = !showMetadataDialog.value;
230
+ }
231
+
232
+ function dockToSplit() {
233
+ // Switch from modal to split view
234
+ metadataSplitMode.value = true;
235
+ setItem(METADATA_MODE_KEY, true);
236
+ // Keep showMetadataDialog true so it shows in split mode
237
+ }
238
+
239
+ function undockToModal() {
240
+ // Switch from split to modal view
241
+ metadataSplitMode.value = false;
242
+ setItem(METADATA_MODE_KEY, false);
243
+ // Keep showMetadataDialog true so it shows as modal
244
+ }
245
+
246
+ // Use file preview composable for metadata
247
+ const {
248
+ filename,
249
+ mimeType,
250
+ hasMetadata,
251
+ metadataKeyCount,
252
+ filteredMetadata
253
+ } = useFilePreview({ file: currentFile });
158
254
  </script>
@@ -22,6 +22,24 @@
22
22
  {{ filename }}
23
23
  </div>
24
24
 
25
+ <!-- Metadata Button -->
26
+ <QBtn
27
+ v-if="showMetadataButton"
28
+ flat
29
+ dense
30
+ class="bg-purple-700 text-purple-200 hover:bg-purple-600"
31
+ @click="$emit('metadata')"
32
+ >
33
+ <div class="flex items-center flex-nowrap gap-1">
34
+ <MetaIcon class="w-4" />
35
+ <QBadge
36
+ class="bg-purple-900 text-purple-200"
37
+ :label="metadataCount"
38
+ />
39
+ <span class="text-sm ml-1">Metadata</span>
40
+ </div>
41
+ </QBtn>
42
+
25
43
  <!-- Transcodes Button -->
26
44
  <QBtn
27
45
  v-if="showTranscodesButton"
@@ -57,16 +75,21 @@
57
75
 
58
76
  <script setup lang="ts">
59
77
  import { ArrowLeftIcon, FilmIcon } from "@heroicons/vue/outline";
78
+ import { FaSolidBarcode as MetaIcon } from "danx-icon";
60
79
  import { XIcon as CloseIcon } from "../../../svg";
61
80
 
62
81
  withDefaults(defineProps<{
63
82
  filename: string;
64
83
  showBackButton?: boolean;
84
+ showMetadataButton?: boolean;
85
+ metadataCount?: number;
65
86
  showTranscodesButton?: boolean;
66
87
  transcodesCount?: number;
67
88
  showCloseButton?: boolean;
68
89
  }>(), {
69
90
  showBackButton: false,
91
+ showMetadataButton: false,
92
+ metadataCount: 0,
70
93
  showTranscodesButton: false,
71
94
  transcodesCount: 0,
72
95
  showCloseButton: false
@@ -74,6 +97,7 @@ withDefaults(defineProps<{
74
97
 
75
98
  defineEmits<{
76
99
  'back': [];
100
+ 'metadata': [];
77
101
  'transcodes': [];
78
102
  'close': [];
79
103
  }>();