@datagouv/components-next 0.0.1

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 (119) hide show
  1. package/README.md +150 -0
  2. package/assets/main.css +136 -0
  3. package/assets/placeholders/author.png +0 -0
  4. package/assets/placeholders/dataset.png +0 -0
  5. package/assets/placeholders/news.png +0 -0
  6. package/assets/placeholders/organization.png +0 -0
  7. package/assets/placeholders/reuse.png +0 -0
  8. package/assets/tailwind.config.js +24 -0
  9. package/dist/components.css +2 -0
  10. package/dist/locales/de.js +155 -0
  11. package/dist/locales/en.js +155 -0
  12. package/dist/locales/es.js +155 -0
  13. package/dist/locales/fr.js +155 -0
  14. package/dist/locales/it.js +155 -0
  15. package/dist/locales/pt.js +155 -0
  16. package/dist/locales/sr.js +155 -0
  17. package/package.json +72 -0
  18. package/src/components/AppLink.vue +51 -0
  19. package/src/components/Avatar.vue +27 -0
  20. package/src/components/AvatarWithName.vue +26 -0
  21. package/src/components/BannerAction.vue +39 -0
  22. package/src/components/BrandedButton.vue +170 -0
  23. package/src/components/CopyButton.vue +84 -0
  24. package/src/components/DataserviceCard.vue +184 -0
  25. package/src/components/DatasetCard.vue +198 -0
  26. package/src/components/DatasetInformationPanel.vue +210 -0
  27. package/src/components/DatasetQuality.vue +68 -0
  28. package/src/components/DatasetQualityInline.vue +32 -0
  29. package/src/components/DatasetQualityItem.vue +32 -0
  30. package/src/components/DatasetQualityItemWarning.vue +21 -0
  31. package/src/components/DatasetQualityScore.vue +35 -0
  32. package/src/components/DatasetQualityTooltipContent.vue +79 -0
  33. package/src/components/DescriptionDetails.vue +23 -0
  34. package/src/components/DescriptionList/DescriptionDetails.stories.ts +43 -0
  35. package/src/components/DescriptionList/DescriptionList.stories.ts +47 -0
  36. package/src/components/DescriptionList/DescriptionTerm.stories.ts +28 -0
  37. package/src/components/DescriptionList.vue +8 -0
  38. package/src/components/DescriptionTerm.vue +8 -0
  39. package/src/components/ExtraAccordion.vue +78 -0
  40. package/src/components/Icons/Archive.vue +21 -0
  41. package/src/components/Icons/Code.vue +21 -0
  42. package/src/components/Icons/Documentation.vue +21 -0
  43. package/src/components/Icons/File.vue +21 -0
  44. package/src/components/Icons/Image.vue +7 -0
  45. package/src/components/Icons/Link.vue +21 -0
  46. package/src/components/Icons/Table.vue +21 -0
  47. package/src/components/OrganizationCard.vue +68 -0
  48. package/src/components/OrganizationNameWithCertificate.vue +45 -0
  49. package/src/components/OwnerType.vue +43 -0
  50. package/src/components/OwnerTypeIcon.vue +18 -0
  51. package/src/components/Pagination.vue +205 -0
  52. package/src/components/Placeholder.vue +29 -0
  53. package/src/components/ReadMore.vue +107 -0
  54. package/src/components/ResourceAccordion/DataStructure.vue +87 -0
  55. package/src/components/ResourceAccordion/EditButton.vue +34 -0
  56. package/src/components/ResourceAccordion/Metadata.vue +171 -0
  57. package/src/components/ResourceAccordion/Preview.vue +229 -0
  58. package/src/components/ResourceAccordion/PreviewLoader.vue +148 -0
  59. package/src/components/ResourceAccordion/ResourceAccordion.vue +484 -0
  60. package/src/components/ResourceAccordion/ResourceIcon.vue +16 -0
  61. package/src/components/ResourceAccordion/SchemaBadge.vue +148 -0
  62. package/src/components/ResourceAccordion/SchemaLoader.vue +30 -0
  63. package/src/components/ResourceAccordion/Swagger.vue +46 -0
  64. package/src/components/ResourceAccordion/france.svg +1 -0
  65. package/src/components/ReuseCard.vue +106 -0
  66. package/src/components/ReuseDetails.vue +45 -0
  67. package/src/components/SimpleBanner.vue +24 -0
  68. package/src/components/SmallChart.vue +149 -0
  69. package/src/components/StatBox.vue +100 -0
  70. package/src/components/Tabs/Tab.vue +62 -0
  71. package/src/components/Tabs/TabGroup.vue +20 -0
  72. package/src/components/Tabs/TabList.vue +15 -0
  73. package/src/components/Tabs/TabPanel.vue +7 -0
  74. package/src/components/Tabs/TabPanels.vue +7 -0
  75. package/src/components/Toggletip.vue +62 -0
  76. package/src/components/ToggletipButton.vue +14 -0
  77. package/src/composables/useActiveDescendant.ts +103 -0
  78. package/src/composables/useReuseType.ts +14 -0
  79. package/src/config.ts +33 -0
  80. package/src/functions/api.ts +96 -0
  81. package/src/functions/api.types.ts +41 -0
  82. package/src/functions/config.ts +12 -0
  83. package/src/functions/datasets.ts +24 -0
  84. package/src/functions/dates.ts +85 -0
  85. package/src/functions/helpers.ts +38 -0
  86. package/src/functions/markdown.ts +47 -0
  87. package/src/functions/matomo.ts +3 -0
  88. package/src/functions/organizations.ts +85 -0
  89. package/src/functions/owned.ts +11 -0
  90. package/src/functions/resources.ts +99 -0
  91. package/src/functions/reuses.ts +28 -0
  92. package/src/functions/schemas.ts +96 -0
  93. package/src/functions/tabularApi.ts +27 -0
  94. package/src/functions/users.ts +7 -0
  95. package/src/locales/de.json +154 -0
  96. package/src/locales/en.json +154 -0
  97. package/src/locales/es.json +154 -0
  98. package/src/locales/fr.json +154 -0
  99. package/src/locales/it.json +154 -0
  100. package/src/locales/pt.json +154 -0
  101. package/src/locales/sr.json +154 -0
  102. package/src/main.ts +147 -0
  103. package/src/types/badges.ts +5 -0
  104. package/src/types/contact_point.ts +7 -0
  105. package/src/types/dataservices.ts +68 -0
  106. package/src/types/datasets.ts +80 -0
  107. package/src/types/frequency.ts +6 -0
  108. package/src/types/granularity.ts +6 -0
  109. package/src/types/harvest.ts +3 -0
  110. package/src/types/keyboard.ts +1 -0
  111. package/src/types/licenses.ts +9 -0
  112. package/src/types/organizations.ts +41 -0
  113. package/src/types/owned.ts +9 -0
  114. package/src/types/resources.ts +37 -0
  115. package/src/types/reuses.ts +49 -0
  116. package/src/types/site.ts +23 -0
  117. package/src/types/topics.ts +20 -0
  118. package/src/types/ui.ts +3 -0
  119. package/src/types/users.ts +10 -0
