@indielayer/ui 1.8.4 → 1.9.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/docs/pages/component/select/usage.vue +17 -5
  2. package/docs/pages/component/table/index.vue +7 -0
  3. package/docs/pages/component/table/virtual.vue +53 -0
  4. package/lib/components/select/Select.vue.d.ts +36 -0
  5. package/lib/components/select/Select.vue.js +224 -201
  6. package/lib/components/select/theme/Select.base.theme.js +1 -1
  7. package/lib/components/table/Table.vue.d.ts +91 -4
  8. package/lib/components/table/Table.vue.js +214 -180
  9. package/lib/components/table/TableHead.vue.d.ts +10 -2
  10. package/lib/components/table/TableHead.vue.js +16 -13
  11. package/lib/components/table/TableHeader.vue.d.ts +0 -4
  12. package/lib/components/table/TableHeader.vue.js +9 -10
  13. package/lib/components/table/theme/TableHead.base.theme.js +7 -4
  14. package/lib/components/table/theme/TableHead.carbon.theme.js +7 -4
  15. package/lib/components/table/theme/TableHeader.base.theme.js +3 -3
  16. package/lib/components/table/theme/TableHeader.carbon.theme.js +1 -1
  17. package/lib/composables/index.d.ts +1 -0
  18. package/lib/composables/useVirtualList.d.ts +48 -0
  19. package/lib/composables/useVirtualList.js +123 -0
  20. package/lib/index.js +34 -32
  21. package/lib/index.umd.js +4 -4
  22. package/lib/node_modules/.pnpm/@vueuse_core@10.2.0_vue@3.3.9_typescript@5.2.2_/node_modules/@vueuse/core/index.js +254 -221
  23. package/lib/version.d.ts +1 -1
  24. package/lib/version.js +1 -1
  25. package/package.json +1 -1
  26. package/src/components/select/Select.vue +56 -26
  27. package/src/components/select/theme/Select.base.theme.ts +1 -1
  28. package/src/components/table/Table.vue +152 -113
  29. package/src/components/table/TableHead.vue +6 -2
  30. package/src/components/table/TableHeader.vue +0 -1
  31. package/src/components/table/theme/TableHead.base.theme.ts +7 -1
  32. package/src/components/table/theme/TableHead.carbon.theme.ts +7 -1
  33. package/src/components/table/theme/TableHeader.base.theme.ts +0 -2
  34. package/src/components/table/theme/TableHeader.carbon.theme.ts +0 -2
  35. package/src/composables/index.ts +1 -0
  36. package/src/composables/useVirtualList.ts +286 -0
  37. package/src/version.ts +1 -1
@@ -14,6 +14,17 @@ const selectProps = {
14
14
  type: String,
15
15
  default: 'Filter by...',
16
16
  },
17
+ virtualList: Boolean,
18
+ virtualListOffsetTop: Number,
19
+ virtualListOffsetBottom: Number,
20
+ virtualListItemHeight: {
21
+ type: Number,
22
+ default: 33,
23
+ },
24
+ virtualListOverscan: {
25
+ type: Number,
26
+ default: 5,
27
+ },
17
28
  }
18
29
 
