quasar-ui-danx 0.4.92 → 0.4.94

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.
@@ -0,0 +1,129 @@
1
+ import { computed, ref, Ref } from "vue";
2
+ import { FileNavigationParent, FileNavigationState, UploadedFile } from "../types";
3
+
4
+ /**
5
+ * Composable for managing file navigation state with parent stack support
6
+ * Enables diving into related files (like transcodes) and navigating back to parent
7
+ */
8
+ export function useFileNavigation(initialFile: Ref<UploadedFile | null>, initialRelatedFiles: Ref<UploadedFile[]> = ref([])) {
9
+ // Current navigation state
10
+ const currentFile = ref<UploadedFile | null>(initialFile.value);
11
+ const relatedFiles = ref<UploadedFile[]>(initialRelatedFiles.value);
12
+ const parentStack = ref<FileNavigationParent[]>([]);
13
+ const currentIndex = ref(0);
14
+
15
+ // Computed properties
16
+ const hasParent = computed(() => parentStack.value.length > 0);
17
+ const canNavigatePrevious = computed(() => currentIndex.value > 0);
18
+ const canNavigateNext = computed(() => currentIndex.value < relatedFiles.value.length - 1);
19
+ const totalFiles = computed(() => relatedFiles.value.length);
20
+
21
+ /**
22
+ * Navigate to a specific file by index
23
+ */
24
+ function navigateTo(index: number) {
25
+ if (index >= 0 && index < relatedFiles.value.length) {
26
+ currentIndex.value = index;
27
+ currentFile.value = relatedFiles.value[index];
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Navigate to the next file
33
+ */
34
+ function navigateNext() {
35
+ if (canNavigateNext.value) {
36
+ navigateTo(currentIndex.value + 1);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Navigate to the previous file
42
+ */
43
+ function navigatePrevious() {
44
+ if (canNavigatePrevious.value) {
45
+ navigateTo(currentIndex.value - 1);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Dive into a related file (e.g., transcodes)
51
+ * Pushes current state onto parent stack
52
+ */
53
+ function diveInto(file: UploadedFile, newRelatedFiles: UploadedFile[]) {
54
+ // Push current state onto parent stack
55
+ parentStack.value.push({
56
+ file: currentFile.value!,
57
+ relatedFiles: relatedFiles.value,
58
+ index: currentIndex.value
59
+ });
60
+
61
+ // Update to new state
62
+ currentFile.value = file;
63
+ relatedFiles.value = newRelatedFiles;
64
+ currentIndex.value = newRelatedFiles.findIndex(f => f.id === file.id);
65
+
66
+ // Fallback to 0 if not found
67
+ if (currentIndex.value === -1) {
68
+ currentIndex.value = 0;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Navigate back to parent file
74
+ * Pops state from parent stack
75
+ */
76
+ function navigateToParent() {
77
+ if (hasParent.value) {
78
+ const parent = parentStack.value.pop()!;
79
+ currentFile.value = parent.file;
80
+ relatedFiles.value = parent.relatedFiles;
81
+ currentIndex.value = parent.index;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Reset navigation state
87
+ */
88
+ function reset(file: UploadedFile | null = null, files: UploadedFile[] = []) {
89
+ currentFile.value = file;
90
+ relatedFiles.value = files;
91
+ parentStack.value = [];
92
+ currentIndex.value = 0;
93
+ }
94
+
95
+ /**
96
+ * Get navigation state snapshot
97
+ */
98
+ function getState(): FileNavigationState {
99
+ return {
100
+ currentFile: currentFile.value,
101
+ relatedFiles: relatedFiles.value,
102
+ parentStack: [...parentStack.value],
103
+ currentIndex: currentIndex.value
104
+ };
105
+ }
106
+
107
+ return {
108
+ // State
109
+ currentFile,
110
+ relatedFiles,
111
+ parentStack,
112
+ currentIndex,
113
+
114
+ // Computed
115
+ hasParent,
116
+ canNavigatePrevious,
117
+ canNavigateNext,
118
+ totalFiles,
119
+
120
+ // Methods
121
+ navigateTo,
122
+ navigateNext,
123
+ navigatePrevious,
124
+ diveInto,
125
+ navigateToParent,
126
+ reset,
127
+ getState
128
+ };
129
+ }
@@ -0,0 +1,58 @@
1
+ import { onMounted, onUnmounted } from "vue";
2
+
3
+ export interface KeyboardNavigationCallbacks {
4
+ onPrevious: () => void;
5
+ onNext: () => void;
6
+ onEscape?: () => void;
7
+ }
8
+
9
+ export interface UseKeyboardNavigationOptions {
10
+ enabled?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Composable for keyboard navigation (arrow keys + escape)
15
+ * Handles left/right arrow keys for navigation and optional escape key
16
+ */
17
+ export function useKeyboardNavigation(
18
+ callbacks: KeyboardNavigationCallbacks,
19
+ options: UseKeyboardNavigationOptions = {}
20
+ ) {
21
+ const { enabled = true } = options;
22
+
23
+ function onKeydown(e: KeyboardEvent) {
24
+ if (!enabled) return;
25
+
26
+ switch (e.key) {
27
+ case "ArrowLeft":
28
+ e.preventDefault();
29
+ callbacks.onPrevious();
30
+ break;
31
+ case "ArrowRight":
32
+ e.preventDefault();
33
+ callbacks.onNext();
34
+ break;
35
+ case "Escape":
36
+ if (callbacks.onEscape) {
37
+ callbacks.onEscape();
38
+ }
39
+ break;
40
+ }
41
+ }
42
+
43
+ onMounted(() => {
44
+ if (enabled) {
45
+ window.addEventListener("keydown", onKeydown);
46
+ }
47
+ });
48
+
49
+ onUnmounted(() => {
50
+ if (enabled) {
51
+ window.removeEventListener("keydown", onKeydown);
52
+ }
53
+ });
54
+
55
+ return {
56
+ onKeydown
57
+ };
58
+ }
@@ -0,0 +1,43 @@
1
+ import { nextTick, Ref, watch } from "vue";
2
+
3
+ export interface UseThumbnailScrollOptions {
4
+ /**
5
+ * Container element that holds the thumbnails
6
+ */
7
+ containerRef: Ref<HTMLElement | null>;
8
+
9
+ /**
10
+ * Current active index
11
+ */
12
+ currentIndex: Ref<number>;
13
+
14
+ /**
15
+ * Optional CSS selector for thumbnail elements (defaults to '.thumbnail')
16
+ */
17
+ thumbnailSelector?: string;
18
+ }
19
+
20
+ /**
21
+ * Composable for auto-scrolling thumbnails into view
22
+ * Watches the current index and scrolls the active thumbnail into the visible area
23
+ */
24
+ export function useThumbnailScroll(options: UseThumbnailScrollOptions) {
25
+ const { containerRef, currentIndex, thumbnailSelector = ".thumbnail" } = options;
26
+
27
+ watch(currentIndex, (newIndex) => {
28
+ nextTick(() => {
29
+ const thumbnails = containerRef.value?.querySelectorAll(thumbnailSelector);
30
+ const activeThumbnail = thumbnails?.[newIndex] as HTMLElement;
31
+
32
+ if (activeThumbnail && containerRef.value) {
33
+ activeThumbnail.scrollIntoView({
34
+ behavior: "smooth",
35
+ block: "nearest",
36
+ inline: "center"
37
+ });
38
+ }
39
+ });
40
+ });
41
+
42
+ return {};
43
+ }
@@ -0,0 +1,93 @@
1
+ import { computed, Ref, shallowRef, watch } from "vue";
2
+ import { UploadedFile, VirtualCarouselSlide } from "../types";
3
+
4
+ const BUFFER_SIZE = 2; // Render current slide ± 2 slides
5
+
6
+ /**
7
+ * Composable for managing virtual carousel rendering
8
+ * Only renders slides within buffer window for performance with large file sets
9
+ */
10
+ export function useVirtualCarousel(
11
+ files: Ref<UploadedFile[]>,
12
+ currentIndex: Ref<number>
13
+ ) {
14
+ // Shallow ref to avoid deep reactivity for performance
15
+ const visibleSlides = shallowRef<VirtualCarouselSlide[]>([]);
16
+
17
+ /**
18
+ * Calculate which slides should be visible based on current index
19
+ */
20
+ const visibleIndices = computed(() => {
21
+ const start = Math.max(0, currentIndex.value - BUFFER_SIZE);
22
+ const end = Math.min(files.value.length - 1, currentIndex.value + BUFFER_SIZE);
23
+ const indices: number[] = [];
24
+
25
+ for (let i = start; i <= end; i++) {
26
+ indices.push(i);
27
+ }
28
+
29
+ return indices;
30
+ });
31
+
32
+ /**
33
+ * Update visible slides based on current index
34
+ */
35
+ function updateVisibleSlides() {
36
+ const indices = visibleIndices.value;
37
+ const newSlides: VirtualCarouselSlide[] = [];
38
+
39
+ for (const index of indices) {
40
+ if (index >= 0 && index < files.value.length) {
41
+ newSlides.push({
42
+ file: files.value[index],
43
+ index,
44
+ isActive: index === currentIndex.value,
45
+ isVisible: true
46
+ });
47
+ }
48
+ }
49
+
50
+ visibleSlides.value = newSlides;
51
+ }
52
+
53
+ /**
54
+ * Check if a slide at given index is visible
55
+ */
56
+ function isSlideVisible(index: number): boolean {
57
+ return visibleIndices.value.includes(index);
58
+ }
59
+
60
+ /**
61
+ * Get slide by index (returns null if not visible)
62
+ */
63
+ function getSlide(index: number): VirtualCarouselSlide | null {
64
+ return visibleSlides.value.find(s => s.index === index) || null;
65
+ }
66
+
67
+ /**
68
+ * Get slides that should be preloaded (visible + buffer)
69
+ */
70
+ const preloadIndices = computed(() => {
71
+ const start = Math.max(0, currentIndex.value - BUFFER_SIZE - 1);
72
+ const end = Math.min(files.value.length - 1, currentIndex.value + BUFFER_SIZE + 1);
73
+ const indices: number[] = [];
74
+
75
+ for (let i = start; i <= end; i++) {
76
+ indices.push(i);
77
+ }
78
+
79
+ return indices;
80
+ });
81
+
82
+ // Watch for changes to update visible slides
83
+ watch([currentIndex, files], updateVisibleSlides, { immediate: true });
84
+
85
+ return {
86
+ visibleSlides,
87
+ visibleIndices,
88
+ preloadIndices,
89
+ isSlideVisible,
90
+ getSlide,
91
+ updateVisibleSlides
92
+ };
93
+ }
@@ -0,0 +1,107 @@
1
+ import { UploadedFile } from "../types";
2
+
3
+ /**
4
+ * Placeholder constants for file preview
5
+ */
6
+ export const FILE_PLACEHOLDERS = {
7
+ IMAGE: "https://placehold.co/64x64?text=?",
8
+ VIDEO_SVG: `data:image/svg+xml;base64,${btoa(
9
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M8 5v14l11-7z"/></svg>'
10
+ )}`
11
+ };
12
+
13
+ /**
14
+ * MIME type regex patterns for file type detection
15
+ */
16
+ export const MIME_TYPES = {
17
+ IMAGE: /^image\//,
18
+ VIDEO: /^video\//,
19
+ TEXT: /^text\//,
20
+ PDF: /^application\/pdf/
21
+ };
22
+
23
+ /**
24
+ * Check if file is an image
25
+ */
26
+ export function isImage(file: UploadedFile): boolean {
27
+ const mimeType = getMimeType(file);
28
+ return MIME_TYPES.IMAGE.test(mimeType);
29
+ }
30
+
31
+ /**
32
+ * Check if file is a video
33
+ */
34
+ export function isVideo(file: UploadedFile): boolean {
35
+ const mimeType = getMimeType(file);
36
+ return MIME_TYPES.VIDEO.test(mimeType);
37
+ }
38
+
39
+ /**
40
+ * Check if file is a text file
41
+ */
42
+ export function isText(file: UploadedFile): boolean {
43
+ const mimeType = getMimeType(file);
44
+ return MIME_TYPES.TEXT.test(mimeType);
45
+ }
46
+
47
+ /**
48
+ * Check if file is a PDF
49
+ */
50
+ export function isPdf(file: UploadedFile): boolean {
51
+ const mimeType = getMimeType(file);
52
+ return MIME_TYPES.PDF.test(mimeType);
53
+ }
54
+
55
+ /**
56
+ * Get the MIME type from a file
57
+ */
58
+ export function getMimeType(file: UploadedFile): string {
59
+ return file.mime || file.type || "";
60
+ }
61
+
62
+ /**
63
+ * Get the file extension from a filename
64
+ */
65
+ export function getFileExtension(filename: string): string {
66
+ return filename.split(".").pop()?.toLowerCase() || "";
67
+ }
68
+
69
+ /**
70
+ * Get the preview URL for a file
71
+ * Priority: optimized > blobUrl > url > thumb > empty string
72
+ */
73
+ export function getPreviewUrl(file: UploadedFile): string {
74
+ if (file.optimized?.url) {
75
+ return file.optimized.url;
76
+ }
77
+
78
+ if (isImage(file)) {
79
+ return file.blobUrl || file.url || "";
80
+ }
81
+
82
+ return file.thumb?.url || "";
83
+ }
84
+
85
+ /**
86
+ * Get the thumbnail URL for a file
87
+ * For videos without thumbs, returns a play icon SVG
88
+ */
89
+ export function getThumbUrl(file: UploadedFile): string {
90
+ if (file.thumb?.url) {
91
+ return file.thumb.url;
92
+ }
93
+
94
+ if (isVideo(file)) {
95
+ return FILE_PLACEHOLDERS.VIDEO_SVG;
96
+ }
97
+
98
+ return getPreviewUrl(file) || FILE_PLACEHOLDERS.IMAGE;
99
+ }
100
+
101
+ /**
102
+ * Get the optimized URL for a file (used for preview)
103
+ * Priority: optimized > blobUrl > url
104
+ */
105
+ export function getOptimizedUrl(file: UploadedFile): string {
106
+ return file.optimized?.url || file.blobUrl || file.url || "";
107
+ }
@@ -63,9 +63,12 @@ export function fMarkdownCode(type: string, string: string | object): string {
63
63
  }
64
64
  }
65
65
 
66
- const regex = new RegExp(`\`\`\`${type}`, "g");
67
66
  string = (string || "") as string;
68
- if (!string.match(regex)) {
67
+ // Check if string STARTS with the code fence (not just contains it anywhere)
68
+ // This fixes a bug where JSON containing embedded code fences in the data
69
+ // would incorrectly be detected as already wrapped
70
+ const startsWithCodeFence = new RegExp(`^\\s*\`\`\`${type}\\s`).test(string);
71
+ if (!startsWithCodeFence) {
69
72
  string = parseMarkdownCode(string as string);
70
73
  return `\`\`\`${type}\n${string}\n\`\`\``;
71
74
  }
@@ -8,6 +8,7 @@ export * from "./download";
8
8
  export * from "./downloadPdf";
9
9
  export * from "./files";
10
10
  export * from "./FileUpload";
11
+ export * from "./filePreviewHelpers";
11
12
  export * from "./FlashMessages";
12
13
  export * from "./formats";
13
14
  export * from "./hotkeys";
@@ -34,6 +34,23 @@ export interface UploadedFile extends TypedObject {
34
34
  meta?: AnyObject;
35
35
  }
36
36
 
37
+ export interface StoredFile extends TypedObject {
38
+ filename: string;
39
+ url: string;
40
+ mime: string;
41
+ size?: number;
42
+ location?: {
43
+ x: number;
44
+ y: number;
45
+ };
46
+ meta?: AnyObject;
47
+ page_number?: number;
48
+ is_transcoding?: boolean;
49
+ thumb?: StoredFile;
50
+ optimized?: StoredFile;
51
+ transcodes?: StoredFile[];
52
+ }
53
+
37
54
  export interface FileUploadCompleteCallbackParams {
38
55
  file?: UploadedFile | null;
39
56
  uploadedFile?: UploadedFile | null;
@@ -60,3 +77,24 @@ export type FileUploadProgressCallback = (params: FileUploadProgressCallbackPara
60
77
  export type FileUploadErrorCallback = (params: FileUploadErrorCallbackParams) => void
61
78
  export type OnFilesChangeCallback = (files: UploadedFile[]) => void;
62
79
  export type VoidCallback = () => void;
80
+
81
+ // File Navigation Types
82
+ export interface FileNavigationState {
83
+ currentFile: UploadedFile | null;
84
+ relatedFiles: UploadedFile[];
85
+ parentStack: FileNavigationParent[];
86
+ currentIndex: number;
87
+ }
88
+
89
+ export interface FileNavigationParent {
90
+ file: UploadedFile;
91
+ relatedFiles: UploadedFile[];
92
+ index: number;
93
+ }
94
+
95
+ export interface VirtualCarouselSlide {
96
+ file: UploadedFile;
97
+ index: number;
98
+ isActive: boolean;
99
+ isVisible: boolean;
100
+ }
@@ -1,5 +1,5 @@
1
1
  export interface LabelPillWidgetProps {
2
2
  label?: string | number;
3
3
  size?: "xs" | "sm" | "md" | "lg";
4
- color?: "sky" | "green" | "red" | "amber" | "yellow" | "blue" | "slate" | "slate-mid" | "gray" | "none";
4
+ color?: "sky" | "green" | "red" | "amber" | "yellow" | "blue" | "purple" | "slate" | "slate-mid" | "gray" | "emerald" | "orange" | "lime" | "teal" | "cyan" | "rose" | "indigo" | "violet" | "fuchsia" | "none";
5
5
  }
package/src/vue-plugin.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./config";
2
2
  export * from "./helpers";
3
3
  export * from "./components";
4
+ export * from "./composables";
4
5
  export * from "./svg";
5
6
 
6
7
  // eslint-disable-next-line import/extensions