@@ -0,0 +1,205 @@
1
+ <template>
2
+ <nav
3
+ v-if="totalResults > pageSize"
4
+ role="navigation"
5
+ class="fr-pagination fr-pagination--centered"
6
+ :aria-label="t('Pagination')"
7
+ >
8
+ <ul class="fr-pagination__list">
9
+ <li>
10
+ <a
11
+ :href="getHref(1)"
12
+ class="fr-pagination__link fr-pagination__link--first"
13
+ data-testid="first-page"
14
+ @click.prevent="onClick(1)"
15
+ >
16
+ {{ t('First page') }}
17
+ </a>
18
+ </li>
19
+ <li>
20
+ <a
21
+ :href="getHref(page - 1)"
22
+ class="fr-pagination__link fr-pagination__link--prev fr-pagination__link--lg-label"
23
+ data-testid="previous-page"
24
+ @click.prevent="previousPage"
25
+ >
26
+ {{ t('Previous page') }}
27
+ </a>
28
+ </li>
29
+ <li>
30
+ <a
31
+ :aria-current="page === 1 ? 'page' : undefined"
32
+ :href="getHref(1)"
33
+ class="fr-pagination__link"
34
+ :class="{ 'fr-hidden fr-unhidden-sm': page > 1 }"
35
+ :title="t('Page {nb}', { nb: 1 })"
36
+ :data-testid="1"
37
+ @click.prevent="onClick(1)"
38
+ >
39
+ 1
40
+ </a>
41
+ </li>
42
+ <li
43
+ v-for="(index, arrayIndex) in visiblePages"
44
+ :key="arrayIndex"
45
+ >
46
+ <a
47
+ v-if="index"
48
+ class="fr-pagination__link"
49
+ :class="{ 'fr-hidden fr-unhidden-lg': index < page - 1 || index > page + 1 }"
50
+ :aria-current="page === index ? 'page' : undefined"
51
+ :href="getHref(index)"
52
+ :title="t('Page {nb}', { nb: index })"
53
+ :data-testid="index"
54
+ @click.prevent="onClick(index)"
55
+ >
56
+ {{ index }}
57
+ </a>
58
+ <a
59
+ v-else
60
+ class="fr-pagination__link fr-hidden fr-unhidden-lg"
61
+ >
62
+
63
+ </a>
64
+ </li>
65
+ <li>
66
+ <a
67
+ class="fr-pagination__link"
68
+ :aria-current="page === pageCount ? 'page' : undefined"
69
+ :href="getHref(pageCount)"
70
+ :title="t('Page {nb}', { nb: pageCount })"
71
+ :data-testid="pageCount"
72
+ @click.prevent="onClick(pageCount)"
73
+ >
74
+ {{ pageCount }}
75
+ </a>
76
+ </li>
77
+ <li>
78
+ <a
79
+ class="fr-pagination__link fr-pagination__link--next fr-pagination__link--lg-label"
80
+ :href="getHref(page + 1)"
81
+ data-testid="next-page"
82
+ @click.prevent="nextPage"
83
+ >
84
+ {{ t('Next page') }}
85
+ </a>
86
+ </li>
87
+ <li>
88
+ <a
89
+ class="fr-pagination__link fr-pagination__link--last"
90
+ :href="getHref(pageCount)"
91
+ data-testid="last-page"
92
+ @click.prevent="onClick(pageCount)"
93
+ >
94
+ {{ t('Last page') }}
95
+ </a>
96
+ </li>
97
+ </ul>
98
+ </nav>
99
+ </template>
100
+
101
+ <script setup lang="ts">
102
+ import { computed } from 'vue'
103
+ import { useI18n } from 'vue-i18n'
104
+
105
+ type Props = {
106
+ /**
107
+ * The current page.
108
+ */
109
+ page?: number
110
+ /**
111
+ * The page size.
112
+ */
113
+ pageSize?: number
114
+ /**
115
+ * Customize the links used
116
+ */
117
+ link?: (page: number) => string
118
+ /**
119
+ * The number of items in the collection. It's used to calculated the number of pages.
120
+ */
121
+ totalResults: number
122
+ }
123
+
124
+ const emit = defineEmits<{
125
+ (event: 'change', page: number): void
126
+ }>()
127
+
128
+ const props = withDefaults(defineProps<Props>(), {
129
+ page: 1,
130
+ pageSize: 20,
131
+ })
132
+
133
+ const PAGES_AROUND = 3
134
+
135
+ function range(size: number, startAt = 1) {
136
+ return [...Array(size).keys()].map(i => i + startAt)
137
+ }
138
+
139
+ function getPages(pageCount: number) {
140
+ return range(pageCount)
141
+ }
142
+
143
+ function getPagesShown(pages: { length: number }, currentPage: number) {
144
+ return Math.min(
145
+ PAGES_AROUND * 2 + 1,
146
+ pages.length - 2,
147
+ PAGES_AROUND + currentPage - 1,
148
+ PAGES_AROUND + pages.length - currentPage,
149
+ )
150
+ }
151
+
152
+ function getStartPage(currentPage: number) {
153
+ return Math.max(
154
+ currentPage - PAGES_AROUND, // we want to start 3 pages before the current one
155
+ 2, // we don't want to start below page 2
156
+ )
157
+ }
158
+
159
+ function getVisiblePages(currentPage: number, pageCount: number) {
160
+ const pages = getPages(pageCount)
161
+ const start = getStartPage(currentPage)
162
+ if (pageCount <= 2) {
163
+ return []
164
+ }
165
+ const pagination: Array<number | null> = range(getPagesShown(pages, currentPage), start)
166
+ if (!pagination.includes(2)) {
167
+ pagination.unshift(null)
168
+ }
169
+ if (!pagination.includes(pageCount - 1)) {
170
+ pagination.push(null)
171
+ }
172
+ return pagination
173
+ }
174
+
175
+ const { t } = useI18n()
176
+ const pageCount = computed(() => Math.ceil(props.totalResults / props.pageSize))
177
+ const visiblePages = computed(() => getVisiblePages(props.page, pageCount.value))
178
+
179
+ function onClick(index: number) {
180
+ if (index !== props.page) {
181
+ return emit('change', index)
182
+ }
183
+ }
184
+
185
+ function nextPage() {
186
+ const index = props.page + 1
187
+ if (index <= pageCount.value) {
188
+ return emit('change', index)
189
+ }
190
+ }
191
+
192
+ function previousPage() {
193
+ const index = props.page - 1
194
+ if (index > 0) {
195
+ return emit('change', index)
196
+ }
197
+ }
198
+
199
+ function getHref(forPage: number) {
200
+ if (forPage < 1 || forPage > pageCount.value) {
201
+ return undefined
202
+ }
203
+ return props.page === forPage ? undefined : (props.link ? props.link(forPage) : '#')
204
+ }
205
+ </script>
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <img
3
+ loading="lazy"
4
+ :src="path"
5
+ :alt="alternativeTextForDefinedImageOnly"
6
+ :width="size"
7
+ :height="size"
8
+ v-bind="$attrs"
9
+ >
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed } from 'vue'
14
+ import { computedAsync } from '@vueuse/core'
15
+
16
+ const props = defineProps<{
17
+ type: 'author' | 'dataset' | 'news' | 'organization' | 'reuse'
18
+ src?: string | null
19
+ alt?: string
20
+ size: number
21
+ }>()
22
+ const placeholderUrl = async () => {
23
+ const module = await import(`../../assets/placeholders/${props.type}.png`)
24
+ return props.src ? props.src : module.default
25
+ }
26
+
27
+ const alternativeTextForDefinedImageOnly = computed(() => props.src ? props.alt : '')
28
+ const path = computedAsync(() => placeholderUrl())
29
+ </script>
@@ -0,0 +1,107 @@
1
+ <template>
2
+ <div class="relative">
3
+ <div
4
+ ref="readMoreRef"
5
+ class="overflow-hidden"
6
+ :style="{ height: containerHeight + 'px' }"
7
+ >
8
+ <div ref="containerRef">
9
+ <slot />
10
+ </div>
11
+ </div>
12
+ <div
13
+ v-if="readMoreRequired"
14
+ class=" bottom-0 w-full cursor-pointer text-center"
15
+ :class="{
16
+ 'relative pt-2.5': expanded,
17
+ 'absolute pt-20 bg-linear-to-b from-white/0 via-70% via-white/100 to-white/100': !expanded,
18
+ }"
19
+ @click.stop="toggle"
20
+ >
21
+ <BrandedButton
22
+ color="primary-softer"
23
+ @click.stop="toggle"
24
+ >
25
+ <template v-if="expanded">
26
+ {{ $t("Read less") }}
27
+ </template>
28
+ <template v-else>
29
+ {{ $t("Read more") }}
30
+ </template>
31
+ </BrandedButton>
32
+ </div>
33
+ </div>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import { templateRef, useElementSize } from '@vueuse/core'
38
+ import { ref, watch } from 'vue'
39
+ import { easing, tween, styler } from 'popmotion'
40
+ import BrandedButton from './BrandedButton.vue'
41
+
42
+ const props = withDefaults(defineProps<{
43
+ maxHeight?: string
44
+ }>(), {
45
+ maxHeight: '',
46
+ })
47
+
48
+ const DEFAULT_HEIGHT = 284
49
+ const expanded = ref(false)
50
+ const readMoreRequired = ref(false)
51
+ const containerRef = templateRef<HTMLElement | null>('containerRef')
52
+ const readMoreRef = templateRef<HTMLElement | null>('readMoreRef')
53
+ const { height } = useElementSize(containerRef)
54
+ const containerHeight = ref(DEFAULT_HEIGHT)
55
+ const getHeight = (elt: Element) => {
56
+ const style = getComputedStyle(elt)
57
+ return parseFloat(style.getPropertyValue('height'))
58
+ + parseFloat(style.getPropertyValue('margin-top'))
59
+ + parseFloat(style.getPropertyValue('margin-bottom'))
60
+ }
61
+ const getDefaultHeight = () => {
62
+ const elementMaxHeight = document.querySelector(`[data-read-more-max-height="${props.maxHeight}"]`)
63
+ if (!elementMaxHeight) {
64
+ return DEFAULT_HEIGHT
65
+ }
66
+ return Array.from(elementMaxHeight.children)
67
+ .map(getHeight)
68
+ .reduce((total, height) => total + height, 0)
69
+ }
70
+ const updateReadMoreHeight = (height: number) => {
71
+ containerHeight.value = getDefaultHeight()
72
+ readMoreRequired.value = height > containerHeight.value
73
+ if (!readMoreRequired.value) {
74
+ containerHeight.value = height
75
+ }
76
+ }
77
+ const toggle = () => {
78
+ if (!readMoreRef.value) {
79
+ return
80
+ }
81
+ const divStyler = styler(readMoreRef.value)
82
+ if (expanded.value) {
83
+ tween({
84
+ from: { height: readMoreRef.value.scrollHeight },
85
+ to: { height: getDefaultHeight() },
86
+ duration: 300,
87
+ ease: easing.anticipate,
88
+ }).start({
89
+ update: divStyler.set,
90
+ complete: () => readMoreRef.value?.scrollIntoView({ behavior: 'smooth' }),
91
+ })
92
+ }
93
+ else {
94
+ tween({
95
+ from: { height: getDefaultHeight() },
96
+ to: { height: readMoreRef.value.scrollHeight },
97
+ duration: 300,
98
+ ease: easing.anticipate,
99
+ }).start({
100
+ update: divStyler.set,
101
+ complete: () => divStyler.set({ height: 'auto' }),
102
+ })
103
+ }
104
+ expanded.value = !expanded.value
105
+ }
106
+ watch(height, updateReadMoreHeight)
107
+ </script>
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <div
3
+ v-if="hasError"
4
+ class="bg-warning-lightest text-warning-dark p-3 mb-4"
5
+ >
6
+ <p class="fr-grid-row fr-m-0">
7
+ <span
8
+ class="fr-icon-warning-line"
9
+ aria-hidden="true"
10
+ />
11
+ {{ $t("The data structure of this file failed to load.") }}
12
+ </p>
13
+ </div>
14
+ <PreviewLoader v-else-if="loading" />
15
+ <div
16
+ v-else
17
+ class="fr-grid-row fr-grid-row--gutters"
18
+ >
19
+ <div
20
+ v-if="!hasColumnInfo"
21
+ class="bg-warning-lightest text-warning-dark p-3 mb-4"
22
+ >
23
+ <p class="fr-grid-row fr-m-0">
24
+ <span
25
+ class="fr-icon-warning-line"
26
+ aria-hidden="true"
27
+ />
28
+ {{ $t("No data structure found for this file.") }}
29
+ </p>
30
+ </div>
31
+ <template v-if="hasColumnInfo">
32
+ <div
33
+ v-for="(column, index) in columns"
34
+ :key="index"
35
+ class="fr-col-12 fr-col-sm-6 fr-col-md-4 fr-col-lg-3"
36
+ >
37
+ <h5 class="fr-text--sm fr-text--bold fr-mt-0 fr-mb-1v">
38
+ {{ column }}
39
+ </h5>
40
+ <code class="code">
41
+ {{ columnsInfo[column].format }}
42
+ </code>
43
+ </div>
44
+ </template>
45
+ </div>
46
+ </template>
47
+
48
+ <script setup lang="ts">
49
+ import { onMounted, ref } from 'vue'
50
+ import type { Resource } from '../../types/resources'
51
+ import { getProfile } from '../../functions/tabularApi'
52
+ import PreviewLoader from './PreviewLoader.vue'
53
+
54
+ const props = defineProps<{ resource: Resource }>()
55
+
56
+ type ColumnInfo = {
57
+ score: number
58
+ format: string
59
+ python_type: string
60
+ }
61
+
62
+ const columns = ref<Array<string>>([])
63
+ const columnsInfo = ref<Record<string, ColumnInfo>>({})
64
+ const loading = ref(true)
65
+ const hasError = ref(false)
66
+ const hasColumnInfo = ref(false)
67
+
68
+ onMounted(async () => {
69
+ try {
70
+ const { data } = await getProfile(props.resource.id) // Assurez-vous que cette fonction retourne bien les données attendues
71
+ if ('profile' in data && data.profile) {
72
+ columns.value = Object.keys(data.profile.columns)
73
+ columnsInfo.value = data.profile.columns
74
+ hasColumnInfo.value = true
75
+ loading.value = false
76
+ }
77
+ else {
78
+ hasError.value = true
79
+ loading.value = false
80
+ }
81
+ }
82
+ catch {
83
+ hasError.value = true
84
+ loading.value = false
85
+ }
86
+ })
87
+ </script>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <BrandedButton
3
+ :href="adminUrl"
4
+ icon-only
5
+ :icon="RiPencilLine"
6
+ color="warning"
7
+ data-testid="edit-button"
8
+ >
9
+ {{ t("Edit file") }}
10
+ </BrandedButton>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { useI18n } from 'vue-i18n'
15
+ import { RiPencilLine } from '@remixicon/vue'
16
+ import { useComponentsConfig } from '../../config'
17
+ import BrandedButton from '../BrandedButton.vue'
18
+
19
+ type Props = {
20
+ datasetId: string
21
+ isCommunityResource?: boolean
22
+ resourceId: string
23
+ }
24
+
25
+ const props = withDefaults(defineProps<Props>(), {
26
+ isCommunityResource: false,
27
+ })
28
+
29
+ const config = useComponentsConfig()
30
+
31
+ const { t } = useI18n()
32
+ const resourceType = props.isCommunityResource ? 'community-resource' : 'resource'
33
+ const adminUrl = `${config.baseUrl}/dataset/${props.datasetId}/${resourceType}/${props.resourceId}`
34
+ </script>
@@ -0,0 +1,171 @@
1
+ <script setup lang="ts">
2
+ import { useI18n } from 'vue-i18n'
3
+ import { computed } from 'vue'
4
+ import type { Resource } from '../../types/resources'
5
+ import CopyButton from '../CopyButton.vue'
6
+ import DescriptionDetails from '../DescriptionDetails.vue'
7
+ import DescriptionList from '../DescriptionList.vue'
8
+ import DescriptionTerm from '../DescriptionTerm.vue'
9
+ import { formatDate } from '../../functions/dates'
10
+ import { filesize } from '../../functions/helpers'
11
+ import ExtraAccordion from '../ExtraAccordion.vue'
12
+ import { getResourceTitleId, getResourceLabel } from '../../functions/resources'
13
+ import { useComponentsConfig } from '../../config'
14
+
15
+ const props = defineProps<{
16
+ resource: Resource
17
+ }>()
18
+
19
+ const hasExtras = computed(() => Object.keys(props.resource.extras).length)
20
+ const resourceTitleId = computed(() => getResourceTitleId(props.resource))
21
+
22
+ const { t } = useI18n()
23
+ const config = useComponentsConfig()
24
+ </script>
25
+
26
+ <template>
27
+ <div>
28
+ <div class="flex gap-3rem flex-col-on-small">
29
+ <DescriptionList class="flex-1">
30
+ <DescriptionTerm>
31
+ {{ t('URL') }}
32
+ <CopyButton
33
+ :label="$t('Copy URL')"
34
+ :copied-label="$t('URL copied!')"
35
+ :text="resource.url"
36
+ :aria-describedby="resourceTitleId"
37
+ />
38
+ </DescriptionTerm>
39
+ <DescriptionDetails :with-ellipsis="false">
40
+ <code class="code">
41
+ <a :href="resource.url"><component
42
+ :is="config.textClamp"
43
+ v-if="config && config.textClamp"
44
+ :max-lines="1"
45
+ :autoresize="true"
46
+ :text="resource.url"
47
+ /></a>
48
+ </code>
49
+ </DescriptionDetails>
50
+ <DescriptionTerm>
51
+ {{ t('Stable URL') }}
52
+ <CopyButton
53
+ :label="$t('Copy stable URL')"
54
+ :copied-label="$t('Stable URL copied!')"
55
+ :text="resource.latest"
56
+ :aria-describedby="resourceTitleId"
57
+ />
58
+ </DescriptionTerm>
59
+ <DescriptionDetails :with-ellipsis="false">
60
+ <code class="code">
61
+ <a :href="resource.latest"><component
62
+ :is="config.textClamp"
63
+ v-if="config && config.textClamp"
64
+ :max-lines="1"
65
+ :autoresize="true"
66
+ :text="resource.latest"
67
+ /></a>
68
+ </code>
69
+ </DescriptionDetails>
70
+ <DescriptionTerm>
71
+ {{ t('Identifier') }}
72
+ <CopyButton
73
+ :label="$t('Copy ID')"
74
+ :copied-label="$t('ID copied!')"
75
+ :text="resource.id"
76
+ :aria-describedby="resourceTitleId"
77
+ />
78
+ </DescriptionTerm>
79
+ <DescriptionDetails :with-ellipsis="false">
80
+ <code class="code">
81
+ <component
82
+ :is="config.textClamp"
83
+ v-if="config && config.textClamp"
84
+ :max-lines="1"
85
+ :autoresize="true"
86
+ :text="resource.id"
87
+ />
88
+ </code>
89
+ </DescriptionDetails>
90
+ <template v-if="resource.checksum">
91
+ <DescriptionTerm>
92
+ {{ resource.checksum.type }}
93
+ <CopyButton
94
+ :label="$t('Copy checksum')"
95
+ :copied-label="$t('Checksum copied!')"
96
+ :text="resource.checksum.value"
97
+ :aria-describedby="resourceTitleId"
98
+ />
99
+ </DescriptionTerm>
100
+ <DescriptionDetails :with-ellipsis="false">
101
+ <code class="code">
102
+ <component
103
+ :is="config.textClamp"
104
+ v-if="config && config.textClamp"
105
+ :max-lines="1"
106
+ :autoresize="true"
107
+ :text="resource.checksum.value"
108
+ />
109
+ </code>
110
+ </DescriptionDetails>
111
+ </template>
112
+ </DescriptionList>
113
+ <DescriptionList style="flex-shrink: 0;">
114
+ <DescriptionTerm>{{ t('Created on') }}</DescriptionTerm>
115
+ <DescriptionDetails>
116
+ {{ formatDate(resource.created_at) }}
117
+ </DescriptionDetails>
118
+ <DescriptionTerm>{{ t('Modified on') }}</DescriptionTerm>
119
+ <DescriptionDetails>
120
+ {{ formatDate(resource.last_modified) }}
121
+ </DescriptionDetails>
122
+ </DescriptionList>
123
+ <DescriptionList style="flex-shrink: 0;">
124
+ <template v-if="resource.filesize">
125
+ <DescriptionTerm>{{ t('Size') }}</DescriptionTerm>
126
+ <DescriptionDetails>
127
+ {{ filesize(resource.filesize) }}
128
+ </DescriptionDetails>
129
+ </template>
130
+ <template v-if="resource.mime">
131
+ <DescriptionTerm>{{ t('Type') }}</DescriptionTerm>
132
+ <DescriptionDetails>
133
+ {{ getResourceLabel(resource.type) }}
134
+ </DescriptionDetails>
135
+ </template>
136
+ <template v-if="resource.mime">
137
+ <DescriptionTerm>{{ t('MIME Type') }}</DescriptionTerm>
138
+ <DescriptionDetails>
139
+ <code class="code text-overflow-ellipsis">{{ resource.mime }}</code>
140
+ </DescriptionDetails>
141
+ </template>
142
+ </DescriptionList>
143
+ </div>
144
+ <div>
145
+ <ExtraAccordion
146
+ v-if="hasExtras"
147
+ class="pt-6 mt-6 border-top border-gray-default"
148
+ :button-text="t('See extras')"
149
+ :title-text="t('Resource Extras')"
150
+ title-level="h5"
151
+ :extra="resource.extras"
152
+ />
153
+ </div>
154
+ </div>
155
+ </template>
156
+
157
+ <style scoped>
158
+ .gap-3rem {
159
+ gap: 3rem;
160
+ }
161
+
162
+ .gap-3rem dl {
163
+ padding-inline-start: 0;
164
+ }
165
+
166
+ @container (max-width: 600px) {
167
+ .flex-col-on-small {
168
+ flex-direction: column
169
+ }
170
+ }
171
+ </style>