@datagouv/components-next 1.0.2-dev.20 → 1.0.2-dev.200

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 (165) hide show
  1. package/README.md +7 -6
  2. package/assets/main.css +4 -0
  3. package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
  4. package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-CFTmBFoY.js} +9 -9
  5. package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
  6. package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
  7. package/dist/ImagePreview.client-BvDM3qjE.js +36 -0
  8. package/dist/JsonPreview.client-BP6gPRxf.js +40 -0
  9. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  10. package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D9N3664r.js} +41 -42
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-C6A6ZiKl.js} +835 -856
  13. package/dist/Pmtiles.client-B_sDZoqu.js +26333 -0
  14. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DGLXBys_.js +60 -0
  15. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  16. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  17. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  18. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  19. package/dist/XmlPreview.client-DFQ1T-LN.js +34 -0
  20. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  21. package/dist/components-next.css +1 -52
  22. package/dist/components-next.js +196 -150
  23. package/dist/components.css +2 -2
  24. package/dist/{index-BFQX7Ccz.js → index-B_tPYi5Z.js} +34094 -27864
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
  26. package/dist/{main-vqhkVhVS.js → main-CVOZp_Nx.js} +41939 -73132
  27. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  28. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  29. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  30. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  31. package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
  32. package/package.json +47 -38
  33. package/src/chart.ts +5 -0
  34. package/src/components/ActivityList/ActivityList.vue +24 -14
  35. package/src/components/AppLink.vue +3 -0
  36. package/src/components/BannerAction.vue +6 -2
  37. package/src/components/Chart/ChartViewer.vue +320 -0
  38. package/src/components/Chart/ChartViewerWrapper.vue +182 -0
  39. package/src/components/CopyButton.vue +6 -2
  40. package/src/components/DataserviceCard.vue +24 -5
  41. package/src/components/DataserviceQuality.vue +82 -0
  42. package/src/components/DataserviceQualityInline.vue +31 -0
  43. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  44. package/src/components/DatasetCard.vue +24 -9
  45. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  46. package/src/components/DatasetQuality.vue +11 -11
  47. package/src/components/DatasetQualityInline.vue +2 -2
  48. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  49. package/src/components/DiscussionMessageCard.vue +14 -15
  50. package/src/components/Form/Listbox.vue +101 -0
  51. package/src/components/Form/SchemaSelect.vue +2 -0
  52. package/src/components/FormattedDate.vue +81 -0
  53. package/src/components/InfiniteLoader.vue +53 -0
  54. package/src/components/LoadingBlock.vue +4 -1
  55. package/src/components/ObjectCardHeader.vue +11 -4
  56. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  57. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  58. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  59. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  60. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  61. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  62. package/src/components/OpenApiViewer/openapi.ts +150 -0
  63. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  64. package/src/components/Pagination.vue +25 -62
  65. package/src/components/PostCard.vue +13 -15
  66. package/src/components/RadioInput.vue +7 -2
  67. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  68. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  69. package/src/components/ResourceAccordion/Downloads.vue +172 -0
  70. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  71. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  72. package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
  73. package/src/components/ResourceAccordion/Metadata.vue +4 -6
  74. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
  75. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  76. package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
  77. package/src/components/ResourceAccordion/Preview.vue +19 -15
  78. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  79. package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
  80. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  81. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
  82. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  83. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  84. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
  85. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  86. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  87. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
  88. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  89. package/src/components/ResourceListItem.vue +168 -0
  90. package/src/components/ReuseCard.vue +28 -9
  91. package/src/components/ReuseDetails.vue +2 -2
  92. package/src/components/ReuseHorizontalCard.vue +2 -2
  93. package/src/components/SchemaCard.vue +10 -1
  94. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  95. package/src/components/Search/GlobalSearch.vue +229 -123
  96. package/src/components/Search/SearchInput.vue +5 -4
  97. package/src/components/Search/Sidemenu.vue +4 -3
  98. package/src/components/SmallChart.vue +11 -10
  99. package/src/components/StatBox.vue +13 -5
  100. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  101. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  102. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  103. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  104. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  105. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  106. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  107. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  108. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  109. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  110. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  111. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  112. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  113. package/src/components/TabularExplorer/types.ts +84 -0
  114. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  115. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  116. package/src/components/TopicCard.vue +13 -14
  117. package/src/composables/useActiveDescendant.ts +5 -0
  118. package/src/composables/useDatasetResources.ts +164 -0
  119. package/src/composables/useHasTabularData.ts +15 -1
  120. package/src/composables/useMetrics.ts +1 -1
  121. package/src/composables/useResizable.ts +46 -0
  122. package/src/composables/useResourceCapabilities.ts +48 -40
  123. package/src/composables/useSearchFilter.ts +120 -0
  124. package/src/composables/useStableQueryParams.ts +38 -6
  125. package/src/composables/useTabularProfile.ts +70 -0
  126. package/src/composables/useTranslation.ts +24 -9
  127. package/src/config.ts +21 -3
  128. package/src/functions/activities.ts +3 -3
  129. package/src/functions/api.ts +20 -42
  130. package/src/functions/api.types.ts +1 -0
  131. package/src/functions/charts.ts +99 -0
  132. package/src/functions/datasets.ts +0 -17
  133. package/src/functions/dates.ts +6 -7
  134. package/src/functions/helpers.ts +15 -0
  135. package/src/functions/metrics.ts +63 -60
  136. package/src/functions/paginate.ts +46 -0
  137. package/src/functions/resourceCapabilities.ts +34 -0
  138. package/src/functions/resources.ts +136 -1
  139. package/src/functions/schemas.ts +27 -0
  140. package/src/functions/scroll.ts +20 -0
  141. package/src/functions/tabular.ts +214 -0
  142. package/src/functions/tabularApi.ts +96 -11
  143. package/src/main.ts +119 -14
  144. package/src/types/dataservices.ts +17 -0
  145. package/src/types/organizations.ts +10 -0
  146. package/src/types/pages.ts +0 -5
  147. package/src/types/posts.ts +2 -2
  148. package/src/types/reports.ts +5 -1
  149. package/src/types/resources.ts +0 -1
  150. package/src/types/reuses.ts +12 -1
  151. package/src/types/search.ts +63 -1
  152. package/src/types/site.ts +5 -3
  153. package/src/types/ui.ts +2 -0
  154. package/src/types/users.ts +2 -1
  155. package/src/types/visualizations.ts +93 -0
  156. package/assets/swagger-themes/newspaper.css +0 -1670
  157. package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
  158. package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
  159. package/dist/Swagger.client-B4y0hXME.js +0 -4
  160. package/dist/XmlPreview.client-LETEfNBH.js +0 -64
  161. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  162. package/src/functions/pagination.ts +0 -9
  163. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  164. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  165. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -11,10 +11,16 @@
