@polymarbot/nuxt-layer-shadcn-ui 0.6.3 → 0.7.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.
Files changed (37) hide show
  1. package/app/components/ui/DataTable/index.stories.ts +249 -154
  2. package/app/components/ui/DataTable/index.vue +94 -65
  3. package/app/components/ui/DataTable/types.ts +2 -0
  4. package/app/components/ui/DatePicker/index.stories.ts +1 -1
  5. package/app/components/ui/DatePicker/index.vue +1 -1
  6. package/app/components/ui/DateRangePicker/index.stories.ts +2 -2
  7. package/app/components/ui/DateRangePicker/index.vue +2 -2
  8. package/app/components/ui/InfiniteDataTable/en.json +6 -0
  9. package/app/components/ui/InfiniteDataTable/index.stories.ts +165 -0
  10. package/app/components/ui/InfiniteDataTable/index.vue +239 -0
  11. package/app/components/ui/InfiniteDataTable/types.ts +35 -0
  12. package/app/components/ui/InputRange/index.stories.ts +2 -2
  13. package/app/components/ui/InputRange/index.vue +2 -2
  14. package/app/components/ui/SearchSelect/index.vue +4 -4
  15. package/app/components/ui/Select/index.stories.ts +10 -0
  16. package/app/components/ui/Select/index.vue +10 -3
  17. package/app/components/ui/Select/types.ts +2 -0
  18. package/i18n/messages/ar.json +6 -0
  19. package/i18n/messages/de.json +6 -0
  20. package/i18n/messages/en.json +6 -0
  21. package/i18n/messages/es.json +6 -0
  22. package/i18n/messages/fr.json +6 -0
  23. package/i18n/messages/hi.json +6 -0
  24. package/i18n/messages/id.json +6 -0
  25. package/i18n/messages/it.json +6 -0
  26. package/i18n/messages/ja.json +6 -0
  27. package/i18n/messages/ko.json +6 -0
  28. package/i18n/messages/nl.json +6 -0
  29. package/i18n/messages/pl.json +6 -0
  30. package/i18n/messages/pt.json +6 -0
  31. package/i18n/messages/ru.json +6 -0
  32. package/i18n/messages/th.json +6 -0
  33. package/i18n/messages/tr.json +6 -0
  34. package/i18n/messages/vi.json +6 -0
  35. package/i18n/messages/zh-CN.json +6 -0
  36. package/i18n/messages/zh-TW.json +6 -0
  37. package/package.json +2 -2
