diff --git a/.gitignore b/.gitignore index 892c0eec..1af9b4b5 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,6 @@ apps/db-agent # Generated htpasswd files (hashes are safe to store in secrets/CI, plaintext never committed) .htpasswd -*.htpasswd \ No newline at end of file +*.htpasswd + +output.css diff --git a/apps/dashboard/src/app.css b/apps/dashboard/src/app.css index a76b25ed..a1b3aaca 100644 --- a/apps/dashboard/src/app.css +++ b/apps/dashboard/src/app.css @@ -26,6 +26,10 @@ z-index: 150; } +@utility z-modal { + z-index: 200; +} + @utility z-menu-elevated { z-index: 250; } diff --git a/apps/dashboard/src/lib/features/course/components/lesson/document/add-document-modal.svelte b/apps/dashboard/src/lib/features/course/components/lesson/document/add-document-modal.svelte index 5a6066a1..58411b2b 100644 --- a/apps/dashboard/src/lib/features/course/components/lesson/document/add-document-modal.svelte +++ b/apps/dashboard/src/lib/features/course/components/lesson/document/add-document-modal.svelte @@ -1,5 +1,5 @@ - { - if (!isOpen) onClose(); - }} -> + {$t('course.navItem.lessons.materials.tabs.document.upload_title')} diff --git a/apps/dashboard/src/lib/features/course/components/lesson/document/document-card-dropdown.svelte b/apps/dashboard/src/lib/features/course/components/lesson/document/document-card-dropdown.svelte deleted file mode 100644 index 47b8ba7a..00000000 --- a/apps/dashboard/src/lib/features/course/components/lesson/document/document-card-dropdown.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - - e.stopPropagation()} - > - - - - - - - {$t('course.navItem.lessons.materials.tabs.document.remove_document')} - - - diff --git a/apps/dashboard/src/lib/features/course/components/lesson/document/document-card.svelte b/apps/dashboard/src/lib/features/course/components/lesson/document/document-card.svelte deleted file mode 100644 index f7739e60..00000000 --- a/apps/dashboard/src/lib/features/course/components/lesson/document/document-card.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - - {#snippet header()} - {#if isEditMode} - - {/if} - {/snippet} - {#snippet actions()} - {#if doc.type === 'pdf'} - - {:else} - - {$t('course.navItem.lessons.materials.tabs.document.view_document')} - - {/if} - - {/snippet} - diff --git a/apps/dashboard/src/lib/features/course/components/lesson/document/document-list.svelte b/apps/dashboard/src/lib/features/course/components/lesson/document/document-list.svelte index 720f1cb8..883bea66 100644 --- a/apps/dashboard/src/lib/features/course/components/lesson/document/document-list.svelte +++ b/apps/dashboard/src/lib/features/course/components/lesson/document/document-list.svelte @@ -1,35 +1,80 @@ {#if mode === MODES.edit} @@ -41,21 +86,16 @@ {/if} {#if !isEmpty(displayDocuments)} - {@const isEditMode = mode === MODES.edit} - - {#each displayDocuments as document, index} - (isEditMode ? requestRemoveDocument(index) : undefined)} - {onViewPDF} - onDownload={downloadDocument} - /> - {/each} - + {:else if mode === MODES.edit} ({ + id: getDocumentAttachmentId(document, index), + name: document.name, + size: document.size, + type: document.type + })); +} + +export function getDocumentIndexByAttachmentId(documents: LessonDocument[], attachmentId: string): number { + return documents.findIndex((document, index) => getDocumentAttachmentId(document, index) === attachmentId); +} + +export function mapAttachmentsToDocuments( + documents: LessonDocument[], + attachments: AttachmentListFile[] +): LessonDocument[] { + return attachments + .map((attachment) => { + const index = getDocumentIndexByAttachmentId(documents, attachment.id); + + if (index === -1) return null; + + return documents[index]; + }) + .filter((document): document is LessonDocument => document != null); +} diff --git a/apps/dashboard/src/lib/features/course/components/lesson/document/document.svelte b/apps/dashboard/src/lib/features/course/components/lesson/document/document.svelte index dfe271df..90bec091 100644 --- a/apps/dashboard/src/lib/features/course/components/lesson/document/document.svelte +++ b/apps/dashboard/src/lib/features/course/components/lesson/document/document.svelte @@ -20,7 +20,6 @@ let { mode = MODES.view }: Props = $props(); - let downloadingDocuments = $state(new Set()); let openDeleteDocumentModal = $state(false); let documentIndexToDelete = $state(null); let viewingPDF: any = $state(null); @@ -83,9 +82,6 @@ } async function downloadDocument(doc: LessonDocument) { - downloadingDocuments.add(doc.name); - downloadingDocuments = downloadingDocuments; - if (!doc.key) { snackbar.error('Document not loaded correctly'); return; @@ -105,12 +101,10 @@ a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); + snackbar.success($t('course.navItem.lessons.materials.tabs.document.download_success')); } catch (error) { console.error('Error downloading document:', error); - window.open(doc.link, '_blank'); - } finally { - downloadingDocuments.delete(doc.name); - downloadingDocuments = downloadingDocuments; + snackbar.error($t('course.navItem.lessons.materials.tabs.document.download_error')); } } @@ -255,6 +249,19 @@ viewPDF(doc); } + function handleViewDocument(doc: LessonDocument) { + if (doc.type === 'pdf') { + handleViewPDF(doc); + return; + } + + window.open(doc.link, '_blank', 'noopener,noreferrer'); + } + + function reorderDocuments(documents: LessonDocument[]) { + lessonApi.updateLessonState('documents', documents); + } + function closePDFViewer() { // Cancel any ongoing render task if (currentRenderTask) { @@ -320,19 +327,19 @@ {#if pdfViewerOpen} -
+
diff --git a/apps/dashboard/src/lib/features/course/components/sidebar/constants.ts b/apps/dashboard/src/lib/features/course/components/sidebar/constants.ts index c8645097..7fe6bc02 100644 --- a/apps/dashboard/src/lib/features/course/components/sidebar/constants.ts +++ b/apps/dashboard/src/lib/features/course/components/sidebar/constants.ts @@ -15,6 +15,6 @@ export const NAV_IDS = { }; export const COURSE_SIDEBAR_DEFAULT_WIDTH = 320; -export const COURSE_SIDEBAR_MIN_WIDTH = 220; +export const COURSE_SIDEBAR_MIN_WIDTH = 280; export const COURSE_SIDEBAR_MAX_WIDTH = 420; export const COURSE_SIDEBAR_STORAGE_KEY = 'classroomio-course-sidebar-width'; diff --git a/apps/dashboard/src/lib/utils/translations/da.json b/apps/dashboard/src/lib/utils/translations/da.json index fb08f39a..bf38273b 100644 --- a/apps/dashboard/src/lib/utils/translations/da.json +++ b/apps/dashboard/src/lib/utils/translations/da.json @@ -2007,6 +2007,8 @@ "cancel_upload": "Annuller upload", "upload_cancelled": "Upload annulleret af brugeren", "downloading": "Downloader...", + "download_success": "Dokument downloadet", + "download_error": "Kunne ikke downloade dokumentet", "view_pdf": "Se PDF", "remove_document": "Fjern dokumentet", "delete": "Slet", @@ -2016,7 +2018,11 @@ "failed_to_render_pdf": "Kunne ikke gengive PDF-siden", "use_arrow_keys": "Brug piletasterne til at navigere, +/- for at zoome, eller klik på kontrollerne ovenfor. Højreklik er deaktiveret.", "empty_title": "Ingen dokumenter endnu", - "empty_description": "Upload dokumenter for at dele med dine elever" + "empty_description": "Upload dokumenter for at dele med dine elever", + "attachments_heading": "Vedhæftede filer", + "file_count": "{count, plural, one {# fil} other {# filer}}", + "view_file": "Vis fil", + "reorder_file": "Omarranger fil" }, "video": { "title": "Video", diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index 8e99ce55..e4301307 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -2007,6 +2007,8 @@ "cancel_upload": "Hochladen abbrechen", "upload_cancelled": "Der Upload wurde vom Benutzer abgebrochen", "downloading": "Herunterladen...", + "download_success": "Dokument erfolgreich heruntergeladen", + "download_error": "Dokument konnte nicht heruntergeladen werden", "view_pdf": "PDF ansehen", "remove_document": "Dokument entfernen", "delete": "Löschen", @@ -2016,7 +2018,11 @@ "failed_to_render_pdf": "PDF-Seite konnte nicht gerendert werden", "use_arrow_keys": "Verwenden Sie die Pfeiltasten zum Navigieren, +/- zum Zoomen oder klicken Sie auf die Steuerelemente oben. Der Rechtsklick ist deaktiviert.", "empty_title": "Noch keine Dokumente", - "empty_description": "Laden Sie Dokumente hoch, um sie mit Ihren Schülern zu teilen" + "empty_description": "Laden Sie Dokumente hoch, um sie mit Ihren Schülern zu teilen", + "attachments_heading": "Anhänge", + "file_count": "{count, plural, one {# Datei} other {# Dateien}}", + "view_file": "Datei anzeigen", + "reorder_file": "Datei neu anordnen" }, "video": { "title": "Video", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index 52f3dec0..178ce8c2 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -2448,6 +2448,8 @@ "cancel_upload": "Cancel Upload", "upload_cancelled": "Upload cancelled by user", "downloading": "Downloading...", + "download_success": "Document downloaded successfully", + "download_error": "Failed to download document", "view_pdf": "View PDF", "remove_document": "Remove document", "delete": "Delete", @@ -2457,7 +2459,11 @@ "failed_to_render_pdf": "Failed to render PDF page", "use_arrow_keys": "Use arrow keys to navigate, +/- to zoom, or click the controls above. Right-click is disabled.", "empty_title": "No documents yet", - "empty_description": "Upload documents to share with your students" + "empty_description": "Upload documents to share with your students", + "attachments_heading": "Attachments", + "file_count": "{count, plural, one {# file} other {# files}}", + "view_file": "View file", + "reorder_file": "Reorder file" } }, "button_done": "Done" diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index 05245e7d..09a42a31 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -2007,6 +2007,8 @@ "cancel_upload": "Cancelar carga", "upload_cancelled": "Subida cancelada por el usuario", "downloading": "Descargando...", + "download_success": "Documento descargado correctamente", + "download_error": "No se pudo descargar el documento", "view_pdf": "Ver PDF", "remove_document": "Quitar documento", "delete": "Eliminar", @@ -2016,7 +2018,11 @@ "failed_to_render_pdf": "No se pudo renderizar la página PDF", "use_arrow_keys": "Utilice las teclas de flecha para navegar, +/- para hacer zoom o haga clic en los controles de arriba. El clic derecho está deshabilitado.", "empty_title": "Aún no hay documentos", - "empty_description": "Cargue documentos para compartir con sus estudiantes" + "empty_description": "Cargue documentos para compartir con sus estudiantes", + "attachments_heading": "Adjuntos", + "file_count": "{count, plural, one {# archivo} other {# archivos}}", + "view_file": "Ver archivo", + "reorder_file": "Reordenar archivo" }, "video": { "title": "Video", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index 37f775b0..196b71d8 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -2007,6 +2007,8 @@ "cancel_upload": "Annuler le téléchargement", "upload_cancelled": "Téléchargement annulé par l'utilisateur", "downloading": "Téléchargement...", + "download_success": "Document téléchargé avec succès", + "download_error": "Échec du téléchargement du document", "view_pdf": "Afficher le PDF", "remove_document": "Supprimer le document", "delete": "Supprimer", @@ -2016,7 +2018,11 @@ "failed_to_render_pdf": "Échec du rendu de la page PDF", "use_arrow_keys": "Utilisez les touches fléchées pour naviguer, +/- pour zoomer ou cliquez sur les commandes ci-dessus. Le clic droit est désactivé.", "empty_title": "Aucun document pour l'instant", - "empty_description": "Téléchargez des documents à partager avec vos étudiants" + "empty_description": "Téléchargez des documents à partager avec vos étudiants", + "attachments_heading": "Pièces jointes", + "file_count": "{count, plural, one {# fichier} other {# fichiers}}", + "view_file": "Voir le fichier", + "reorder_file": "Réordonner le fichier" }, "video": { "title": "Vidéo", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index 165c2316..dcb8466f 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -2007,6 +2007,8 @@ "cancel_upload": "अपलोड रद्द करें", "upload_cancelled": "उपयोगकर्ता द्वारा अपलोड रद्द कर दिया गया", "downloading": "डाउनलोड हो रहा है...", + "download_success": "दस्तावेज़ सफलतापूर्वक डाउनलोड हुआ", + "download_error": "दस्तावेज़ डाउनलोड करने में विफल", "view_pdf": "पीडीएफ देखें", "remove_document": "दस्तावेज़ हटाएँ", "delete": "हटाएँ", @@ -2016,7 +2018,11 @@ "failed_to_render_pdf": "पीडीएफ पेज प्रस्तुत करने में विफल", "use_arrow_keys": "नेविगेट करने के लिए तीर कुंजियों का उपयोग करें, ज़ूम करने के लिए +/- का उपयोग करें, या ऊपर दिए गए नियंत्रणों पर क्लिक करें। राइट-क्लिक अक्षम है.", "empty_title": "अभी तक कोई दस्तावेज़ नहीं", - "empty_description": "अपने छात्रों के साथ साझा करने के लिए दस्तावेज़ अपलोड करें" + "empty_description": "अपने छात्रों के साथ साझा करने के लिए दस्तावेज़ अपलोड करें", + "attachments_heading": "अटैचमेंट", + "file_count": "{count, plural, one {# फ़ाइल} other {# फ़ाइलें}}", + "view_file": "फ़ाइल देखें", + "reorder_file": "फ़ाइल को पुनः क्रमित करें" }, "video": { "title": "वीडियो", diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json index ee644b9d..79214763 100644 --- a/apps/dashboard/src/lib/utils/translations/pl.json +++ b/apps/dashboard/src/lib/utils/translations/pl.json @@ -2007,6 +2007,8 @@ "cancel_upload": "Anuluj przesyłanie", "upload_cancelled": "Przesyłanie anulowane przez użytkownika", "downloading": "Pobieram...", + "download_success": "Dokument pobrano pomyślnie", + "download_error": "Nie udało się pobrać dokumentu", "view_pdf": "Zobacz PDF", "remove_document": "Usuń dokument", "delete": "Usuń", @@ -2016,7 +2018,11 @@ "failed_to_render_pdf": "Nie udało się wyrenderować strony PDF", "use_arrow_keys": "Użyj klawiszy strzałek do nawigacji, +/-, aby powiększyć, lub kliknij elementy sterujące powyżej. Kliknięcie prawym przyciskiem myszy jest wyłączone.", "empty_title": "Nie ma jeszcze dokumentów", - "empty_description": "Prześlij dokumenty, aby udostępnić je swoim uczniom" + "empty_description": "Prześlij dokumenty, aby udostępnić je swoim uczniom", + "attachments_heading": "Załączniki", + "file_count": "{count, plural, one {# plik} few {# pliki} many {# plików} other {# plika}}", + "view_file": "Wyświetl plik", + "reorder_file": "Zmień kolejność pliku" }, "video": { "title": "Wideo", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index 583e131f..10e776f3 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -2007,6 +2007,8 @@ "cancel_upload": "Cancelar upload", "upload_cancelled": "Upload cancelado pelo usuário", "downloading": "Baixando...", + "download_success": "Documento baixado com sucesso", + "download_error": "Falha ao baixar o documento", "view_pdf": "Ver PDF", "remove_document": "Remover documento", "delete": "Excluir", @@ -2016,7 +2018,11 @@ "failed_to_render_pdf": "Falha ao renderizar a página PDF", "use_arrow_keys": "Use as teclas de seta para navegar, +/- para ampliar ou clique nos controles acima. O clique com o botão direito está desativado.", "empty_title": "Ainda não há documentos", - "empty_description": "Faça upload de documentos para compartilhar com seus alunos" + "empty_description": "Faça upload de documentos para compartilhar com seus alunos", + "attachments_heading": "Anexos", + "file_count": "{count, plural, one {# arquivo} other {# arquivos}}", + "view_file": "Ver arquivo", + "reorder_file": "Reordenar arquivo" }, "video": { "title": "Vídeo", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 18fb1c0f..7b037ec5 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -2007,6 +2007,8 @@ "cancel_upload": "Отменить загрузку", "upload_cancelled": "Загрузка отменена пользователем", "downloading": "Загрузка...", + "download_success": "Документ успешно загружен", + "download_error": "Не удалось загрузить документ", "view_pdf": "Посмотреть PDF", "remove_document": "Удалить документ", "delete": "Удалить", @@ -2016,7 +2018,11 @@ "failed_to_render_pdf": "Не удалось отобразить страницу PDF.", "use_arrow_keys": "Используйте клавиши со стрелками для навигации, +/- для масштабирования или нажимайте элементы управления выше. Щелчок правой кнопкой мыши отключен.", "empty_title": "Документов пока нет", - "empty_description": "Загрузите документы, чтобы поделиться ими со своими учениками" + "empty_description": "Загрузите документы, чтобы поделиться ими со своими учениками", + "attachments_heading": "Вложения", + "file_count": "{count, plural, one {# файл} few {# файла} many {# файлов} other {# файла}}", + "view_file": "Просмотреть файл", + "reorder_file": "Изменить порядок файла" }, "video": { "title": "Видео", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index 28b92d14..5617b438 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -2007,6 +2007,8 @@ "cancel_upload": "Hủy tải lên", "upload_cancelled": "Tải lên bị người dùng hủy", "downloading": "Đang tải xuống...", + "download_success": "Tài liệu đã được tải xuống thành công", + "download_error": "Không thể tải xuống tài liệu", "view_pdf": "Xem PDF", "remove_document": "Xóa tài liệu", "delete": "Xóa", @@ -2016,7 +2018,11 @@ "failed_to_render_pdf": "Không thể hiển thị trang PDF", "use_arrow_keys": "Sử dụng các phím mũi tên để điều hướng, +/- để thu phóng hoặc nhấp vào các điều khiển ở trên. Nhấp chuột phải bị vô hiệu hóa.", "empty_title": "Chưa có tài liệu nào", - "empty_description": "Tải lên tài liệu để chia sẻ với sinh viên của bạn" + "empty_description": "Tải lên tài liệu để chia sẻ với sinh viên của bạn", + "attachments_heading": "Tệp đính kèm", + "file_count": "{count, plural, one {# tệp} other {# tệp}}", + "view_file": "Xem tệp", + "reorder_file": "Sắp xếp lại tệp" }, "video": { "title": "Video", diff --git a/packages/storybook/src/molecules/attachment-list/attachment-list.stories.svelte b/packages/storybook/src/molecules/attachment-list/attachment-list.stories.svelte new file mode 100644 index 00000000..e46d1692 --- /dev/null +++ b/packages/storybook/src/molecules/attachment-list/attachment-list.stories.svelte @@ -0,0 +1,103 @@ + + + + {#snippet template()} +
+ console.log('view', file.name)} + onDownload={(file) => console.log('download', file.name)} + /> +
+ {/snippet} +
+ + + {#snippet template()} +
+ console.log('view', file.name)} + onDelete={(file) => console.log('delete', file.name)} + onReorder={(files) => + console.log( + 'reorder', + files.map((file) => file.name) + )} + /> +
+ {/snippet} +
+ + + {#snippet template()} +
+ console.log('view', file.name)} + onDownload={(file) => console.log('download', file.name)} + /> +
+ {/snippet} +
diff --git a/packages/storybook/src/molecules/attachment-list/fields.ts b/packages/storybook/src/molecules/attachment-list/fields.ts new file mode 100644 index 00000000..3f419594 --- /dev/null +++ b/packages/storybook/src/molecules/attachment-list/fields.ts @@ -0,0 +1 @@ +export const FIELDS = ['mode', 'files', 'labels', 'class']; diff --git a/packages/ui/README.md b/packages/ui/README.md index 554dca0a..e03a1109 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -96,6 +96,10 @@ Learner and author UIs for exercise questions (take, preview, review, submission Marketing / demo widget: left-hand list of question types and a live **take**-mode preview using `ExerciseQuestion.QuestionRenderer`. Copy is English-only (no dashboard i18n). Also consumed by the **`@cio/embeds`** app as a CDN bundle (`apps/embeds`). +### Attachment list (`src/custom/attachment-list/`) + +Presentational list for lesson (or similar) file attachments with **view** and **edit** modes. View mode shows a header (paperclip + title + file count) and rows with view/download icon buttons. Edit mode shows sortable rows (when `onReorder` is provided) with a drag handle, view, and delete actions. Copy is passed via the `labels: AttachmentListLabels` prop (including `reorder` for the drag handle) so dashboard wrappers can supply translated strings. `AttachmentListFile.type` accepts a file extension or MIME type for icon styling. See `Molecules/AttachmentList` in Storybook. + ### Live session card (`src/custom/live-session-card/`) Presentational card for a live-class lesson with three states (`live`, `upcoming`, `ended`) derived from `lessonAt` + `durationMinutes`, or forced via the `status` prop (used by Storybook). Shows a join/copy action set when live, an "Add to calendar" combo button (Google, Outlook.com, Office 365, Yahoo, plus an `.ics` download for Apple) and a countdown when upcoming. All copy is passed in via the `labels: LiveSessionLabels` prop, so the dashboard wrapper supplies translated strings; `onCopyLink` fires after the link is copied (e.g. for a snackbar). See `Molecules/LiveSessionCard` in Storybook. diff --git a/packages/ui/src/custom/attachment-list/attachment-list-row.svelte b/packages/ui/src/custom/attachment-list/attachment-list-row.svelte new file mode 100644 index 00000000..47663f20 --- /dev/null +++ b/packages/ui/src/custom/attachment-list/attachment-list-row.svelte @@ -0,0 +1,119 @@ + + +
+ {#if showDragHandle} +
+
+ {/if} + + + +
+

{file.name}

+ {#if sizeLabel} +

{sizeLabel}

+ {/if} +
+ +
+ {#if onView} + + {/if} + + {#if mode === 'view' && onDownload} + + {:else if mode === 'edit' && onDelete} + + {/if} +
+
diff --git a/packages/ui/src/custom/attachment-list/attachment-list.svelte b/packages/ui/src/custom/attachment-list/attachment-list.svelte new file mode 100644 index 00000000..5c396fa5 --- /dev/null +++ b/packages/ui/src/custom/attachment-list/attachment-list.svelte @@ -0,0 +1,149 @@ + + +
+ {#if mode === 'view'} +
+
+
+ {labels.fileCount} +
+ {/if} + + {#if isReorderable} +
+ {#each displayFiles as file (file.id)} +
+ +
+ {/each} +
+ {:else} +
+ {#each displayFiles as file (file.id)} +
+ +
+ {/each} +
+ {/if} +
diff --git a/packages/ui/src/custom/attachment-list/file-type-style.ts b/packages/ui/src/custom/attachment-list/file-type-style.ts new file mode 100644 index 00000000..1b3b71cf --- /dev/null +++ b/packages/ui/src/custom/attachment-list/file-type-style.ts @@ -0,0 +1,51 @@ +export type FileTypeStyle = { + backgroundClass: string; + iconClass: string; +}; + +const MIME_TO_EXTENSION: Record = { + 'application/pdf': 'pdf', + 'application/msword': 'doc', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx' +}; + +function resolveExtension(typeOrName: string): string { + const normalized = typeOrName.trim().toLowerCase(); + + if (!normalized) return ''; + + const mimeExtension = MIME_TO_EXTENSION[normalized]; + + if (mimeExtension) return mimeExtension; + + if (normalized.includes('/')) return ''; + + if (normalized.includes('.')) { + return normalized.split('.').pop() ?? normalized; + } + + return normalized; +} + +export function getFileTypeStyle(typeOrName: string): FileTypeStyle { + const extension = resolveExtension(typeOrName); + + switch (extension) { + case 'pdf': + return { + backgroundClass: 'ui:bg-destructive/15', + iconClass: 'ui:text-destructive' + }; + case 'doc': + case 'docx': + return { + backgroundClass: 'ui:bg-primary/15', + iconClass: 'ui:text-primary' + }; + default: + return { + backgroundClass: 'ui:bg-muted', + iconClass: 'ui:text-muted-foreground' + }; + } +} diff --git a/packages/ui/src/custom/attachment-list/format-file-size.ts b/packages/ui/src/custom/attachment-list/format-file-size.ts new file mode 100644 index 00000000..30df5dc9 --- /dev/null +++ b/packages/ui/src/custom/attachment-list/format-file-size.ts @@ -0,0 +1,10 @@ +export function formatFileSize(bytes: number): string { + if (!Number.isFinite(bytes) || bytes <= 0) return '0 Bytes'; + + const unit = 1024; + const units = ['Bytes', 'KB', 'MB', 'GB']; + const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(unit)), units.length - 1); + const value = bytes / Math.pow(unit, unitIndex); + + return `${parseFloat(value.toFixed(2))} ${units[unitIndex]}`; +} diff --git a/packages/ui/src/custom/attachment-list/index.ts b/packages/ui/src/custom/attachment-list/index.ts new file mode 100644 index 00000000..2d519b7c --- /dev/null +++ b/packages/ui/src/custom/attachment-list/index.ts @@ -0,0 +1,4 @@ +export { default as AttachmentList } from './attachment-list.svelte'; +export { formatFileSize } from './format-file-size'; +export { getFileTypeStyle } from './file-type-style'; +export type { AttachmentListFile, AttachmentListLabels, AttachmentListMode } from './types'; diff --git a/packages/ui/src/custom/attachment-list/types.ts b/packages/ui/src/custom/attachment-list/types.ts new file mode 100644 index 00000000..7d1a24bd --- /dev/null +++ b/packages/ui/src/custom/attachment-list/types.ts @@ -0,0 +1,18 @@ +export type AttachmentListFile = { + id: string; + name: string; + size?: number; + /** File extension (e.g. `pdf`, `docx`) or MIME type (e.g. `application/pdf`). Used for icon styling. */ + type?: string; +}; + +export type AttachmentListMode = 'view' | 'edit'; + +export type AttachmentListLabels = { + title: string; + fileCount: string; + view: string; + download: string; + delete: string; + reorder: string; +}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 520e1505..81883968 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -91,3 +91,5 @@ export * as CommentTree from './custom/comment-tree'; export * as Certificate from './custom/certificate-preview'; export * as EmailPreview from './custom/email-preview'; export * from './custom/animation'; +export { AttachmentList } from './custom/attachment-list'; +export type { AttachmentListFile, AttachmentListLabels, AttachmentListMode } from './custom/attachment-list';