11
11
  class="flex flex-wrap justify-between"
12
12
  >
13
13
  <p class="text-[0.625rem] m-0 text-gray-medium">
14
- {{ startDate }}
14
+ <FormattedDate
15
+ :date="startDate"
16
+ :options="{ dateStyle: undefined, year: '2-digit', month: '2-digit' }"
17
+ />
15
18
  </p>
16
19
  <p class="text-[0.625rem] m-0 text-gray-medium">
17
- {{ endDate }}
20
+ <FormattedDate
21
+ :date="endDate"
22
+ :options="{ dateStyle: undefined, year: '2-digit', month: '2-digit' }"
23
+ />
18
24
  </p>
19
25
  </div>
20
26
  </div>
@@ -24,6 +30,7 @@
24
30
  import type { ChartOptions, ScriptableLineSegmentContext } from 'chart.js'
25
31
  import Chart from 'chart.js/auto'
26
32
  import { computed, onMounted, ref, useTemplateRef, watchEffect } from 'vue'
33
+ import FormattedDate from './FormattedDate.vue'
27
34
 
28
35
  const LIGHT_COLOR = '#B6CFFB'
29
36
  const COLOR = '#3558A2'
@@ -107,14 +114,8 @@ const additionalDatasetConfig = computed<{
107
114
  return {}
108
115
  })