@@ -0,0 +1,239 @@
1
+ <script setup lang="ts" generic="TData extends Record<string, any>">
2
+ import type { InfiniteDataTableFetchParams, InfiniteDataTableProps } from './types'
3
+
4
+ const props = withDefaults(defineProps<InfiniteDataTableProps<TData>>(), {
5
+ columns: () => [],
6
+ fetchMethod: undefined,
7
+ autoFetch: true,
8
+ filters: undefined,
9
+ pageSize: 30,
10
+ height: undefined,
11
+ clickable: false,
12
+ })
13
+
14
+ const emit = defineEmits<{
15
+ 'update:filters': [filters: Record<string, any>]
16
+ 'rowClick': [row: TData, index: number, event: MouseEvent]
17
+ }>()
18
+
19
+ const T = useTranslations('components.ui.InfiniteDataTable')
20
+
21
+ // -- Internal state --
22
+
23
+ const loading = ref(false)
24
+ const internalData = ref<TData[]>([]) as Ref<TData[]>
25
+ const next = ref<string | undefined>(undefined)
26
+ const hasMore = ref(true)
27
+ const total = ref<number | undefined>(undefined)
28
+ const requestVersion = ref(0)
29
+
30
+ const sortState = ref<{ sortBy: string | null, sortOrder: number | null }>({
31
+ sortBy: props.filters?.sortBy ? String(props.filters.sortBy) : null,
32
+ sortOrder: props.filters?.sortOrder ? Number(props.filters.sortOrder) : null,
33
+ })
34
+
35
+ const isInitialLoad = computed(() => loading.value && internalData.value.length === 0)
36
+
37
+ // -- IntersectionObserver root: only when internal scroll is active --
38
+
39
+ const dataTableRef = ref<{ scrollEl?: HTMLElement } | null>(null)
40
+ const intersectionOptions = computed<IntersectionObserverInit | undefined>(() => { // eslint-disable-line no-undef
41
+ if (!props.height) return undefined
42
+ const root = dataTableRef.value?.scrollEl
43
+ return root ? { root } : undefined
44
+ })
45
+
46
+ // -- Helpers --
47
+
48
+ function getFilters (): Record<string, any> {
49
+ return {
50
+ ...(props.filters ?? {}),
51
+ sortBy: sortState.value.sortBy,
52
+ sortOrder: sortState.value.sortOrder,
53
+ }
54
+ }
55
+
56
+ function buildFetchParams (): InfiniteDataTableFetchParams {
57
+ return {
58
+ ...getFilters(),
59
+ next: next.value,
60
+ limit: props.pageSize,
61
+ }
62
+ }
63
+
64
+ function resetState () {
65
+ internalData.value = []
66
+ next.value = undefined
67
+ hasMore.value = true
68
+ total.value = undefined
69
+ }
70
+
71
+ // -- Loading --
72
+
73
+ async function loadMore () {
74
+ if (!props.fetchMethod) return
75
+ if (loading.value || !hasMore.value) return
76
+
77
+ const currentVersion = ++requestVersion.value
78
+ loading.value = true
79
+ try {
80
+ const result = await props.fetchMethod(buildFetchParams())
81
+ if (currentVersion !== requestVersion.value) return
82
+
83
+ internalData.value = [ ...internalData.value, ...result.items ]
84
+ if (result.total != null) total.value = result.total
85
+ next.value = result.next
86
+ hasMore.value = !!result.next
87
+ } catch (error) {
88
+ if (currentVersion !== requestVersion.value) return
89
+ console.error('InfiniteDataTable loadMore failed:', error)
90
+ } finally {
91
+ if (currentVersion === requestVersion.value) loading.value = false
92
+ }
93
+ }
94
+
95
+ async function refresh () {
96
+ resetState()
97
+ emit('update:filters', getFilters())
98
+ await loadMore()
99
+ }
100
+
101
+ function scrollToTop () {
102
+ const el = dataTableRef.value?.scrollEl
103
+ if (!el) return
104
+ if (el.scrollHeight > el.clientHeight) {
105
+ el.scrollTo({ top: 0, behavior: 'smooth' })
106
+ } else {
107
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' })
108
+ }
109
+ }
110
+
111
+ // -- Sort: incremental loading requires a full reset on sort change --
112
+
113
+ let sortUpdatePending = false
114
+
115
+ function onSortByUpdate (value: string | null) {
116
+ sortState.value.sortBy = value
117
+ scheduleAfterSort()
118
+ }
119
+
120
+ function onSortOrderUpdate (value: number | null) {
121
+ sortState.value.sortOrder = value
122
+ scheduleAfterSort()
123
+ }
124
+
125
+ function scheduleAfterSort () {
126
+ if (sortUpdatePending) return
127
+ sortUpdatePending = true
128
+ nextTick(() => {
129
+ sortUpdatePending = false
130
+ refresh()
131
+ })
132
+ }
133
+
134
+ // -- External filters: any change resets and reloads --
135
+
136
+ watch(() => props.filters, (newVal, oldVal) => {
137
+ if (JSON.stringify(newVal) === JSON.stringify(oldVal)) return
138
+ refresh()
139
+ }, { deep: true })
140
+
141
+ // -- Expose --
142
+
143
+ defineExpose({
144
+ refresh,
145
+ loadMore,
146
+ scrollToTop,
147
+ })
148
+
149
+ // -- Lifecycle --
150
+
151
+ onMounted(() => {
152
+ emit('update:filters', getFilters())
153
+ if (props.autoFetch) loadMore()
154
+ })
155
+ </script>
156
+
157
+ <template>
158
+ <DataTable
159
+ ref="dataTableRef"
160
+ :data="internalData"
161
+ :columns
162
+ :height
163
+ :loading="isInitialLoad"
164
+ :clickable
165
+ :sortBy="sortState.sortBy"
166
+ :sortOrder="sortState.sortOrder"
167
+ @update:sortBy="onSortByUpdate"
168
+ @update:sortOrder="onSortOrderUpdate"
169
+ @rowClick="(row, i, e) => emit('rowClick', row as TData, i, e)"
170
+ >
171
+ <template
172
+ v-for="name in Object.keys($slots).filter(n => n !== 'bodyEnd' && n !== 'footer')"
173
+ :key="name"
174
+ #[name]="slotData"
175
+ >
176
+ <slot
177
+ :name="name"
178
+ v-bind="slotData ?? {}"
179
+ />
180
+ </template>
181
+
182
+ <template
183
+ v-if="hasMore || internalData.length > 0"
184
+ #bodyEnd
185
+ >
186
+ <div
187
+ v-if="!hasMore"
188
+ class="py-2 text-xs text-muted-foreground text-center"
189
+ >
190
+ {{ T('allLoaded') }}
191
+ </div>
192
+ <EffectIntersectionChecker
193
+ v-else-if="!isInitialLoad"
194
+ :disabled="loading"
195
+ :options="intersectionOptions"
196
+ class="py-2 flex items-center justify-center"
197
+ @show="loadMore"
198
+ >
199
+ <Icon
200
+ name="loader-circle"
201
+ class="size-4 animate-spin text-muted-foreground"
202
+ />
203
+ </EffectIntersectionChecker>
204
+ </template>
205
+
206
+ <template #footer>
207
+ <slot name="footer">
208
+ <div class="gap-2 text-xs flex items-center justify-between">
209
+ <div class="gap-2 flex items-center">
210
+ <Tooltip :text="T('scrollToTop')">
211
+ <Button
212
+ variant="ghost"
213
+ size="icon-sm"
214
+ icon="arrow-up-to-line"
215
+ :disabled="loading || internalData.length === 0"
216
+ @click="scrollToTop"
217
+ />
218
+ </Tooltip>
219
+ <Tooltip :text="T('refresh')">
220
+ <Button
221
+ variant="ghost"
222
+ size="icon-sm"
223
+ icon="rotate-cw"
224
+ :disabled="loading"
225
+ @click="refresh"
226
+ />
227
+ </Tooltip>
228
+ </div>
229
+ <span
230
+ v-if="total != null"
231
+ class="text-muted-foreground"
232
+ >
233
+ {{ T('count', { loaded: internalData.length, total }) }}
234
+ </span>
235
+ </div>
236
+ </slot>
237
+ </template>
238
+ </DataTable>
239
+ </template>
@@ -0,0 +1,35 @@
1
+ import type { DataTableColumn } from '../DataTable/types'
2
+
3
+ export interface InfiniteDataTableFetchParams {
4
+ /** Opaque token returned by the previous page; absent on the first page */
5
+ next?: string
6
+ /** Page size requested */
7
+ limit: number
8
+ /** Filter / sort fields are spread onto the params object */
9
+ [key: string]: any
10
+ }
11
+
12
+ export interface InfiniteDataTableFetchResult<T = Record<string, any>> {
13
+ items: T[]
14
+ /** Token for the next page; absent signals "no more" */
15
+ next?: string
16
+ /** Optional total count */
17
+ total?: number
18
+ }
19
+
20
+ export interface InfiniteDataTableProps<T = Record<string, any>> {
21
+ /** Column definitions */
22
+ columns?: DataTableColumn[]
23
+ /** Async function to fetch a page of rows */
24
+ fetchMethod?: (params: InfiniteDataTableFetchParams) => Promise<InfiniteDataTableFetchResult<T>>
25
+ /** Whether to fetch the first page on mount (default: true) */
26
+ autoFetch?: boolean
27
+ /** External filter state — changing this resets and reloads */
28
+ filters?: Record<string, any>
29
+ /** Number of rows per page (default: 30) */
30
+ pageSize?: number
31
+ /** Fixed height enabling internal vertical scroll (e.g. '400px') */
32
+ height?: string
33
+ /** Whether rows are clickable (shows pointer cursor and pairs with `@rowClick`) */
34
+ clickable?: boolean
35
+ }
@@ -19,8 +19,8 @@ const meta = {
19
19
  end: undefined,
20
20
  min: 0,
21
21
  max: 100,
22
- startPlaceholder: undefined,
23
- endPlaceholder: undefined,
22
+ startPlaceholder: '',
23
+ endPlaceholder: '',
24
24
  disabled: false,
25
25
  },