19
30
  export type SelectOption = {
@@ -44,6 +55,7 @@ import { useCommon } from '../../composables/useCommon'
44
55
  import { useInputtable } from '../../composables/useInputtable'
45
56
  import { useInteractive } from '../../composables/useInteractive'
46
57
  import { useTheme, type ThemeComponent } from '../../composables/useTheme'
58
+ import { useVirtualList } from '../../composables/useVirtualList'
47
59
  import { checkIcon, selectIcon } from '../../common/icons'
48
60
 
49
61
  import XLabel from '../label/Label.vue'
@@ -113,6 +125,17 @@ const internalOptions = computed(() => {
113
125
 
114
126
  const availableOptions = computed(() => internalOptions.value.filter((option) => !option.disabled))
115
127
 
128
+ const { list, scrollTo: scrollToVirtualList, containerProps, wrapperProps } = useVirtualList(
129
+ internalOptions,
130
+ {
131
+ disabled: !props.virtualList,
132
+ itemHeight: props.virtualListItemHeight,
133
+ topOffset: props.virtualListOffsetTop || 0,
134
+ bottomOffset: props.virtualListOffsetBottom || 0,
135
+ overscan: props.virtualListOverscan,
136
+ },
137
+ )
138
+
116
139
  const isOpen = computed(() => popoverRef.value?.isOpen)
117
140
 
118
141
  watch(filter, (val) => {
@@ -158,7 +181,8 @@ function findSelectedIndex() {
158
181
  }
159
182
 
160
183
  function scrollToIndex(index: number) {
161
- if (itemsRef.value) itemsRef.value[index]?.$el.scrollIntoView({ block: 'nearest', inline: 'nearest' })
184
+ if (props.virtualList) scrollToVirtualList(index)
185
+ else if (itemsRef.value) itemsRef.value[index]?.$el.scrollIntoView({ block: 'nearest', inline: 'nearest' })
162
186
  }
163
187
 
164
188
  watch(selectedIndex, (index) => {
@@ -419,7 +443,9 @@ defineExpose({ focus, blur, reset, validate, setError })
419
443
  </div>
420
444
 
421
445
  <template #content>
422
- <x-popover-container :class="classes.content">
446
+ <x-popover-container
447
+ :class="classes.content"
448
+ >
423
449
  <slot name="content-header">
424
450
  <div v-if="filterable" :class="classes.search">
425
451
  <x-input
@@ -431,22 +457,24 @@ defineExpose({ focus, blur, reset, validate, setError })
431
457
  />
432
458
  </div>
433
459
  </slot>
434
- <div v-if="internalOptions.length > 0" :class="classes.contentBody">
435
- <x-menu-item
436
- v-for="(item, index) in internalOptions"
437
- :key="index"
438
- ref="itemsRef"
439
- :item="item"
440
- :size="size"
441
- :disabled="item.disabled"
442
- :selected="index === selectedIndex"
443
- :color="color"
444
- filled
445
- @click="() => !multiple && popoverRef?.hide()"
446
- />
447
- </div>
448
- <div v-else class="px-2 text-center text-secondary-400">
449
- No options
460
+ <div v-bind="containerProps" :class="classes.contentBody">
461
+ <div v-bind="wrapperProps">
462
+ <x-menu-item
463
+ v-for="item in list"
464
+ :key="item.index"
465
+ ref="itemsRef"
466
+ :item="item.data"
467
+ :size="size"
468
+ :disabled="item.data.disabled"
469
+ :selected="item.index === selectedIndex"
470
+ :color="color"
471
+ filled
472
+ @click="() => !multiple && popoverRef?.hide()"
473
+ />
474
+ </div>
475
+ <div v-if="list.length === 0" class="p-2 text-center text-secondary-400">
476
+ No options
477
+ </div>
450
478
  </div>
451
479
  <slot name="content-footer"></slot>
452
480
  </x-popover-container>
@@ -465,14 +493,16 @@ defineExpose({ focus, blur, reset, validate, setError })
465
493
  :readonly="readonly"
466
494
  v-on="inputListeners"
467
495
  >
468
- <option
469
- v-for="(option, index) in options"
470
- :key="index"
471
- :value="option.value"
472
- :disabled="option.disabled"
473
- >
474
- {{ option.label }}
475
- </option>
496
+ <template v-if="native">
497
+ <option
498
+ v-for="(option, index) in options"
499
+ :key="index"
500
+ :value="option.value"
501
+ :disabled="option.disabled"
502
+ >
503
+ {{ option.label }}
504
+ </option>
505
+ </template>
476
506
  </select>
477
507
 
478
508
  <div :class="classes.iconWrapper">
@@ -31,7 +31,7 @@ const theme: SelectTheme = {
31
31
 
32
32
  search: 'p-1 mb-0.5',
33
33
 
34
- contentBody: 'overflow-y-auto max-h-64',
34
+ contentBody: 'overflow-y-auto max-h-64 min-w-[280px]',
35
35
 
36
36
  iconWrapper: 'pointer-events-none absolute inset-y-0 right-0 flex items-center px-2',
37
37
 
@@ -32,6 +32,17 @@ const tableProps = {
32
32
  default: true,
33
33
  },
34
34
  expandable: Boolean,
35
+ virtualList: Boolean,
36
+ virtualListOffsetTop: Number,
37
+ virtualListOffsetBottom: Number,
38
+ virtualListItemHeight: {
39
+ type: Number,
40
+ default: 54,
41
+ },
42
+ virtualListOverscan: {
43
+ type: Number,
44
+ default: 5,
45
+ },
35
46
  }
36
47
 
37
48
  export type TableHeader = {
@@ -54,8 +65,9 @@ export default { name: 'XTable' }
54
65
  </script>
55
66
 
56
67
  <script setup lang="ts" generic="T">
57
- import { ref, type ExtractPublicPropTypes, type PropType, watch } from 'vue'
68
+ import { ref, type ExtractPublicPropTypes, type PropType, watch, computed } from 'vue'
58
69
  import { useTheme, type ThemeComponent } from '../../composables/useTheme'
70
+ import { useVirtualList } from '../../composables/useVirtualList'
59
71
 
60
72
  import XTableHead from './TableHead.vue'
61
73
  import XTableHeader, { type TableHeaderSort, type TableHeaderAlign } from './TableHeader.vue'
@@ -90,9 +102,22 @@ function clone<T>(source: T[]): T[] {
90
102
  }
91
103
  }
92
104
 
105
+ const items = computed(() => props.items)
106
+
107
+ const { list, containerProps, wrapperProps } = useVirtualList(
108
+ items,
109
+ {
110
+ disabled: !props.virtualList,
111
+ itemHeight: props.virtualListItemHeight || 54,
112
+ topOffset: props.virtualListOffsetTop || 0,
113
+ bottomOffset: props.virtualListOffsetBottom || 0,
114
+ overscan: props.virtualListOverscan,
115
+ },
116
+ )
117
+
93
118
  const internalItems = ref<internalT[]>([])
94
119
 
95
- watch(() => props.items, (newValue) => {
120
+ watch(items, (newValue) => {
96
121
  if (props.expandable) internalItems.value = clone(newValue as any)
97
122
  }, { immediate: true })
98
123
 
@@ -153,125 +178,139 @@ const { styles, classes, className } = useTheme('Table', {}, props)
153
178
  </script>
154
179
 
155
180
  <template>
156
- <div :class="[className, classes.wrapper]">
181
+ <div
182
+ :class="[className, classes.wrapper]"
183
+ v-bind="containerProps"
184
+ >
157
185
  <slot name="title"></slot>
158
186
  <slot name="actions"></slot>
159
187
 
160
- <table
161
- :style="styles"
162
- :class="classes.table"
188
+ <div
189
+ v-bind="wrapperProps"
190
+ :class="{
191
+ '!h-auto': props.loading
192
+ }"
163
193
  >
164
- <x-table-head>
165
- <x-table-header v-if="expandable" width="48" class="!p-0" :sticky-header="stickyHeader"/>
166
- <x-table-header
167
- v-for="(header, index) in headers"
168
- :key="index"
169
- :sticky-header="stickyHeader"
170
- :text-align="header.align"
171
- :sort="getSort(header.value, sort)"
172
- :sortable="header.sortable"
173
- :width="header.width"
174
- @click="header.sortable ? sortHeader(header) : null"
175
- >
176
- <slot :name="`header-${header.value}`" :header="header">
177
- {{ header.text }}
178
- </slot>
179
- </x-table-header>
180
- </x-table-head>
181
- <x-table-body>
182
- <template v-if="loading">
183
- <x-table-row
184
- v-for="(item, index) in Number(loadingLines)"
194
+ <table
195
+ :style="styles"
196
+ :class="classes.table"
197
+ >
198
+ <x-table-head :sticky-header="stickyHeader">
199
+ <x-table-header v-if="expandable" width="48" class="!p-0"/>
200
+ <x-table-header
201
+ v-for="(header, index) in headers"
185
202
  :key="index"
186
- :striped="striped"
203
+ :text-align="header.align"
204
+ :sort="getSort(header.value, sort)"
205
+ :sortable="header.sortable"
206
+ :width="header.width"
207
+ @click="header.sortable ? sortHeader(header) : null"
187
208
  >
188
- <x-table-cell
189
- v-for="(header, index2) in headers"
190
- :key="index2"
191
- :text-align="header.align"
192
- :width="header.width"
193
- :dense="dense"
194
- :fixed="fixed"
209
+ <slot :name="`header-${header.value}`" :header="header">
210
+ {{ header.text }}
211
+ </slot>
212
+ </x-table-header>
213
+ </x-table-head>
214
+ <x-table-body>
215
+ <template v-if="loading">
216
+ <x-table-row
217
+ v-for="(item, index) in Number(loadingLines)"
218
+ :key="index"
219
+ :striped="striped"
195
220
  >
196
- <slot :name="`loading-${header.value}`" :item="item">
197
- <x-skeleton
198
- class="max-w-[60%]"
199
- :shape="header.skeletonShape || 'line'"
200
- :class="{
201
- 'mx-auto': header.align === 'center',
202
- 'ml-auto': header.align === 'right',
203
- }"
204
- />
205
- </slot>
206
- </x-table-cell>
207
- </x-table-row>
208
- </template>
209
- <template v-else-if="error">
210
- <tr>
211
- <td colspan="999">
212
- <slot name="error"></slot>
213
- </td>
214
- </tr>
215
- </template>
216
- <template v-else-if="!items || items.length === 0">
217
- <tr>
218
- <td colspan="999">
219
- <slot name="empty"></slot>
220
- </td>
221
- </tr>
222
- </template>
223
- <template v-for="(item, index) in items" v-else :key="index">
224
- <x-table-row
225
- :pointer="pointer"
226
- :striped="striped"
227
- @click="$emit('click-row', item)"
228
- >
229
- <x-table-cell v-if="expandable" width="48" class="!p-1">
230
- <button
231
- type="button"
232
- class="px-3 p-2"
233
- :class="[dense ? 'p-0.5' : 'px-3 py-2']"
234
- @click="internalItems[index].__expanded = !internalItems[index].__expanded"
221
+ <x-table-cell
222
+ v-for="(header, index2) in headers"
223
+ :key="index2"
224
+ :text-align="header.align"
225
+ :width="header.width"
226
+ :dense="dense"
227
+ :fixed="fixed"
235
228
  >
236
- <x-icon
237
- :icon="chevronDownIcon"
238
- :size="dense ? 'xs' : 'md'"
239
- class="transition-transform"
240
- :class="{
241
- 'rotate-180': internalItems[index]?.__expanded,
242
- }"
243
- />
244
- </button>
245
- </x-table-cell>
246
- <x-table-cell
247
- v-for="(header, index2) in headers"
248
- :key="index2"
249
- :text-align="header.align"
250
- :truncate="header.truncate"
251
- :width="header.width"
252
- :dense="dense"
253
- :fixed="fixed"
229
+ <slot :name="`loading-${header.value}`" :item="item">
230
+ <x-skeleton
231
+ class="max-w-[60%]"
232
+ :shape="header.skeletonShape || 'line'"
233
+ :class="{
234
+ 'mx-auto': header.align === 'center',
235
+ 'ml-auto': header.align === 'right',
236
+ }"
237
+ />
238
+ </slot>
239
+ </x-table-cell>
240
+ </x-table-row>
241
+ </template>
242
+ <template v-else-if="error">
243
+ <tr>
244
+ <td colspan="999">
245
+ <slot name="error"></slot>
246
+ </td>
247
+ </tr>
248
+ </template>
249
+ <template v-else-if="!items || items.length === 0">
250
+ <tr>
251
+ <td colspan="999">
252
+ <slot name="empty"></slot>
253
+ </td>
254
+ </tr>
255
+ </template>
256
+ <template v-for="(item, index) in list" v-else :key="index">
257
+ <x-table-row
258
+ :pointer="pointer"
259
+ :striped="striped"
260
+ @click="$emit('click-row', item.data)"
254
261
  >
255
- <slot :name="`item-${header.value}`" :item="item">
256
- {{ getValue(item, header.value) }}
257
- </slot>
258
- </x-table-cell>
259
- </x-table-row>
260
- <tr v-if="expandable" :class="{ 'hidden': !internalItems[index]?.__expanded }">
261
- <td colspan="999">
262
- <div class="overflow-hidden transition-opacity" :class="[internalItems[index]?.__expanded ? '' : 'opacity-0 max-h-0']">
263
- <slot name="expanded-row" :item="item"></slot>
264
- </div>
265
- </td>
266
- </tr>
267
- </template>
268
- </x-table-body>
269
- <div
270
- v-if="loading"
271
- :class="classes.loadingWrapper"
272
- >
273
- <x-spinner size="lg"/>
274
- </div>
275
- </table>
262
+ <x-table-cell v-if="expandable" width="48" class="!p-1">
263
+ <button
264
+ type="button"
265
+ class="px-3 p-2"
266
+ :class="[dense ? 'p-0.5' : 'px-3 py-2']"
267
+ @click="internalItems[item.index].__expanded = !internalItems[item.index].__expanded"
268
+ >
269
+ <x-icon
270
+ :icon="chevronDownIcon"
271
+ :size="dense ? 'xs' : 'md'"
272
+ class="transition-transform"
273
+ :class="{
274
+ 'rotate-180': internalItems[item.index]?.__expanded,
275
+ }"
276
+ />
277
+ </button>
278
+ </x-table-cell>
279
+ <x-table-cell
280
+ v-for="(header, index2) in headers"
281
+ :key="index2"
282
+ :text-align="header.align"
283
+ :truncate="header.truncate"
284
+ :width="header.width"
285
+ :dense="dense"
286
+ :style="[props.virtualListItemHeight ? {
287
+ height: `${props.virtualListItemHeight}px`,
288
+ maxHeight: `${props.virtualListItemHeight}px`,
289
+ overflow: 'hidden',
290
+ whiteSpace: 'nowrap',
291
+ } : {}]"
292
+ >
293
+ <slot :name="`item-${header.value}`" :item="item.data">
294
+ {{ getValue(item.data, header.value) }}
295
+ </slot>
296
+ </x-table-cell>
297
+ </x-table-row>
298
+ <tr v-if="expandable" :class="{ 'hidden': !internalItems[item.index]?.__expanded }">
299
+ <td colspan="999">
300
+ <div class="overflow-hidden transition-opacity" :class="[internalItems[item.index]?.__expanded ? '' : 'opacity-0 max-h-0']">
301
+ <slot name="expanded-row" :item="item.data"></slot>
302
+ </div>
303
+ </td>
304
+ </tr>
305
+ </template>
306
+ </x-table-body>
307
+ <div
308
+ v-if="loading"
309
+ :class="classes.loadingWrapper"
310
+ >
311
+ <x-spinner size="lg"/>
312
+ </div>
313
+ </table>
314
+ </div>
276
315
  </div>
277
316
  </template>
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
- const tableHeadProps = {}
2
+ const tableHeadProps = {
3
+ stickyHeader: Boolean,
4
+ }
3
5
 
4
6
  export type TableHeadProps = ExtractPublicPropTypes<typeof tableHeadProps>
5
7
 
@@ -13,7 +15,9 @@ export default { name: 'XTableHead' }
13
15
  import type { ExtractPublicPropTypes } from 'vue'
14
16
  import { useTheme, type ThemeComponent } from '../../composables/useTheme'
15
17
 
16
- const { styles, classes, className } = useTheme('TableHead', {}, {})
18
+ const props = defineProps(tableHeadProps)
19
+
20
+ const { styles, classes, className } = useTheme('TableHead', {}, props)
17
21
  </script>
18
22
 
19
23
  <template>
@@ -15,7 +15,6 @@ const tableHeaderProps = {
15
15
  default: 'left',
16
16
  validator: (value: string) => validators.textAlign.includes(value as any),
17
17
  },
18
- stickyHeader: Boolean,
19
18
  }
20
19
 
21
20
  export type TableHeaderSort = typeof validators.sort[number]
@@ -2,7 +2,13 @@ import type { TableHeadTheme } from '../TableHead.vue'
2
2
 
3
3
  const theme: TableHeadTheme = {
4
4
  classes: {
5
- thead: 'align-bottom bg-secondary-50 dark:bg-secondary-700',
5
+ thead: ({ props }) => {
6
+ const classes = ['align-bottom bg-secondary-50 dark:bg-secondary-700']
7
+
8
+ if (props.stickyHeader) classes.push('sticky top-0 z-10')
9
+
10
+ return classes
11
+ },
6
12
  row: 'text-sm text-secondary-600 dark:text-secondary-200 border-b',
7
13
  },
8
14
  }
@@ -2,7 +2,13 @@ import type { TableHeadTheme } from '../TableHead.vue'
2
2
 
3
3
  const theme: TableHeadTheme = {
4
4
  classes: {
5
- thead: 'align-bottom',
5
+ thead: ({ props }) => {
6
+ const classes = ['align-bottom']
7
+
8
+ if (props.stickyHeader) classes.push('sticky top-0 z-10')
9
+
10
+ return classes
11
+ },
6
12
  row: 'text-secondary-900 dark:text-secondary-50',
7
13
  },
8
14
  }
@@ -7,8 +7,6 @@ const theme: TableHeaderTheme = {
7
7
 
8
8
  if (props.sortable) classes.push('cursor-pointer hover:text-secondary-800 dark:hover:text-secondary-300 transition-colors duration-150 ease-in-out')
9
9
 
10
- if (props.stickyHeader) classes.push('sticky top-0')
11
-
12
10
  if (props.textAlign === 'left') classes.push('text-left')
13
11
  if (props.textAlign === 'right') classes.push('text-right')
14
12
  if (props.textAlign === 'center') classes.push('text-center')
@@ -7,8 +7,6 @@ const theme: TableHeaderTheme = {
7
7
 
8
8
  if (props.sortable) classes.push('cursor-pointer hover:bg-secondary-300 dark:hover:bg-secondary-600 transition-colors duration-150 ease-in-out pr-5')
9
9
 
10
- if (props.stickyHeader) classes.push('sticky top-0')
11
-
12
10
  if (props.textAlign === 'left') classes.push('text-left')
13
11
  if (props.textAlign === 'right') classes.push('text-right')
14
12
  if (props.textAlign === 'center') classes.push('text-center')
@@ -5,3 +5,4 @@ export * from './useCSS'
5
5
  export * from './useInputtable'
6
6
  export * from './useInteractive'
7
7
  export * from './useNotifications'
8
+ export * from './useVirtualList'