109
116
 
110
- const getMonthYear = (dateAsString: string): string => {
111
- const date = new Date(dateAsString)
112
-
113
- return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getFullYear().toString().slice(-2)}`
114
- }
115
-
116
- const startDate = computed(() => months.value.length ? getMonthYear(months.value[0]!) : null)
117
- const endDate = computed(() => months.value.length ? getMonthYear(months.value[months.value.length - 1]!) : null)
117
+ const startDate = computed(() => months.value[0] ?? null)
118
+ const endDate = computed(() => months.value[months.value.length - 1] ?? null)
118
119
 
119
120
  const OPTIONS = {
120
121
  // @ts-expect-error animation can be `true` but the typing is not expecting it
@@ -65,14 +65,20 @@
65
65
  class="mt-1 mb-0 text-xs"
66
66
  >
67
67
  {{ t("depuis ") }}
68
- {{ formatDate(since, { dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }) }}
68
+ <FormattedDate
69
+ :date="since"
70
+ :options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
71
+ />
69
72
  </p>
70
73
  <p class="mt-1 mb-0 text-xs text-success-darkest">
71
74
  <strong>
72
75
  + {{ summarize(lastValue, 2) }}
73
76
  </strong>
74
77
  {{ t(" en ") }}
75
- {{ formatDate(lastMonth, { dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }) }}
78
+ <FormattedDate
79
+ :date="lastMonth"
80
+ :options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
81
+ />
76
82
  </p>
77
83
  </template>
78
84
  </div>
@@ -146,7 +152,10 @@
146
152
  + {{ summarize(lastValue, 2) }}
147
153
  </strong>
148
154
  {{ t(" en ") }}
149
- {{ formatDate(lastMonth, { dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }) }}
155
+ <FormattedDate
156
+ :date="lastMonth"
157
+ :options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
158
+ />
150
159
  </p>
151
160
  </div>
152
161
  </template>
@@ -154,7 +163,7 @@
154
163
  <script setup lang="ts">
155
164
  import { computed } from 'vue'
156
165
  import { ContentLoader } from 'vue-content-loader'
157
- import { useFormatDate } from '../functions/dates'
166
+ import FormattedDate from './FormattedDate.vue'
158
167
  import { summarize } from '../functions/helpers'
159
168
  import { useTranslation } from '../composables/useTranslation'
160
169
  import SmallChart from './SmallChart.vue'
@@ -169,7 +178,6 @@ const props = defineProps<{
169
178
  }>()
170
179
 
171
180
  const { t } = useTranslation()
172
- const { formatDate } = useFormatDate()
173
181
 
174
182
  const months = computed(() => props.data ? Object.keys(props.data) : [])
175
183
  const values = computed(() => props.data ? Object.values(props.data) : [])
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <div
3
+ v-if="activeFilters.length > 0 || sort"
4
+ class="flex flex-wrap items-center gap-1.5"
5
+ >
6
+ <!-- Sort chip -->
7
+ <span
8
+ v-if="sort"
9
+ class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
10
+ >
11
+ <component
12
+ :is="sort.direction === 'asc' ? RiArrowUpLine : RiArrowDownLine"
13
+ class="size-3.5"
14
+ aria-hidden="true"
15
+ />
16
+ <span class="font-medium">{{ sort.column }}</span>
17
+ <span>{{ sort.direction === 'asc' ? t('croissant') : t('décroissant') }}</span>
18
+ <button
19
+ :aria-label="t('Supprimer le tri')"
20
+ class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
21
+ @click="sort = null"
22
+ >
23
+ <RiCloseLine
24
+ class="size-3.5"
25
+ aria-hidden="true"
26
+ />
27
+ </button>
28
+ </span>
29
+
30
+ <!-- Filter chips -->
31
+ <span
32
+ v-for="af in activeFilters"
33
+ :key="af.column"
34
+ :data-testid="`active-filter-${af.column}`"
35
+ class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
36
+ >
37
+ <component
38
+ :is="getColumnDisplay(af.column).icon"
39
+ class="size-3.5"
40
+ aria-hidden="true"
41
+ />
42
+ <span class="font-medium">{{ af.column }}</span>
43
+ <span>{{ af.label }}</span>
44
+ <button
45
+ :aria-label="t('Supprimer ce filtre')"
46
+ class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
47
+ @click="removeFilter(af.column)"
48
+ >
49
+ <RiCloseLine
50
+ class="size-3.5"
51
+ aria-hidden="true"
52
+ />
53
+ </button>
54
+ </span>
55
+
56
+ <!-- Clear all -->
57
+ <BrandedButton
58
+ v-if="withClear"
59
+ color="tertiary"
60
+ size="2xs"
61
+ :icon="RiCloseLine"
62
+ @click="clearAllFilters(); sort = null"
63
+ >
64
+ {{ t('Tout effacer') }}
65
+ </BrandedButton>
66
+ </div>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ import { RiArrowDownLine, RiArrowUpLine, RiCloseLine } from '@remixicon/vue'
71
+ import BrandedButton from '../BrandedButton.vue'
72
+ import { useTranslation } from '../../composables/useTranslation'
73
+ import { useTabularContext } from './useTabularContext'
74
+
75
+ defineProps<{ withClear?: boolean }>()
76
+
77
+ const { t } = useTranslation()
78
+ const { activeFilters, sort, removeFilter, clearAllFilters, getColumnDisplay } = useTabularContext()
79
+ </script>
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <span
3
+ v-if="value == null || value === ''"
4
+ class="font-[Inconsolata,monospace] text-gray-low italic"
5
+ :class="compact ? 'text-xs' : 'text-sm'"
6
+ >null</span>
7
+ <span
8
+ v-else-if="columnType === 'boolean'"
9
+ class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs"
10
+ :class="isTruthy(value) ? 'bg-new-success-light text-new-success' : 'bg-new-warning-light text-new-error'"
11
+ >
12
+ <span
13
+ class="size-2 rounded-full"
14
+ :class="isTruthy(value) ? 'bg-new-success' : 'bg-new-error'"
15
+ />
16
+ {{ isTruthy(value) ? t('Vrai') : t('Faux') }}
17
+ </span>
18
+ <span
19
+ v-else-if="columnType === 'categorical'"
20
+ class="inline-block max-w-full truncate rounded bg-gray-lower px-2 py-0.5 text-xs text-gray-plain"
21
+ >{{ value }}</span>
22
+ <span
23
+ v-else-if="columnType === 'number' || columnType === 'year'"
24
+ :class="compact ? 'font-mono tabular-nums text-xs' : ''"
25
+ >{{ columnType === 'year' || noNumberFormat ? value : formatNumber(value) }}</span>
26
+ <span
27
+ v-else-if="columnType === 'date'"
28
+ :class="compact ? 'font-mono tabular-nums text-xs' : ''"
29
+ >{{ formatCellDate(value) }}</span>
30
+ <span
31
+ v-else
32
+ class="text-gray-title truncate block text-xs"
33
+ >{{ typeof value === 'object' ? JSON.stringify(value) : value }}</span>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import { useTranslation } from '../../composables/useTranslation'
38
+ import { useFormatTabular, isTruthy } from '../../functions/tabular'
39
+ import type { ColumnType } from './types'
40
+
41
+ defineProps<{
42
+ value: unknown
43
+ columnType: ColumnType
44
+ compact?: boolean
45
+ noNumberFormat?: boolean
46
+ }>()
47
+
48
+ const { t } = useTranslation()
49
+ const { formatNumber, formatCellDate } = useFormatTabular()
50
+ </script>
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <ClientOnly>
3
+ <Teleport to="#tooltips">
4
+ <div
5
+ v-if="cell"
6
+ ref="panel"
7
+ class="bg-white border border-black/10 rounded-lg shadow-md w-80 absolute z-[800]"
8
+ :style="floatingStyles"
9
+ >
10
+ <!-- Value -->
11
+ <div class="px-3 pt-3 pb-2 border-b border-gray-default">
12
+ <p class="text-[10px] text-gray-plain mb-0">
13
+ {{ t('Valeur brute') }}
14
+ </p>
15
+ <p class="text-xs text-gray-title mb-0">
16
+ {{ displayValue }}
17
+ </p>
18
+ </div>
19
+
20
+ <!-- Type -->
21
+ <div class="flex items-center gap-2 px-3 py-2 border-b border-gray-default">
22
+ <span class="text-[10px] text-gray-plain">{{ t('Type') }}</span>
23
+ <span class="inline-flex items-center gap-1 bg-gray-some rounded px-1.5 py-0.5 text-xs text-gray-plain">
24
+ <component
25
+ :is="typeIcon"
26
+ class="size-3"
27
+ aria-hidden="true"
28
+ />
29
+ {{ typeLabel }}
30
+ </span>
31
+ <span class="text-[10px] text-gray-plain shrink-0">·</span>
32
+ <span class="text-[10px] text-gray-plain truncate min-w-0">{{ cell.column }}</span>
33
+ </div>
34
+
35
+ <!-- Actions -->
36
+ <div class="p-1">
37
+ <button
38
+ class="flex items-center gap-2.5 w-full px-3 py-2 rounded-md text-xs font-medium hover:bg-gray-50"
39
+ @click="filterByValue"
40
+ >
41
+ <RiFilterLine
42
+ class="size-4"
43
+ aria-hidden="true"
44
+ />
45
+ {{ t('Filtrer par cette valeur') }}
46
+ </button>
47
+ <button
48
+ class="flex items-center gap-2.5 w-full px-3 py-2 rounded-md text-xs font-medium hover:bg-gray-50"
49
+ @click="copyValue"
50
+ >
51
+ <RiCheckLine
52
+ v-if="copied"
53
+ class="size-4 text-green-500"
54
+ aria-hidden="true"
55
+ />
56
+ <RiFileCopyLine
57
+ v-else
58
+ class="size-4 text-gray-plain"
59
+ aria-hidden="true"
60
+ />
61
+ {{ copied ? t('Copié !') : t('Copier la valeur') }}
62
+ </button>
63
+ </div>
64
+ </div>
65
+ </Teleport>
66
+ </ClientOnly>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ import { computed, ref, useTemplateRef, watch } from 'vue'
71
+ import { flip, shift, autoUpdate, useFloating } from '@floating-ui/vue'
72
+ import { onClickOutside } from '@vueuse/core'
73
+ import {
74
+ RiFilterLine,
75
+ RiFileCopyLine,
76
+ RiCheckLine,
77
+ } from '@remixicon/vue'
78
+ import { toast } from 'vue-sonner'
79
+ import { useTranslation } from '../../composables/useTranslation'
80
+ import { buildTypeConfig } from '../../functions/tabular'
81
+ import ClientOnly from '../ClientOnly.vue'
82
+ import type { ColumnType, ColumnFilters } from './types'
83
+
84
+ export interface CellInfo {
85
+ column: string
86
+ columnType: ColumnType
87
+ value: unknown
88
+ element: HTMLElement
89
+ }
90
+
91
+ const cell = defineModel<CellInfo | null>('cell', { default: null })
92
+ const filters = defineModel<Record<string, ColumnFilters>>('filters', { default: () => ({}) })
93
+
94
+ const { t } = useTranslation()
95
+
96
+ const panelRef = useTemplateRef<HTMLElement>('panel')
97
+ const anchorRef = ref<HTMLElement | null>(null)
98
+
99
+ watch(cell, (c) => {
100
+ anchorRef.value = c?.element ?? null
101
+ })
102
+
103
+ const { floatingStyles } = useFloating(anchorRef, panelRef, {
104
+ placement: 'bottom-start',
105
+ middleware: [flip(), shift()],
106
+ whileElementsMounted: autoUpdate,
107
+ })
108
+
109
+ const displayValue = computed(() => {
110
+ if (!cell.value) return ''
111
+ const v = cell.value.value
112
+ if (v == null || v === '') return '–'
113
+ if (typeof v === 'object') return JSON.stringify(v)
114
+ return String(v)
115
+ })
116
+
117
+ const typeConfig = buildTypeConfig(t)
118
+
119
+ const typeIcon = computed(() => cell.value ? typeConfig[cell.value.columnType].icon : typeConfig.text.icon)
120
+ const typeLabel = computed(() => cell.value ? typeConfig[cell.value.columnType].label : '')
121
+
122
+ function close() {
123
+ cell.value = null
124
+ }
125
+
126
+ function filterByValue() {
127
+ if (!cell.value) return
128
+ const val = String(cell.value.value ?? '')
129
+ const col = cell.value.column
130
+ const existing = filters.value[col] ?? {}
131
+ if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'date' || cell.value.columnType === 'year') {
132
+ const current = existing.in ?? []
133
+ if (!current.includes(val)) {
134
+ filters.value = { ...filters.value, [col]: { ...existing, in: [...current, val] } }
135
+ }
136
+ }
137
+ else if (cell.value.columnType === 'number') {
138
+ const num = Number(cell.value.value)
139
+ if (Number.isFinite(num)) {
140
+ filters.value = { ...filters.value, [col]: { ...existing, min: num, max: num } }
141
+ }
142
+ }
143
+ else if (cell.value.columnType === 'boolean') {
144
+ filters.value = { ...filters.value, [col]: { ...existing, exact: val } }
145
+ }
146
+ close()
147
+ }
148
+
149
+ const copied = ref(false)
150
+
151
+ async function copyValue() {
152
+ try {
153
+ await navigator.clipboard.writeText(displayValue.value)
154
+ copied.value = true
155
+ setTimeout(() => {
156
+ copied.value = false
157
+ }, 1500)
158
+ }
159
+ catch {
160
+ toast.error(t('Impossible de copier dans le presse-papier'))
161
+ }
162
+ }
163
+
164
+ onClickOutside(panelRef, (e) => {
165
+ if (!cell.value) return
166
+ const clickedCell = (e.target as HTMLElement).closest('[data-cell]')
167
+ if (clickedCell && clickedCell === cell.value.element) return
168
+ close()
169
+ })
170
+ </script>
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <Popover
3
+ ref="columnAnchor"
4
+ v-slot="{ open }"
5
+ class="relative"
6
+ >
7
+ <PopoverButton class="flex items-center gap-1.5 text-xs text-gray-plain rounded px-2 py-1 hover:bg-gray-50 focus:outline-none">
8
+ <RiLayoutColumnLine
9
+ class="size-3"
10
+ aria-hidden="true"
11
+ />
12
+ <span class="font-bold hidden md:inline">{{ t('Colonnes') }}</span>
13
+ <span class="font-mono tabular-nums">{{ visibleColumns.size }}/{{ allColumns.length }}</span>
14
+ <RiArrowDownSLine
15
+ class="size-3 opacity-50"
16
+ aria-hidden="true"
17
+ />
18
+ </PopoverButton>
19
+
20
+ <ClientOnly>
21
+ <Teleport to="#tooltips">
22
+ <PopoverPanel
23
+ v-show="open"
24
+ ref="columnPanel"
25
+ static
26
+ class="bg-white border border-gray-default rounded shadow-lg w-72 absolute z-[800]"
27
+ :style="columnFloatingStyles"
28
+ >
29
+ <div class="px-3 py-2 border-b border-gray-default space-y-1.5">
30
+ <span class="block text-xs font-medium text-gray-title">
31
+ {{ visibleColumns.size }} {{ t('sur') }} {{ allColumns.length }} {{ t('colonnes visibles') }}
32
+ </span>
33
+ <div class="flex items-center gap-1">
34
+ <BrandedButton
35
+ v-if="hiddenCount"
36
+ color="tertiary"
37
+ size="2xs"
38
+ @click="showAllColumns"
39
+ >
40
+ {{ t('Tout afficher') }}
41
+ </BrandedButton>
42
+ <BrandedButton
43
+ v-if="visibleColumns.size > 0"
44
+ color="tertiary"
45
+ size="2xs"
46
+ @click="hideAllColumns"
47
+ >
48
+ {{ t('Tout masquer') }}
49
+ </BrandedButton>
50
+ </div>
51
+ </div>
52
+ <div class="max-h-64 overflow-auto p-1">
53
+ <div
54
+ v-for="col in allColumns"
55
+ :key="col"
56
+ class="group/col flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-50 text-xs"
57
+ >
58
+ <label class="flex items-center gap-2 flex-1 min-w-0 cursor-pointer">
59
+ <input
60
+ type="checkbox"
61
+ :checked="visibleColumns.has(col)"
62
+ class="size-3.5 accent-new-primary"
63
+ @change="toggleColumn(col)"
64
+ >
65
+ <span class="truncate">{{ col }}</span>
66
+ </label>
67
+ <button
68
+ class="shrink-0 px-1 text-new-primary hover:underline hidden group-hover/col:block group-focus-within/col:block"
69
+ @click="selectOnlyColumn(col)"
70
+ >
71
+ {{ t('Uniquement') }}
72
+ </button>
73
+ </div>
74
+ </div>
75
+ </PopoverPanel>
76
+ </Teleport>
77
+ </ClientOnly>
78
+ </Popover>
79
+ </template>
80
+
81
+ <script setup lang="ts">
82
+ import { computed, useTemplateRef } from 'vue'
83
+ import { autoUpdate, flip, shift, useFloating } from '@floating-ui/vue'
84
+ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
85
+ import { RiArrowDownSLine, RiLayoutColumnLine } from '@remixicon/vue'
86
+ import ClientOnly from '../ClientOnly.vue'
87
+ import BrandedButton from '../BrandedButton.vue'
88
+ import { useTranslation } from '../../composables/useTranslation'
89
+ import { useTabularContext } from './useTabularContext'
90
+
91
+ const { t } = useTranslation()
92
+ const {
93
+ allColumns,
94
+ visibleColumns,
95
+ hiddenCount,
96
+ toggleColumn,
97
+ showAllColumns,
98
+ hideAllColumns,
99
+ selectOnlyColumn,
100
+ } = useTabularContext()
101
+
102
+ // The panel is teleported, so it's positioned against its trigger with floating-ui.
103
+ const columnAnchor = useTemplateRef<InstanceType<typeof Popover>>('columnAnchor')
104
+ const columnPanel = useTemplateRef<InstanceType<typeof PopoverPanel>>('columnPanel')
105
+ const columnAnchorEl = computed(() => columnAnchor.value?.$el as HTMLElement | undefined)
106
+ const columnPanelEl = computed(() => columnPanel.value?.$el as HTMLElement | undefined)
107
+ const { floatingStyles: columnFloatingStyles } = useFloating(columnAnchorEl, columnPanelEl, {
108
+ placement: 'bottom-start',
109
+ middleware: [flip(), shift()],
110
+ whileElementsMounted: autoUpdate,
111
+ })
112
+ </script>