@meeovi/layer-shared 1.0.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.
- package/README.md +0 -0
- package/app/components/Gallery/Gallery.vue +187 -0
- package/app/components/Gallery/__tests__/Gallery.spec.ts +14 -0
- package/app/components/Heading/Heading.vue +14 -0
- package/app/components/Heading/__tests__/Heading.spec.ts +14 -0
- package/app/components/Heading/types.ts +5 -0
- package/app/components/media/audioGallery.vue +70 -0
- package/app/components/media/dragDropUpload.vue +67 -0
- package/app/components/media/fullscreenMediaModal.vue +66 -0
- package/app/components/media/imageCard.vue +65 -0
- package/app/components/media/imageGallery.vue +40 -0
- package/app/components/media/mediaCard.vue +89 -0
- package/app/components/media/mediaCarousel.vue +65 -0
- package/app/components/media/mediaFolderSidebar.vue +72 -0
- package/app/components/media/mediaPlayer.vue +40 -0
- package/app/components/media/mediaSearchBar.vue +16 -0
- package/app/components/media/videoGallery.vue +77 -0
- package/app/components/ui/AccordionItem/AccordionItem.vue +24 -0
- package/app/components/ui/AccordionItem/__tests__/AccordionItem.spec.ts +14 -0
- package/app/components/ui/AccordionItem/types.ts +5 -0
- package/app/components/ui/Alert/Alert.vue +34 -0
- package/app/components/ui/Alert/types.ts +5 -0
- package/app/components/ui/Breadcrumbs/Breadcrumbs.vue +76 -0
- package/app/components/ui/Breadcrumbs/__tests__/Breadcrumbs.spec.ts +14 -0
- package/app/components/ui/Breadcrumbs/types.ts +8 -0
- package/app/components/ui/CartProductCard/CartProductCard.vue +66 -0
- package/app/components/ui/CartProductCard/types.ts +18 -0
- package/app/components/ui/CategoryCard/CategoryCard.vue +41 -0
- package/app/components/ui/CategoryCard/types.ts +9 -0
- package/app/components/ui/Display/Display.vue +55 -0
- package/app/components/ui/Display/types.ts +12 -0
- package/app/components/ui/Divider/Divider.vue +3 -0
- package/app/components/ui/Divider/__tests__/Divider.spec.tsx +10 -0
- package/app/components/ui/Footer.vue +92 -0
- package/app/components/ui/Form/FormHelperText.vue +5 -0
- package/app/components/ui/Form/FormLabel.vue +5 -0
- package/app/components/ui/Form/FormPasswordInput.vue +15 -0
- package/app/components/ui/Form/__tests__/FormHelperText.spec.ts +10 -0
- package/app/components/ui/Form/__tests__/FormLabel.spec.ts +10 -0
- package/app/components/ui/Hero/Hero.vue +44 -0
- package/app/components/ui/Hero/types.ts +10 -0
- package/app/components/ui/Modal/Modal.vue +19 -0
- package/app/components/ui/Modal/types.ts +8 -0
- package/app/components/ui/Motionable.vue +45 -0
- package/app/components/ui/NavbarBottom.vue +63 -0
- package/app/components/ui/NavbarTop.vue +25 -0
- package/app/components/ui/Overlay/Overlay.vue +14 -0
- package/app/components/ui/Overlay/__tests__/Overlay.spec.ts +14 -0
- package/app/components/ui/Overlay/types.ts +3 -0
- package/app/components/ui/PageBuilder.vue +39 -0
- package/app/components/ui/PageContainer.vue +5 -0
- package/app/components/ui/Pagination/Pagination.vue +151 -0
- package/app/components/ui/Pagination/__tests__/Pagination.spec.ts +17 -0
- package/app/components/ui/Pagination/types.ts +6 -0
- package/app/components/ui/ProductCard/ProductCard.vue +55 -0
- package/app/components/ui/ProductCard/__tests__/ProductCard.spec.ts +16 -0
- package/app/components/ui/ProductCard/types.ts +12 -0
- package/app/components/ui/ProductCardHorizontal/ProductCardHorizontal.vue +34 -0
- package/app/components/ui/ProductCardHorizontal/__tests__/ProductCardHorizontal.spec.ts +36 -0
- package/app/components/ui/ProductCardHorizontal/types.ts +8 -0
- package/app/components/ui/PurchaseCard/PurchaseCard.vue +109 -0
- package/app/components/ui/PurchaseCard/__tests__/PurchaseCard.spec.ts +15 -0
- package/app/components/ui/PurchaseCard/types.ts +5 -0
- package/app/components/ui/QuantitySelector/QuantitySelector.vue +69 -0
- package/app/components/ui/QuantitySelector/__tests__/QuantitySelector.spec.ts +15 -0
- package/app/components/ui/QuantitySelector/types.ts +5 -0
- package/app/components/ui/RadialProgress.vue +44 -0
- package/app/components/ui/Review/Review.vue +57 -0
- package/app/components/ui/Review/__tests__/Review.spec.ts +15 -0
- package/app/components/ui/Review/types.ts +5 -0
- package/app/components/ui/ScrollTop.vue +82 -0
- package/app/components/ui/Search.vue +54 -0
- package/app/components/ui/Tag/Tag.vue +38 -0
- package/app/components/ui/Tag/__tests__/Tag.spec.ts +10 -0
- package/app/components/ui/Tag/types.ts +16 -0
- package/app/components/ui/VsfLogo.vue +7 -0
- package/app/components/ui/forms/BooleanInput.vue +34 -0
- package/app/components/ui/forms/DateTime.vue +44 -0
- package/app/components/ui/forms/DirectusFormElement.vue +60 -0
- package/app/components/ui/forms/DynamicTableElement.vue +57 -0
- package/app/components/ui/forms/FileInput.vue +85 -0
- package/app/components/ui/forms/FormField.vue +34 -0
- package/app/components/ui/forms/RelationSelect.vue +63 -0
- package/app/components/ui/forms/RepeaterInput.vue +121 -0
- package/app/components/ui/forms/SelectInput.vue +65 -0
- package/app/components/ui/forms/TextArea.vue +59 -0
- package/app/components/ui/forms/TextInput.vue +42 -0
- package/app/components/ui/forms/TiptapEditor.vue +136 -0
- package/app/components/ui/forms/[collection].vue +22 -0
- package/app/components/ui/studio/builder.vue +57 -0
- package/app/components/ui/studio/document.vue +69 -0
- package/app/components/ui/studio/email.vue +57 -0
- package/app/composables/globals/uploadFiles.js +41 -0
- package/app/composables/globals/useAdminTable.ts +12 -0
- package/app/composables/globals/useCustomFetch.ts +13 -0
- package/app/composables/globals/useDirectusField.ts +144 -0
- package/app/composables/globals/useDirectusForm.ts +70 -0
- package/app/composables/globals/useDirectusSchema.js +9 -0
- package/app/composables/globals/useFileManager.ts +76 -0
- package/app/composables/globals/useLivePreview.ts +17 -0
- package/app/composables/globals/useLoading.ts +23 -0
- package/app/composables/globals/useNavigation.js +19 -0
- package/app/composables/globals/useNotifications.ts +153 -0
- package/app/composables/globals/usePages.js +36 -0
- package/app/composables/globals/useRichText.ts +33 -0
- package/app/composables/globals/useServerRootMixin.ts +19 -0
- package/app/composables/globals/useVisualEditing.ts +38 -0
- package/app/composables/media/useFile.ts +6 -0
- package/app/composables/media/useMediaCenter.ts +353 -0
- package/app/composables/media/useVideojs.ts +45 -0
- package/app/composables/registry.ts +13 -0
- package/app/composables/types.ts +12 -0
- package/app/composables/useContent.ts +13 -0
- package/app/composables/useDirectusRequest.ts +32 -0
- package/app/stores/index.ts +0 -0
- package/app/types/api/global-search.ts +8 -0
- package/app/types/blocks/block-button-group.ts +7 -0
- package/app/types/blocks/block-button.ts +14 -0
- package/app/types/blocks/block-column.ts +20 -0
- package/app/types/blocks/block-cta.ts +10 -0
- package/app/types/blocks/block-divider.ts +4 -0
- package/app/types/blocks/block-faq.ts +12 -0
- package/app/types/blocks/block-form.ts +8 -0
- package/app/types/blocks/block-gallery.ts +14 -0
- package/app/types/blocks/block-hero.ts +12 -0
- package/app/types/blocks/block-html.ts +4 -0
- package/app/types/blocks/block-logocloud.ts +14 -0
- package/app/types/blocks/block-quote.ts +11 -0
- package/app/types/blocks/block-richtext.ts +7 -0
- package/app/types/blocks/block-steps.ts +22 -0
- package/app/types/blocks/block-team.ts +6 -0
- package/app/types/blocks/block-testimonial.ts +14 -0
- package/app/types/blocks/block-video.ts +10 -0
- package/app/types/blocks/block.ts +49 -0
- package/app/types/blocks/index.ts +18 -0
- package/app/types/componentMap.ts +15 -0
- package/app/types/content/category.ts +11 -0
- package/app/types/content/form.ts +20 -0
- package/app/types/content/index.ts +6 -0
- package/app/types/content/page.ts +76 -0
- package/app/types/content/post.ts +39 -0
- package/app/types/content/team.ts +16 -0
- package/app/types/content/testimonial.ts +19 -0
- package/app/types/directus.d.ts +47 -0
- package/app/types/env.d.ts +8 -0
- package/app/types/help/index.ts +53 -0
- package/app/types/index.d.ts +9 -0
- package/app/types/index.ts +7 -0
- package/app/types/meta/analytics.ts +18 -0
- package/app/types/meta/config.ts +21 -0
- package/app/types/meta/globals.ts +30 -0
- package/app/types/meta/index.ts +6 -0
- package/app/types/meta/navigation.ts +32 -0
- package/app/types/meta/redirect.ts +13 -0
- package/app/types/meta/seo.ts +19 -0
- package/app/types/os/contact.ts +23 -0
- package/app/types/os/conversation.ts +25 -0
- package/app/types/os/index.ts +16 -0
- package/app/types/os/organization.ts +54 -0
- package/app/types/os/os-activity.ts +28 -0
- package/app/types/os/os-deal.ts +45 -0
- package/app/types/os/os-expense.ts +22 -0
- package/app/types/os/os-invoice.ts +48 -0
- package/app/types/os/os-item.ts +18 -0
- package/app/types/os/os-payment.ts +29 -0
- package/app/types/os/os-project.ts +47 -0
- package/app/types/os/os-proposal.ts +84 -0
- package/app/types/os/os-settings.ts +19 -0
- package/app/types/os/os-subscription.ts +12 -0
- package/app/types/os/os-task.ts +34 -0
- package/app/types/os/os-tax-rate.ts +13 -0
- package/app/types/pageComponentMap.ts +8 -0
- package/app/types/schema.d.ts +39 -0
- package/app/types/schema.ts +151 -0
- package/app/types/system/file.ts +46 -0
- package/app/types/system/folder.ts +8 -0
- package/app/types/system/index.ts +4 -0
- package/app/types/system/role.ts +21 -0
- package/app/types/system/user.ts +56 -0
- package/app/utils/Timer.js +44 -0
- package/app/utils/billing-address.ts +24 -0
- package/app/utils/color.ts +14 -0
- package/app/utils/currency.ts +29 -0
- package/app/utils/embed.ts +57 -0
- package/app/utils/errors.ts +9 -0
- package/app/utils/fieldRegistry.js +89 -0
- package/app/utils/fonts.ts +24 -0
- package/app/utils/formkit.ts +75 -0
- package/app/utils/icons.ts +62 -0
- package/app/utils/index.js +0 -0
- package/app/utils/links.ts +28 -0
- package/app/utils/lodash.ts +33 -0
- package/app/utils/markdown.ts +9 -0
- package/app/utils/math.ts +25 -0
- package/app/utils/navigation.ts +11 -0
- package/app/utils/objects.ts +11 -0
- package/app/utils/paths.ts +21 -0
- package/app/utils/relations.ts +33 -0
- package/app/utils/strings.ts +113 -0
- package/app/utils/time.ts +148 -0
- package/app/utils/url.ts +22 -0
- package/app/utils/user-name.ts +21 -0
- package/app/utils/video/README.md +51 -0
- package/app/utils/video/playlist.js +0 -0
- package/dist/api/global-search.d.ts +8 -0
- package/dist/api/global-search.js +1 -0
- package/dist/blocks/block-button-group.d.ts +6 -0
- package/dist/blocks/block-button-group.js +1 -0
- package/dist/blocks/block-button.d.ts +13 -0
- package/dist/blocks/block-button.js +1 -0
- package/dist/blocks/block-column.d.ts +18 -0
- package/dist/blocks/block-column.js +1 -0
- package/dist/blocks/block-cta.d.ts +11 -0
- package/dist/blocks/block-cta.js +1 -0
- package/dist/blocks/block-divider.d.ts +4 -0
- package/dist/blocks/block-divider.js +1 -0
- package/dist/blocks/block-faq.d.ts +11 -0
- package/dist/blocks/block-faq.js +1 -0
- package/dist/blocks/block-form.d.ts +7 -0
- package/dist/blocks/block-form.js +1 -0
- package/dist/blocks/block-gallery.d.ts +13 -0
- package/dist/blocks/block-gallery.js +1 -0
- package/dist/blocks/block-hero.d.ts +11 -0
- package/dist/blocks/block-hero.js +1 -0
- package/dist/blocks/block-html.d.ts +4 -0
- package/dist/blocks/block-html.js +1 -0
- package/dist/blocks/block-logocloud.d.ts +13 -0
- package/dist/blocks/block-logocloud.js +1 -0
- package/dist/blocks/block-quote.d.ts +10 -0
- package/dist/blocks/block-quote.js +1 -0
- package/dist/blocks/block-richtext.d.ts +7 -0
- package/dist/blocks/block-richtext.js +1 -0
- package/dist/blocks/block-steps.d.ts +21 -0
- package/dist/blocks/block-steps.js +1 -0
- package/dist/blocks/block-team.d.ts +6 -0
- package/dist/blocks/block-team.js +1 -0
- package/dist/blocks/block-testimonial.d.ts +13 -0
- package/dist/blocks/block-testimonial.js +1 -0
- package/dist/blocks/block-video.d.ts +9 -0
- package/dist/blocks/block-video.js +1 -0
- package/dist/blocks/block.d.ts +17 -0
- package/dist/blocks/block.js +1 -0
- package/dist/blocks/index.d.ts +18 -0
- package/dist/blocks/index.js +1 -0
- package/dist/componentMap.d.ts +6 -0
- package/dist/componentMap.js +8 -0
- package/dist/content/category.d.ts +10 -0
- package/dist/content/category.js +1 -0
- package/dist/content/form.d.ts +21 -0
- package/dist/content/form.js +1 -0
- package/dist/content/index.d.ts +6 -0
- package/dist/content/index.js +1 -0
- package/dist/content/page.d.ts +38 -0
- package/dist/content/page.js +1 -0
- package/dist/content/post.d.ts +38 -0
- package/dist/content/post.js +1 -0
- package/dist/content/team.d.ts +17 -0
- package/dist/content/team.js +1 -0
- package/dist/content/testimonial.d.ts +18 -0
- package/dist/content/testimonial.js +1 -0
- package/dist/help/index.d.ts +51 -0
- package/dist/help/index.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -0
- package/dist/meta/analytics.d.ts +21 -0
- package/dist/meta/analytics.js +1 -0
- package/dist/meta/config.d.ts +22 -0
- package/dist/meta/config.js +1 -0
- package/dist/meta/globals.d.ts +33 -0
- package/dist/meta/globals.js +1 -0
- package/dist/meta/index.d.ts +6 -0
- package/dist/meta/index.js +1 -0
- package/dist/meta/navigation.d.ts +31 -0
- package/dist/meta/navigation.js +1 -0
- package/dist/meta/redirect.d.ts +12 -0
- package/dist/meta/redirect.js +1 -0
- package/dist/meta/seo.d.ts +19 -0
- package/dist/meta/seo.js +1 -0
- package/dist/os/contact.d.ts +22 -0
- package/dist/os/contact.js +1 -0
- package/dist/os/conversation.d.ts +23 -0
- package/dist/os/conversation.js +1 -0
- package/dist/os/index.d.ts +16 -0
- package/dist/os/index.js +1 -0
- package/dist/os/organization.d.ts +51 -0
- package/dist/os/organization.js +1 -0
- package/dist/os/os-activity.d.ts +26 -0
- package/dist/os/os-activity.js +1 -0
- package/dist/os/os-deal.d.ts +42 -0
- package/dist/os/os-deal.js +1 -0
- package/dist/os/os-expense.d.ts +21 -0
- package/dist/os/os-expense.js +1 -0
- package/dist/os/os-invoice.d.ts +46 -0
- package/dist/os/os-invoice.js +1 -0
- package/dist/os/os-item.d.ts +17 -0
- package/dist/os/os-item.js +1 -0
- package/dist/os/os-payment.d.ts +27 -0
- package/dist/os/os-payment.js +1 -0
- package/dist/os/os-project.d.ts +45 -0
- package/dist/os/os-project.js +1 -0
- package/dist/os/os-proposal.d.ts +61 -0
- package/dist/os/os-proposal.js +1 -0
- package/dist/os/os-settings.d.ts +17 -0
- package/dist/os/os-settings.js +1 -0
- package/dist/os/os-subscription.d.ts +12 -0
- package/dist/os/os-subscription.js +1 -0
- package/dist/os/os-task.d.ts +32 -0
- package/dist/os/os-task.js +1 -0
- package/dist/os/os-tax-rate.d.ts +12 -0
- package/dist/os/os-tax-rate.js +1 -0
- package/dist/pageComponentMap.d.ts +2 -0
- package/dist/pageComponentMap.js +7 -0
- package/dist/schema.d.ts +78 -0
- package/dist/schema.js +1 -0
- package/dist/system/file.d.ts +47 -0
- package/dist/system/file.js +1 -0
- package/dist/system/folder.d.ts +8 -0
- package/dist/system/folder.js +1 -0
- package/dist/system/index.d.ts +4 -0
- package/dist/system/index.js +1 -0
- package/dist/system/role.d.ts +20 -0
- package/dist/system/role.js +1 -0
- package/dist/system/user.d.ts +57 -0
- package/dist/system/user.js +1 -0
- package/nuxt.config.ts +5 -0
- package/package.json +42 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex-col md:flex-row h-full flex relative scroll-smooth md:gap-4" data-testid="gallery">
|
|
3
|
+
<div
|
|
4
|
+
class="after:block after:pt-[100%] flex-1 relative overflow-hidden w-full max-h-[600px]"
|
|
5
|
+
data-testid="gallery-images"
|
|
6
|
+
>
|
|
7
|
+
<SfScrollable
|
|
8
|
+
class="items-center flex snap-x snap-mandatory [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none'] w-full h-full"
|
|
9
|
+
wrapper-class="!absolute top-0 left-0 w-full h-full"
|
|
10
|
+
buttons-placement="none"
|
|
11
|
+
:active-index="activeIndex"
|
|
12
|
+
is-active-index-centered
|
|
13
|
+
:drag="{ containerWidth: true }"
|
|
14
|
+
@on-scroll="onScroll"
|
|
15
|
+
>
|
|
16
|
+
<div
|
|
17
|
+
v-for="({ url, alt }, index) in images"
|
|
18
|
+
:key="`${alt}-${index}-thumbnail`"
|
|
19
|
+
class="w-full h-full relative snap-center snap-always basis-full shrink-0 grow"
|
|
20
|
+
>
|
|
21
|
+
<NuxtImg
|
|
22
|
+
:alt="alt ?? ''"
|
|
23
|
+
:aria-hidden="activeIndex !== index"
|
|
24
|
+
fit="fill"
|
|
25
|
+
class="object-contain h-full w-full"
|
|
26
|
+
:quality="80"
|
|
27
|
+
:src="url"
|
|
28
|
+
sizes="2xs:100vw, md:700px"
|
|
29
|
+
draggable="false"
|
|
30
|
+
:loading="index !== 0 ? 'lazy' : undefined"
|
|
31
|
+
:fetchpriority="index === 0 ? 'high' : undefined"
|
|
32
|
+
:preload="index === 0"
|
|
33
|
+
format="webp"
|
|
34
|
+
width="600"
|
|
35
|
+
height="600"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
</SfScrollable>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="md:-order-1 overflow-hidden flex-shrink-0 basis-auto">
|
|
42
|
+
<SfScrollable
|
|
43
|
+
ref="thumbsReference"
|
|
44
|
+
wrapper-class="hidden md:inline-flex"
|
|
45
|
+
direction="vertical"
|
|
46
|
+
class="flex-row w-full items-center md:flex-col md:h-full md:px-0 md:scroll-pl-4 snap-y snap-mandatory flex gap-0.5 md:gap-2 overflow-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']"
|
|
47
|
+
:active-index="activeIndex"
|
|
48
|
+
:prev-disabled="activeIndex === 0"
|
|
49
|
+
:next-disabled="activeIndex === images.length - 1"
|
|
50
|
+
>
|
|
51
|
+
<template #previousButton>
|
|
52
|
+
<SfButton
|
|
53
|
+
variant="secondary"
|
|
54
|
+
size="sm"
|
|
55
|
+
square
|
|
56
|
+
class="absolute !rounded-full bg-white z-10 top-4 rotate-90 disabled:!hidden !ring-neutral-500 !text-neutral-500"
|
|
57
|
+
:class="{ hidden: firstVisibleThumbnailIntersected }"
|
|
58
|
+
:aria-label="$t('gallery.prev')"
|
|
59
|
+
>
|
|
60
|
+
<template #prefix>
|
|
61
|
+
<SfIconChevronLeft />
|
|
62
|
+
</template>
|
|
63
|
+
</SfButton>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<button
|
|
67
|
+
v-for="({ url, alt }, index) in images"
|
|
68
|
+
:key="`${alt}-${index}-thumbnail`"
|
|
69
|
+
:ref="(el) => assignReference(el, index)"
|
|
70
|
+
type="button"
|
|
71
|
+
:aria-current="activeIndex === index"
|
|
72
|
+
:aria-label="$t('gallery.thumb', index)"
|
|
73
|
+
class="w-20 h-[88px] relative shrink-0 pb-1 border-b-4 snap-start cursor-pointer transition-colors flex-grow-0"
|
|
74
|
+
:class="[activeIndex === index ? 'border-primary-700' : 'border-transparent']"
|
|
75
|
+
@mouseover="onChangeIndex(index)"
|
|
76
|
+
@focus="onChangeIndex(index)"
|
|
77
|
+
>
|
|
78
|
+
<NuxtImg
|
|
79
|
+
alt=""
|
|
80
|
+
class="object-contain"
|
|
81
|
+
width="80"
|
|
82
|
+
height="80"
|
|
83
|
+
:src="url"
|
|
84
|
+
:quality="80"
|
|
85
|
+
loading="lazy"
|
|
86
|
+
format="webp"
|
|
87
|
+
/>
|
|
88
|
+
</button>
|
|
89
|
+
|
|
90
|
+
<template #nextButton>
|
|
91
|
+
<SfButton
|
|
92
|
+
variant="secondary"
|
|
93
|
+
size="sm"
|
|
94
|
+
square
|
|
95
|
+
class="absolute !rounded-full bg-white z-10 bottom-4 rotate-90 disabled:!hidden !ring-neutral-500 !text-neutral-500"
|
|
96
|
+
:class="{ hidden: lastVisibleThumbnailIntersected }"
|
|
97
|
+
:aria-label="$t('gallery.next')"
|
|
98
|
+
>
|
|
99
|
+
<template #prefix>
|
|
100
|
+
<SfIconChevronRight />
|
|
101
|
+
</template>
|
|
102
|
+
</SfButton>
|
|
103
|
+
</template>
|
|
104
|
+
</SfScrollable>
|
|
105
|
+
<div class="flex md:hidden gap-0.5" role="group">
|
|
106
|
+
<button
|
|
107
|
+
v-for="({ url }, index) in images"
|
|
108
|
+
:key="url"
|
|
109
|
+
type="button"
|
|
110
|
+
:aria-current="activeIndex === index"
|
|
111
|
+
:aria-label="$t('gallery.thumb', index + 1)"
|
|
112
|
+
class="relative shrink-0 pb-1 border-b-4 cursor-pointer transition-colors flex-grow"
|
|
113
|
+
:class="[activeIndex === index ? 'border-primary-700' : 'border-neutral-200']"
|
|
114
|
+
@click="onChangeIndex(index)"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<script setup lang="ts">
|
|
122
|
+
import type { ComponentPublicInstance } from 'vue';
|
|
123
|
+
import { clamp, type SfScrollableOnScrollData } from '@storefront-ui/shared';
|
|
124
|
+
import { SfScrollable, SfButton, SfIconChevronLeft, SfIconChevronRight } from '@storefront-ui/vue';
|
|
125
|
+
import type { SfImage } from '@vue-storefront/unified-data-model';
|
|
126
|
+
import { unrefElement, useIntersectionObserver, useTimeoutFn } from '@vueuse/core';
|
|
127
|
+
|
|
128
|
+
const props = defineProps<{
|
|
129
|
+
images: SfImage[];
|
|
130
|
+
}>();
|
|
131
|
+
|
|
132
|
+
const { isPending, start, stop } = useTimeoutFn(() => {}, 50);
|
|
133
|
+
|
|
134
|
+
const thumbsReference = ref<HTMLElement>();
|
|
135
|
+
const firstThumbReference = ref<HTMLButtonElement>();
|
|
136
|
+
const lastThumbReference = ref<HTMLButtonElement>();
|
|
137
|
+
const firstVisibleThumbnailIntersected = ref(true);
|
|
138
|
+
const lastVisibleThumbnailIntersected = ref(true);
|
|
139
|
+
const activeIndex = ref(0);
|
|
140
|
+
|
|
141
|
+
const registerThumbsWatch = (
|
|
142
|
+
singleThumbReference: Ref<HTMLButtonElement | undefined>,
|
|
143
|
+
thumbnailIntersected: Ref<boolean>,
|
|
144
|
+
) => {
|
|
145
|
+
watch(
|
|
146
|
+
thumbsReference,
|
|
147
|
+
(reference) => {
|
|
148
|
+
if (reference) {
|
|
149
|
+
useIntersectionObserver(
|
|
150
|
+
singleThumbReference,
|
|
151
|
+
([{ isIntersecting }]) => {
|
|
152
|
+
thumbnailIntersected.value = isIntersecting;
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
root: unrefElement(reference),
|
|
156
|
+
rootMargin: '0px',
|
|
157
|
+
threshold: 1,
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{ immediate: true },
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
registerThumbsWatch(firstThumbReference, firstVisibleThumbnailIntersected);
|
|
167
|
+
registerThumbsWatch(lastThumbReference, lastVisibleThumbnailIntersected);
|
|
168
|
+
|
|
169
|
+
const onChangeIndex = (index: number) => {
|
|
170
|
+
stop();
|
|
171
|
+
activeIndex.value = clamp(index, 0, props.images.length - 1);
|
|
172
|
+
start();
|
|
173
|
+
};
|
|
174
|
+
const onScroll = ({ left, scrollWidth }: SfScrollableOnScrollData) => {
|
|
175
|
+
if (!isPending.value) {
|
|
176
|
+
onChangeIndex(Math.round(left / (scrollWidth / props.images.length)));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const assignReference = (element: Element | ComponentPublicInstance | null, index: number) => {
|
|
180
|
+
if (!element) return;
|
|
181
|
+
if (index === props.images.length - 1) {
|
|
182
|
+
lastThumbReference.value = element as HTMLButtonElement;
|
|
183
|
+
} else if (index === 0) {
|
|
184
|
+
firstThumbReference.value = element as HTMLButtonElement;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import Gallery from '~/components/Gallery/Gallery.vue';
|
|
3
|
+
|
|
4
|
+
describe('<Gallery />', () => {
|
|
5
|
+
it('should render component', () => {
|
|
6
|
+
const { getByTestId } = mount(Gallery, {
|
|
7
|
+
props: {
|
|
8
|
+
images: [{ alt: 'Test', url: '/images/test.webp' }],
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(getByTestId('gallery'));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="tag" :class="className" data-testid="heading">
|
|
3
|
+
{{ title }}
|
|
4
|
+
</component>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import type { HeadingProps } from '~/components/Heading/types';
|
|
9
|
+
|
|
10
|
+
withDefaults(defineProps<HeadingProps>(), {
|
|
11
|
+
tag: 'h1',
|
|
12
|
+
className: 'text-center mb-6 font-bold typography-headline-3 md:typography-headline-2',
|
|
13
|
+
});
|
|
14
|
+
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import Heading from '~/components/Heading/Heading.vue';
|
|
3
|
+
|
|
4
|
+
describe('<Heading />', () => {
|
|
5
|
+
it('should render component', () => {
|
|
6
|
+
const { getByTestId } = mount(Heading, {
|
|
7
|
+
props: {
|
|
8
|
+
title: 'test',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(getByTestId('heading'));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<client-only>
|
|
3
|
+
<div ref="galleryRef" class="audio-gallery">
|
|
4
|
+
<NuxtLink v-for="item in items" :key="item.id" class="gallery-item" :data-sub-html="audioHtml(item)"
|
|
5
|
+
:href="placeholder">
|
|
6
|
+
<v-card class="audio-card">
|
|
7
|
+
<v-card-title>
|
|
8
|
+
{{ item.directus_files_id?.title || 'Audio File' }}
|
|
9
|
+
</v-card-title>
|
|
10
|
+
</v-card>
|
|
11
|
+
</NuxtLink>
|
|
12
|
+
</div>
|
|
13
|
+
</client-only>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
import {
|
|
18
|
+
ref,
|
|
19
|
+
onMounted
|
|
20
|
+
} from 'vue'
|
|
21
|
+
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
items: {
|
|
24
|
+
type: Array,
|
|
25
|
+
default: () => []
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const galleryRef = ref(null)
|
|
30
|
+
|
|
31
|
+
const runtime = useRuntimeConfig()
|
|
32
|
+
const base = runtime.public.directus.url
|
|
33
|
+
|
|
34
|
+
const fileUrl = (item) => `${base}assets/${item.directus_files_id.id}`
|
|
35
|
+
|
|
36
|
+
// Transparent pixel placeholder (required by LightGallery)
|
|
37
|
+
const placeholder =
|
|
38
|
+
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
|
|
39
|
+
|
|
40
|
+
const audioHtml = (item) => `
|
|
41
|
+
<div style="padding:20px;text-align:center;">
|
|
42
|
+
<audio controls style="width:100%;">
|
|
43
|
+
<source src="${fileUrl(item)}" type="audio/mpeg">
|
|
44
|
+
</audio>
|
|
45
|
+
</div>
|
|
46
|
+
`
|
|
47
|
+
|
|
48
|
+
onMounted(async () => {
|
|
49
|
+
const lg = await import('lightgallery')
|
|
50
|
+
const lightGallery = lg.default
|
|
51
|
+
|
|
52
|
+
lightGallery(galleryRef.value, {
|
|
53
|
+
speed: 400,
|
|
54
|
+
download: false,
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<style scoped>
|
|
60
|
+
.audio-gallery {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 10px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.audio-card {
|
|
67
|
+
padding: 10px;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog max-width="500">
|
|
3
|
+
<template v-slot:activator="{ props: activatorProps }">
|
|
4
|
+
<v-btn color="primary" v-bind="activatorProps" icon="fas fa-gear" size="medium"
|
|
5
|
+
title="Upload Media"></v-btn>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<template v-slot:default="{ isActive }">
|
|
9
|
+
<v-card title="Dialog">
|
|
10
|
+
<v-card-text>
|
|
11
|
+
<div v-if="formError" class="error">{{ formError }}</div>
|
|
12
|
+
<div v-else-if="formSuccess" class="success">{{ formSuccess }}</div>
|
|
13
|
+
<form @submit.prevent="submitForm">
|
|
14
|
+
<DirectusFormElement v-for="field in mediaFields" :key="field.field" :field="field"
|
|
15
|
+
v-model="form[field.field]" />
|
|
16
|
+
<v-btn type="submit">Post</v-btn>
|
|
17
|
+
</form>
|
|
18
|
+
</v-card-text>
|
|
19
|
+
</v-card>
|
|
20
|
+
</template>
|
|
21
|
+
</v-dialog>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup>
|
|
25
|
+
import {
|
|
26
|
+
ref
|
|
27
|
+
} from 'vue'
|
|
28
|
+
import DirectusFormElement from '~/components/ui/forms/DirectusFormElement.vue'
|
|
29
|
+
import {
|
|
30
|
+
useDirectusForm
|
|
31
|
+
} from '~/composables/globals/useDirectusForm'
|
|
32
|
+
|
|
33
|
+
const dialog = ref(false)
|
|
34
|
+
const {
|
|
35
|
+
$directus,
|
|
36
|
+
$readFieldsByCollection
|
|
37
|
+
} = useNuxtApp()
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
data,
|
|
41
|
+
error
|
|
42
|
+
} = await useAsyncData('media', async () => {
|
|
43
|
+
return $directus.request($readFieldsByCollection('media'))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// guard against undefined/null data.value and empty arrays
|
|
47
|
+
if (error.value || data.value == null || (data.value?.length ?? 0) === 0) {
|
|
48
|
+
console.error(error)
|
|
49
|
+
throw createError({
|
|
50
|
+
statusCode: 404,
|
|
51
|
+
statusMessage: 'Note not found'
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mediaFields = data
|
|
56
|
+
|
|
57
|
+
// use composable for form handling (validation, submit, provide context)
|
|
58
|
+
const {
|
|
59
|
+
form,
|
|
60
|
+
formError,
|
|
61
|
+
formSuccess,
|
|
62
|
+
submitForm
|
|
63
|
+
} = useDirectusForm('media', mediaFields, {
|
|
64
|
+
clearOnSuccess: true,
|
|
65
|
+
closeDialogRef: dialog
|
|
66
|
+
})
|
|
67
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog :model-value="modelValue" max-width="960" @update:model-value="$emit('update:model-value', $event)">
|
|
3
|
+
<v-card v-if="item">
|
|
4
|
+
<v-toolbar density="comfortable">
|
|
5
|
+
<v-toolbar-title>
|
|
6
|
+
{{ file?.title || file?.filename_download || 'Media' }}
|
|
7
|
+
</v-toolbar-title>
|
|
8
|
+
<v-spacer />
|
|
9
|
+
<v-btn icon="mdi-close" @click="$emit('update:model-value', false)" />
|
|
10
|
+
</v-toolbar>
|
|
11
|
+
|
|
12
|
+
<v-card-text>
|
|
13
|
+
<div v-if="mime.startsWith('image')" class="viewer">
|
|
14
|
+
<img :src="fileUrl" :alt="file?.title || 'Image'" />
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div v-else-if="mime.startsWith('video')" class="viewer">
|
|
18
|
+
<video :src="fileUrl" controls style="max-width: 100%;" />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div v-else-if="mime.startsWith('audio')" class="viewer">
|
|
22
|
+
<audio :src="fileUrl" controls style="width: 100%;" />
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div v-else class="viewer">
|
|
26
|
+
<p>Preview not available for this file type.</p>
|
|
27
|
+
<v-btn color="primary" :href="fileUrl" target="_blank">
|
|
28
|
+
Open file
|
|
29
|
+
</v-btn>
|
|
30
|
+
</div>
|
|
31
|
+
</v-card-text>
|
|
32
|
+
</v-card>
|
|
33
|
+
</v-dialog>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup>
|
|
37
|
+
import {
|
|
38
|
+
computed
|
|
39
|
+
} from 'vue'
|
|
40
|
+
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
modelValue: Boolean,
|
|
43
|
+
item: Object,
|
|
44
|
+
})
|
|
45
|
+
defineEmits(['update:model-value'])
|
|
46
|
+
|
|
47
|
+
const config = useRuntimeConfig()
|
|
48
|
+
const file = computed(() => props.item?.directus_files_id || null)
|
|
49
|
+
const mime = computed(() => file.value?.type || '')
|
|
50
|
+
const fileUrl = computed(() =>
|
|
51
|
+
file.value?.id ? `${config.public.directus.url}/assets/${file.value.id}` : ''
|
|
52
|
+
)
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.viewer {
|
|
57
|
+
display: flex;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
align-items: center;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.viewer img {
|
|
63
|
+
max-width: 100%;
|
|
64
|
+
max-height: 75vh;
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card class="image-card" elevation="2" rounded="lg">
|
|
3
|
+
<!-- IMAGE PREVIEW -->
|
|
4
|
+
<v-img :src="fileUrl" :alt="media?.title || media?.filename_download || 'Image'" height="180" cover
|
|
5
|
+
class="image-preview" @click="openFull" />
|
|
6
|
+
|
|
7
|
+
<!-- TITLE -->
|
|
8
|
+
<v-card-text class="py-2 text-truncate">
|
|
9
|
+
{{ media?.title || media?.filename_download || 'Untitled image' }}
|
|
10
|
+
</v-card-text>
|
|
11
|
+
|
|
12
|
+
<!-- ACTIONS SLOT -->
|
|
13
|
+
<v-card-actions>
|
|
14
|
+
<slot name="actions" />
|
|
15
|
+
</v-card-actions>
|
|
16
|
+
</v-card>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup>
|
|
20
|
+
import {
|
|
21
|
+
computed
|
|
22
|
+
} from 'vue'
|
|
23
|
+
|
|
24
|
+
const props = defineProps({
|
|
25
|
+
media: {
|
|
26
|
+
type: Object,
|
|
27
|
+
required: true,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const config = useRuntimeConfig()
|
|
32
|
+
|
|
33
|
+
// Build Directus file URL
|
|
34
|
+
const fileUrl = computed(() => {
|
|
35
|
+
if (!props.media?.id) return ''
|
|
36
|
+
return `${config.public.directus.url}/assets/${props.media.id}`
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Open full image in new tab
|
|
40
|
+
function openFull() {
|
|
41
|
+
if (fileUrl.value) window.open(fileUrl.value, '_blank')
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style scoped>
|
|
46
|
+
.image-card {
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
transition: box-shadow 0.2s ease;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.image-card:hover {
|
|
52
|
+
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.15);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.image-preview {
|
|
56
|
+
border-top-left-radius: inherit;
|
|
57
|
+
border-top-right-radius: inherit;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.text-truncate {
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
text-overflow: ellipsis;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<client-only>
|
|
3
|
+
<div ref="galleryRef" class="image-gallery">
|
|
4
|
+
<NuxtLink v-for="item in items" :key="item.id" :href="fileUrl(item)" class="gallery-item">
|
|
5
|
+
<img :src="fileUrl(item)" :alt="item.directus_files_id?.title || 'Image'" />
|
|
6
|
+
</NuxtLink>
|
|
7
|
+
</div>
|
|
8
|
+
</client-only>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
import {
|
|
13
|
+
onMounted,
|
|
14
|
+
ref
|
|
15
|
+
} from 'vue'
|
|
16
|
+
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
items: Array,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const galleryRef = ref(null)
|
|
22
|
+
|
|
23
|
+
const fileUrl = (item) =>
|
|
24
|
+
`${useRuntimeConfig().public.directus.url}assets/${item.directus_files_id.id}`
|
|
25
|
+
|
|
26
|
+
onMounted(async () => {
|
|
27
|
+
const lg = await import('lightgallery')
|
|
28
|
+
const lightGallery = lg.default
|
|
29
|
+
|
|
30
|
+
const lgThumbnail = (await import('lg-thumbnail')).default
|
|
31
|
+
const lgZoom = (await import('lg-zoom')).default
|
|
32
|
+
|
|
33
|
+
lightGallery(galleryRef.value, {
|
|
34
|
+
plugins: [lgThumbnail, lgZoom],
|
|
35
|
+
speed: 400,
|
|
36
|
+
thumbnail: true,
|
|
37
|
+
zoom: true,
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
</script>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card class="media-card" elevation="2" rounded="lg" @click="emit('open', media)">
|
|
3
|
+
<!-- THUMBNAIL -->
|
|
4
|
+
<div class="thumb-wrapper">
|
|
5
|
+
<component :is="thumbnailComponent" :media="media" class="thumb" />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- TITLE -->
|
|
9
|
+
<v-card-text class="py-2 text-truncate">
|
|
10
|
+
{{ media?.title || media?.filename_download || 'Untitled file' }}
|
|
11
|
+
</v-card-text>
|
|
12
|
+
|
|
13
|
+
<!-- OPTIONAL ACTIONS -->
|
|
14
|
+
<v-card-actions v-if="$slots.actions">
|
|
15
|
+
<slot name="actions" />
|
|
16
|
+
</v-card-actions>
|
|
17
|
+
</v-card>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
import {
|
|
22
|
+
computed
|
|
23
|
+
} from 'vue'
|
|
24
|
+
import imageCard from '#shared/app/components/media/imageCard.vue'
|
|
25
|
+
import mediaPlayer from '#shared/app/components/media/mediaPlayer.vue'
|
|
26
|
+
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
media: {
|
|
29
|
+
type: Object,
|
|
30
|
+
required: true,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits(['open'])
|
|
35
|
+
|
|
36
|
+
const mime = computed(() => props.media?.type || '')
|
|
37
|
+
|
|
38
|
+
const thumbnailComponent = computed(() => {
|
|
39
|
+
if (mime.value.startsWith('image')) return imageCard
|
|
40
|
+
if (mime.value.startsWith('audio') || mime.value.startsWith('video')) return mediaPlayer
|
|
41
|
+
return {
|
|
42
|
+
template: `
|
|
43
|
+
<div class="generic-thumb">
|
|
44
|
+
<v-icon size="48">mdi-file</v-icon>
|
|
45
|
+
</div>
|
|
46
|
+
`,
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.media-card {
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
transition: box-shadow 0.2s ease;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.media-card:hover {
|
|
58
|
+
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.15);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.thumb-wrapper {
|
|
62
|
+
width: 100%;
|
|
63
|
+
height: 160px;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
border-top-left-radius: inherit;
|
|
66
|
+
border-top-right-radius: inherit;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.thumb {
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: 100%;
|
|
72
|
+
object-fit: cover;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.generic-thumb {
|
|
76
|
+
width: 100%;
|
|
77
|
+
height: 100%;
|
|
78
|
+
background: #f3f3f3;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.text-truncate {
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
text-overflow: ellipsis;
|
|
88
|
+
}
|
|
89
|
+
</style>
|