26
26
  render: args => ({
@@ -38,7 +38,7 @@ const end = computed({
38
38
  v-bind="$attrs"
39
39
  :min="min"
40
40
  :max="end ?? max"
41
- :placeholder="startPlaceholder ?? T('startPlaceholder')"
41
+ :placeholder="startPlaceholder || T('startPlaceholder')"
42
42
  fluid
43
43
  />
44
44
  <span class="leading-0 text-muted-foreground">
@@ -49,7 +49,7 @@ const end = computed({
49
49
  v-bind="$attrs"
50
50
  :min="start ?? min"
51
51
  :max="max"
52
- :placeholder="endPlaceholder ?? T('endPlaceholder')"
52
+ :placeholder="endPlaceholder || T('endPlaceholder')"
53
53
  fluid
54
54
  />
55
55
  </div>
@@ -135,8 +135,8 @@ function handleSearch (value: string) {
135
135
  // -- Empty text --
136
136
 
137
137
  const computedEmptyText = computed(() => {
138
- if (keyword.value) return props.searchEmptyText ?? T('noSearchItems')
139
- return props.emptyText ?? T('noItems')
138
+ if (keyword.value) return props.searchEmptyText || T('noSearchItems')
139
+ return props.emptyText || T('noItems')
140
140
  })
141
141
 
142
142
  // -- Popover open/close --
@@ -188,9 +188,10 @@ defineExpose({ refresh: resetAndLoad })
188
188
  :options="displayedOptions"
189
189
  :filter="filterFunction"
190
190
  :placeholder="placeholder"
191
- :disabled="disabled"
192
191
  :searchPlaceholder="searchPlaceholder"
193
192
  :emptyText="computedEmptyText"
193
+ :disabled="disabled"
194
+ :loading="isLoading"
194
195
  @search="handleSearch"
195
196
  @open="handleOpen"
196
197
  @close="handleClose"
@@ -220,7 +221,6 @@ defineExpose({ refresh: resetAndLoad })
220
221
  <EffectIntersectionChecker
221
222
  v-if="hasMore"
222
223
  :disabled="isLoading"
223
- bao
224
224
  class="py-2 flex items-center justify-center"
225
225
  @show="loadMore"
226
226
  >
@@ -36,6 +36,7 @@ const meta = {
36
36
  argTypes: {
37
37
  placeholder: { control: 'text' },
38
38
  disabled: { control: 'boolean' },
39
+ loading: { control: 'boolean' },
39
40
  filter: { control: 'boolean' },
40
41
  multiple: { control: 'boolean' },
41
42
  searchPlaceholder: { control: 'text' },
@@ -44,6 +45,7 @@ const meta = {
44
45
  args: {
45
46
  placeholder: 'Select an option',
46
47
  disabled: false,
48
+ loading: false,
47
49
  filter: false,
48
50
  multiple: false,
49
51
  searchPlaceholder: '',
@@ -277,6 +279,14 @@ export const Disabled: Story = {
277
279
  }),
278
280
  }
279
281
 
282
+ export const Loading: Story = {
283
+ parameters: noControls,
284
+ args: {
285
+ loading: true,
286
+ placeholder: 'Loading options',
287
+ },
288
+ }
289
+
280
290
  export const EventHandling: Story = {
281
291
  parameters: noControls,
282
292
  render: () => ({
@@ -29,6 +29,7 @@ const props = withDefaults(defineProps<SelectProps<TValue, TMeta>>(), {
29
29
  modelValue: undefined,
30
30
  placeholder: undefined,
31
31
  disabled: false,
32
+ loading: false,
32
33
  filter: false,
33
34
  searchPlaceholder: undefined,
34
35
  emptyText: undefined,
@@ -188,7 +189,7 @@ function handleClear (event: MouseEvent) {
188
189
  v-else
189
190
  class="text-muted-foreground"
190
191
  >
191
- {{ placeholder ?? T('placeholder') }}
192
+ {{ placeholder || T('placeholder') }}
192
193
  </span>
193
194
  </span>
194
195
  <InputGroupAddon
@@ -205,6 +206,12 @@ function handleClear (event: MouseEvent) {
205
206
  </InputGroupAddon>
206
207
  <InputGroupAddon align="inline-end">
207
208
  <Icon
209
+ v-if="loading"
210
+ name="loader-circle"
211
+ class="size-4 animate-spin opacity-50"
212
+ />
213
+ <Icon
214
+ v-else
208
215
  name="chevron-down"
209
216
  class="size-4 opacity-50"
210
217
  />
@@ -221,11 +228,11 @@ function handleClear (event: MouseEvent) {
221
228
  >
222
229
  <CommandInput
223
230
  v-if="!!filter"
224
- :placeholder="searchPlaceholder ?? T('searchPlaceholder')"
231
+ :placeholder="searchPlaceholder || T('searchPlaceholder')"
225
232
  />
226
233
  <CommandList>
227
234
  <CommandEmpty>
228
- {{ emptyText ?? T('noItems') }}
235
+ {{ emptyText || T('noItems') }}
229
236
  </CommandEmpty>
230
237
 
231
238
  <template
@@ -13,6 +13,8 @@ export type SelectBaseProps<V extends string | number = string, M = unknown> = {
13
13
  options?: SelectOption<V, M>[]
14
14
  placeholder?: string
15
15
  disabled?: boolean
16
+ /** Show a spinner in place of the chevron */
17
+ loading?: boolean
16
18
  /** true: enable client-side label filter; function: custom filter (disables internal filter) */
17
19
  filter?: boolean | SelectFilterFunction
18
20
  /** Search input placeholder */
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "لا توجد عناصر"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— تم تحميل الكل —",
56
+ "count": "{loaded} من {total} تم تحميله",
57
+ "refresh": "تحديث",
58
+ "scrollToTop": "الرجوع إلى الأعلى"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "أقصى",
56
62
  "startPlaceholder": "أدنى"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Keine Elemente"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— alle geladen —",
56
+ "count": "{loaded} von {total} geladen",
57
+ "refresh": "Aktualisieren",
58
+ "scrollToTop": "Nach oben scrollen"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Maximum",
56
62
  "startPlaceholder": "Minimum"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "No items"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— all loaded —",
56
+ "count": "{loaded} of {total} loaded",
57
+ "refresh": "Refresh",
58
+ "scrollToTop": "Scroll to top"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Max",
56
62
  "startPlaceholder": "Min"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Sin elementos"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— todos cargados —",
56
+ "count": "{loaded} de {total} cargados",
57
+ "refresh": "Actualizar",
58
+ "scrollToTop": "Desplazarse hacia arriba"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Máximo",
56
62
  "startPlaceholder": "Mínimo"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Aucun élément"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— tout chargé —",
56
+ "count": "{loaded} sur {total} chargés",
57
+ "refresh": "Actualiser",
58
+ "scrollToTop": "Retour au haut"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Max",
56
62
  "startPlaceholder": "Min"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "कोई आइटम नहीं"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— सभी लोड हो गए —",
56
+ "count": "{loaded} of {total} लोड हो गया",
57
+ "refresh": "ताज़ा करें",
58
+ "scrollToTop": "शीर्ष पर जाएं"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "अधिकतम",
56
62
  "startPlaceholder": "न्यूनतम"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Tidak ada item"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— semua dimuat —",
56
+ "count": "{loaded} dari {total} dimuat",
57
+ "refresh": "Segarkan",
58
+ "scrollToTop": "Gulir ke atas"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Maksimal",
56
62
  "startPlaceholder": "Minimal"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Nessun elemento"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— completamente caricato —",
56
+ "count": "{loaded} di {total} caricati",
57
+ "refresh": "Aggiorna",
58
+ "scrollToTop": "Scorri verso l'alto"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Massimo",
56
62
  "startPlaceholder": "Minimo"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "アイテムなし"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "すべて読み込み完了",
56
+ "count": "{loaded}/{total} 読み込み済み",
57
+ "refresh": "更新",
58
+ "scrollToTop": "トップへスクロール"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "最大値",
56
62
  "startPlaceholder": "最小値"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "항목 없음"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— 모두 로드됨 —",
56
+ "count": "{loaded}개/{total}개 로드됨",
57
+ "refresh": "새로고침",
58
+ "scrollToTop": "상단으로 이동"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "최대값",
56
62
  "startPlaceholder": "최소값"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Geen items"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— alles geladen —",
56
+ "count": "{loaded} van {total} geladen",
57
+ "refresh": "Vernieuwen",
58
+ "scrollToTop": "Naar boven schuiven"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Maximum",
56
62
  "startPlaceholder": "Minimum"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Brak elementów"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— wszystko załadowane —",
56
+ "count": "{loaded} z {total} załadowane",
57
+ "refresh": "Odśwież",
58
+ "scrollToTop": "Przewiń do góry"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Maksimum",
56
62
  "startPlaceholder": "Minimum"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Sem itens"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— Tudo carregado —",
56
+ "count": "{loaded} de {total} carregado(s)",
57
+ "refresh": "Atualizar",
58
+ "scrollToTop": "Voltar ao topo"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Máximo",
56
62
  "startPlaceholder": "Mínimo"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Нет элементов"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— все загружено —",
56
+ "count": "{loaded} из {total} загружено",
57
+ "refresh": "Обновить",
58
+ "scrollToTop": "Прокрутить вверх"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Максимум",
56
62
  "startPlaceholder": "Минимум"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "ไม่มีรายการ"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— โหลดเสร็จแล้วทั้งหมด —",
56
+ "count": "{loaded} จาก {total} โหลดแล้ว",
57
+ "refresh": "รีเฟรช",
58
+ "scrollToTop": "เลื่อนขึ้นด้านบน"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "สูงสุด",
56
62
  "startPlaceholder": "ต่ำสุด"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Öğe yok"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— tümü yüklendi —",
56
+ "count": "{loaded} / {total} yüklendi",
57
+ "refresh": "Yenile",
58
+ "scrollToTop": "Başa dön"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Maks",
56
62
  "startPlaceholder": "Min"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "Không có mục"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "— tất cả đã tải —",
56
+ "count": "{loaded} của {total} đã tải",
57
+ "refresh": "Làm mới",
58
+ "scrollToTop": "Cuộn lên đầu"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "Tối đa",
56
62
  "startPlaceholder": "Tối thiểu"
@@ -51,6 +51,12 @@
51
51
  "Dropdown": {
52
52
  "empty": "无项目"
53
53
  },
54
+ "InfiniteDataTable": {
55
+ "allLoaded": "全部已加载",
56
+ "count": "{loaded} / {total} 已加载",
57
+ "refresh": "刷新",
58
+ "scrollToTop": "回到顶部"
59
+ },
54
60
  "InputRange": {
55
61
  "endPlaceholder": "最大值",
56
62
  "startPlaceholder": "最小值"