@katlux/block-ecommerce 0.1.1-alpha.94 → 0.1.1-alpha.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.1.1-alpha.94",
7
+ "version": "0.1.1-alpha.96",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -1,4 +1,4 @@
1
- import type { KProductItemData, KProductRowAction } from '@katlux/providers';
1
+ import type { KProductRowAction } from '@katlux/providers';
2
2
  import { ADataProvider } from '@katlux/providers';
3
3
  type __VLS_Props = {
4
4
  dataProvider: ADataProvider;
@@ -8,35 +8,34 @@ type __VLS_Props = {
8
8
  gap?: string;
9
9
  locale?: string;
10
10
  emptyText?: string;
11
+ currentPage?: number;
12
+ pageSize?: number;
13
+ virtualScroll?: boolean;
14
+ itemHeight?: number;
15
+ viewportHeight?: number;
16
+ buffer?: number;
11
17
  };
12
- declare var __VLS_1: {
13
- loading: true;
14
- }, __VLS_3: {}, __VLS_5: {
15
- item: KProductItemData;
16
- index: number;
17
- }, __VLS_15: string, __VLS_16: any;
18
- type __VLS_Slots = {} & {
19
- [K in NonNullable<typeof __VLS_15>]?: (props: typeof __VLS_16) => any;
20
- } & {
21
- loading?: (props: typeof __VLS_1) => any;
22
- } & {
23
- empty?: (props: typeof __VLS_3) => any;
24
- } & {
25
- default?: (props: typeof __VLS_5) => any;
26
- };
27
- declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
28
- rowActions: KProductRowAction[];
29
- locale: string;
18
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
19
+ "update:currentPage": (page: number) => any;
20
+ "update:pageSize": (size: number) => any;
21
+ "page-change": (page: number) => any;
22
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
23
+ "onUpdate:currentPage"?: ((page: number) => any) | undefined;
24
+ "onUpdate:pageSize"?: ((size: number) => any) | undefined;
25
+ "onPage-change"?: ((page: number) => any) | undefined;
26
+ }>, {
27
+ currentPage: number;
28
+ pageSize: number;
30
29
  layout: "grid" | "list";
30
+ rowActions: KProductRowAction[];
31
31
  gridColumns: number | string;
32
32
  gap: string;
33
+ locale: string;
33
34
  emptyText: string;
35
+ virtualScroll: boolean;
36
+ itemHeight: number;
37
+ viewportHeight: number;
38
+ buffer: number;
34
39
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
35
- declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
36
40
  declare const _default: typeof __VLS_export;
37
41
  export default _default;
38
- type __VLS_WithSlots<T, S> = T & {
39
- new (): {
40
- $slots: S;
41
- };
42
- };
@@ -7,7 +7,19 @@ export interface KProductListProps {
7
7
  gridColumns?: number | string;
8
8
  gap?: string;
9
9
  locale?: string;
10
+ emptyText?: string;
11
+ currentPage?: number;
12
+ pageSize?: number;
13
+ virtualScroll?: boolean;
14
+ itemHeight?: number;
15
+ viewportHeight?: number;
16
+ buffer?: number;
10
17
  }
18
+ export type KProductListEmits = {
19
+ (e: 'update:currentPage', page: number): void;
20
+ (e: 'update:pageSize', size: number): void;
21
+ (e: 'page-change', page: number): void;
22
+ };
11
23
  export declare const KProductListDefaultProps: {
12
24
  dataProvider: {
13
25
  type: PropType<ADataProvider>;
@@ -29,11 +41,47 @@ export declare const KProductListDefaultProps: {
29
41
  type: StringConstructor;
30
42
  default: string;
31
43
  };
44
+ locale: {
45
+ type: StringConstructor;
46
+ default: undefined;
47
+ };
48
+ emptyText: {
49
+ type: StringConstructor;
50
+ default: undefined;
51
+ };
52
+ currentPage: {
53
+ type: NumberConstructor;
54
+ default: undefined;
55
+ };
56
+ pageSize: {
57
+ type: NumberConstructor;
58
+ default: undefined;
59
+ };
60
+ virtualScroll: {
61
+ type: BooleanConstructor;
62
+ default: boolean;
63
+ };
64
+ itemHeight: {
65
+ type: NumberConstructor;
66
+ default: undefined;
67
+ };
68
+ viewportHeight: {
69
+ type: NumberConstructor;
70
+ default: number;
71
+ };
72
+ buffer: {
73
+ type: NumberConstructor;
74
+ default: number;
75
+ };
32
76
  };
33
- export declare function useKProductListLogic(props: KProductListProps): {
77
+ export declare function useKProductListLogic(props: KProductListProps, emit?: KProductListEmits): {
34
78
  isGrid: import("vue").ComputedRef<boolean>;
35
79
  pageRows: import("vue").ComputedRef<KProductItemData[]>;
36
80
  loading: import("vue").Ref<boolean, boolean>;
37
81
  initialLoad: import("vue").Ref<boolean, boolean>;
38
82
  rowCount: import("vue").Ref<number, number>;
83
+ currentPage: import("vue").Ref<number, number>;
84
+ pageSize: import("vue").Ref<number, number>;
85
+ setCurrentPage: (page: number) => void;
86
+ setPageSize: (size: number) => void;
39
87
  };
@@ -1,4 +1,4 @@
1
- import { computed } from "vue";
1
+ import { computed, watch } from "vue";
2
2
  import { useDataProvider } from "#toolkit/composables/useDataProvider";
3
3
  export const KProductListDefaultProps = {
4
4
  dataProvider: {
@@ -20,17 +20,79 @@ export const KProductListDefaultProps = {
20
20
  gap: {
21
21
  type: String,
22
22
  default: "1rem"
23
+ },
24
+ locale: {
25
+ type: String,
26
+ default: void 0
27
+ },
28
+ emptyText: {
29
+ type: String,
30
+ default: void 0
31
+ },
32
+ currentPage: {
33
+ type: Number,
34
+ default: void 0
35
+ },
36
+ pageSize: {
37
+ type: Number,
38
+ default: void 0
39
+ },
40
+ virtualScroll: {
41
+ type: Boolean,
42
+ default: false
43
+ },
44
+ itemHeight: {
45
+ type: Number,
46
+ default: void 0
47
+ },
48
+ viewportHeight: {
49
+ type: Number,
50
+ default: 600
51
+ },
52
+ buffer: {
53
+ type: Number,
54
+ default: 2
23
55
  }
24
56
  };
25
- export function useKProductListLogic(props) {
57
+ export function useKProductListLogic(props, emit) {
26
58
  const isGrid = computed(() => props.layout !== "list");
27
- const { pageData, loading, initialLoad, rowCount } = useDataProvider(() => props.dataProvider);
59
+ const {
60
+ pageData,
61
+ loading,
62
+ initialLoad,
63
+ rowCount,
64
+ currentPage: dpCurrentPage,
65
+ pageSize: dpPageSize,
66
+ setCurrentPage,
67
+ setPageSize
68
+ } = useDataProvider(() => props.dataProvider);
69
+ watch(() => props.currentPage, (val) => {
70
+ if (val !== void 0 && val !== dpCurrentPage.value) {
71
+ setCurrentPage(val);
72
+ }
73
+ }, { immediate: true });
74
+ watch(() => props.pageSize, (val) => {
75
+ if (val !== void 0 && val !== dpPageSize.value) {
76
+ setPageSize(val);
77
+ }
78
+ }, { immediate: true });
79
+ watch(dpCurrentPage, (val) => {
80
+ emit?.("update:currentPage", val);
81
+ emit?.("page-change", val);
82
+ });
83
+ watch(dpPageSize, (val) => {
84
+ emit?.("update:pageSize", val);
85
+ });
28
86
  const pageRows = computed(() => pageData.value || []);
29
87
  return {
30
88
  isGrid,
31
89
  pageRows,
32
90
  loading,
33
91
  initialLoad,
34
- rowCount
92
+ rowCount,
93
+ currentPage: dpCurrentPage,
94
+ pageSize: dpPageSize,
95
+ setCurrentPage,
96
+ setPageSize
35
97
  };
36
98
  }
@@ -1,65 +1,80 @@
1
- <template>
2
- <div class="k-product-list" :class="{ 'is-grid': isGrid, 'is-list': !isGrid }" :style="containerVars">
3
- <!-- Loading state -->
4
- <div v-if="loading" class="loading-container">
5
- <slot name="loading" :loading="loading">
6
- <div class="list-container">
7
- <div v-for="i in skeletonCount" :key="i" class="k-product-skeleton-card">
8
- <div class="skeleton-image"></div>
9
- <div class="skeleton-details">
10
- <div class="skeleton-line skeleton-title"></div>
11
- <div class="skeleton-line skeleton-price"></div>
12
- </div>
13
- </div>
14
- </div>
15
- </slot>
16
- </div>
1
+ <template lang="pug">
2
+ .k-product-list(:class="{ 'is-grid': isGrid, 'is-list': !isGrid, 'is-virtual-scroll': props.virtualScroll }" :style="containerVars")
3
+ //- Loading state
4
+ .loading-container(v-if="loading")
5
+ slot(name="loading" :loading="loading")
6
+ .list-container
7
+ .k-product-skeleton-card(v-for="i in skeletonCount" :key="i")
8
+ .skeleton-image
9
+ .skeleton-details
10
+ .skeleton-line.skeleton-title
11
+ .skeleton-line.skeleton-price
17
12
 
18
- <!-- Empty state -->
19
- <div v-else-if="pageRows.length === 0" class="empty-container">
20
- <slot name="empty">
21
- <div class="k-product-empty-state">
22
- <p class="empty-message">{{ props.emptyText || t("No products found") }}</p>
23
- </div>
24
- </slot>
25
- </div>
13
+ //- Empty state
14
+ .empty-container(v-else-if="isEmpty")
15
+ slot(name="empty")
16
+ .k-product-empty-state
17
+ p.empty-message {{ props.emptyText || t('No products found') }}
26
18
 
27
- <!-- If there's a custom template provided via default slot -->
28
- <template v-else-if="$slots.default">
29
- <div class="list-container">
30
- <slot v-for="(item, index) in pageRows" :item="item" :index="index"></slot>
31
- </div>
32
- </template>
19
+ //- Virtual scrolling mode
20
+ .k-product-virtual-container(
21
+ v-else-if="props.virtualScroll"
22
+ ref="containerRef"
23
+ tabindex="0"
24
+ role="feed"
25
+ aria-busy="false"
26
+ :style="containerCssVars"
27
+ @scroll.passive="onScroll"
28
+ @keydown.down.prevent="onKeyDownDown"
29
+ @keydown.up.prevent="onKeyDownUp"
30
+ )
31
+ .k-virtual-spacer.is-top(aria-hidden="true")
32
+ .list-container
33
+ template(v-if="$slots.default")
34
+ slot(v-for="(item, index) in displayProducts" :item="item" :index="startIndex + index")
35
+ template(v-else)
36
+ KProductListItem(
37
+ v-for="(item, index) in displayProducts"
38
+ :key="item.id || (startIndex + index)"
39
+ :item="item"
40
+ :row-actions="rowActions"
41
+ :locale="props.locale"
42
+ )
43
+ template(v-for="(_, slot) in $slots" #[slot]="scope")
44
+ slot(:name="slot" v-bind="scope")
45
+ .k-virtual-spacer.is-bottom(aria-hidden="true")
33
46
 
34
- <!-- Otherwise, use the default KProductListItem -->
35
- <template v-else>
36
- <div class="list-container">
37
- <KProductListItem
38
- v-for="(item, index) in pageRows"
47
+ //- Traditional paginated mode
48
+ template(v-else)
49
+ .list-container(v-if="$slots.default")
50
+ slot(v-for="(item, index) in pageRows" :item="item" :index="index")
51
+ .list-container(v-else)
52
+ KProductListItem(
53
+ v-for="(item, index) in pageRows"
39
54
  :key="item.id || index"
40
55
  :item="item"
41
56
  :row-actions="rowActions"
42
57
  :locale="props.locale"
43
- >
44
- <template v-for="(_, slot) in $slots" #[slot]="scope">
45
- <slot :name="slot" v-bind="scope"></slot>
46
- </template>
47
- </KProductListItem>
48
- </div>
49
- </template>
58
+ )
59
+ template(v-for="(_, slot) in $slots" #[slot]="scope")
60
+ slot(:name="slot" v-bind="scope")
50
61
 
51
- <div class="footer" v-if="!loading && pageRows.length > 0">
52
- <KPagination class="pagination" :dataProvider="props.dataProvider" />
53
- </div>
54
- </div>
62
+ .footer(v-if="!loading && !isEmpty && !props.virtualScroll")
63
+ KPagination.pagination(
64
+ :dataProvider="props.dataProvider"
65
+ @update:currentPage="emit('update:currentPage', $event)"
66
+ @update:pageSize="emit('update:pageSize', $event)"
67
+ )
55
68
  </template>
56
69
 
57
70
  <script setup>
58
71
  import { computed } from "vue";
59
72
  import { ADataProvider } from "@katlux/providers";
60
73
  import { useKatluxI18n } from "#toolkit/composables/useKatluxI18n";
74
+ import { useVirtualScroll } from "#toolkit/composables/useVirtualScroll";
61
75
  import { useKProductListLogic } from "./KProductList.logic";
62
76
  const { t } = useKatluxI18n();
77
+ const emit = defineEmits(["update:currentPage", "update:pageSize", "page-change"]);
63
78
  const props = defineProps({
64
79
  dataProvider: { type: Object, required: true },
65
80
  layout: { type: String, required: false, default: "grid" },
@@ -67,14 +82,83 @@ const props = defineProps({
67
82
  gridColumns: { type: [Number, String], required: false, default: 4 },
68
83
  gap: { type: String, required: false, default: "1rem" },
69
84
  locale: { type: String, required: false, default: void 0 },
70
- emptyText: { type: String, required: false, default: void 0 }
85
+ emptyText: { type: String, required: false, default: void 0 },
86
+ currentPage: { type: Number, required: false, default: void 0 },
87
+ pageSize: { type: Number, required: false, default: void 0 },
88
+ virtualScroll: { type: Boolean, required: false, default: false },
89
+ itemHeight: { type: Number, required: false, default: void 0 },
90
+ viewportHeight: { type: Number, required: false, default: 600 },
91
+ buffer: { type: Number, required: false, default: 2 }
71
92
  });
72
- const { isGrid, pageRows, loading } = useKProductListLogic(props);
93
+ const { isGrid, pageRows, loading } = useKProductListLogic(props, emit);
73
94
  const skeletonCount = computed(() => Number(props.gridColumns) || 4);
74
95
  const containerVars = computed(() => ({
75
96
  "--k-product-grid-cols": String(props.gridColumns),
76
97
  "--k-product-gap": props.gap
77
98
  }));
99
+ const productItems = computed(() => {
100
+ const currentRows = pageRows.value;
101
+ const dp = props.dataProvider;
102
+ if (props.virtualScroll) {
103
+ if (Array.isArray(dp?.allData?.value)) return dp.allData.value;
104
+ if (Array.isArray(dp?.allData)) return dp.allData;
105
+ if (Array.isArray(dp?.data)) return dp.data;
106
+ }
107
+ return currentRows;
108
+ });
109
+ const columnsCount = computed(() => isGrid.value ? Number(props.gridColumns) || 4 : 1);
110
+ const effectiveItemHeight = computed(() => {
111
+ if (typeof props.itemHeight === "number" && props.itemHeight > 0) return props.itemHeight;
112
+ return isGrid.value ? 350 : 140;
113
+ });
114
+ const {
115
+ visibleItems,
116
+ startIndex,
117
+ endIndex,
118
+ topSpacerHeight,
119
+ bottomSpacerHeight,
120
+ containerRef,
121
+ onScroll,
122
+ scrollToIndex,
123
+ containerCssVars
124
+ } = useVirtualScroll({
125
+ items: productItems,
126
+ itemHeight: effectiveItemHeight,
127
+ containerHeight: props.viewportHeight || 600,
128
+ columns: columnsCount,
129
+ buffer: props.buffer ?? 2,
130
+ virtualScroll: computed(() => !!props.virtualScroll),
131
+ onThresholdReach: () => {
132
+ const dp = props.dataProvider;
133
+ if (typeof dp?.loadMore === "function") {
134
+ dp.loadMore();
135
+ } else if (typeof dp?.nextPage === "function" && dp.hasMore) {
136
+ dp.nextPage();
137
+ }
138
+ }
139
+ });
140
+ const displayProducts = computed(() => {
141
+ if (props.virtualScroll) {
142
+ return visibleItems.value;
143
+ }
144
+ return pageRows.value;
145
+ });
146
+ const isEmpty = computed(() => {
147
+ if (props.virtualScroll) {
148
+ return productItems.value.length === 0;
149
+ }
150
+ return pageRows.value.length === 0;
151
+ });
152
+ const onKeyDownDown = () => {
153
+ if (containerRef.value) {
154
+ containerRef.value.scrollTop += effectiveItemHeight.value;
155
+ }
156
+ };
157
+ const onKeyDownUp = () => {
158
+ if (containerRef.value) {
159
+ containerRef.value.scrollTop = Math.max(0, containerRef.value.scrollTop - effectiveItemHeight.value);
160
+ }
161
+ };
78
162
  </script>
79
163
 
80
164
  <style scoped>
@@ -94,6 +178,26 @@ const containerVars = computed(() => ({
94
178
  flex-direction: column;
95
179
  gap: var(--k-product-gap, 1rem);
96
180
  }
181
+ .k-product-list .k-product-virtual-container {
182
+ overflow-y: auto;
183
+ max-height: var(--virtual-container-height, 600px);
184
+ position: relative;
185
+ width: 100%;
186
+ }
187
+ .k-product-list .k-product-virtual-container:focus-visible {
188
+ outline: 2px solid var(--primary-color, #3b82f6);
189
+ outline-offset: -2px;
190
+ }
191
+ .k-product-list .k-product-virtual-container .k-virtual-spacer {
192
+ width: 100%;
193
+ flex-shrink: 0;
194
+ }
195
+ .k-product-list .k-product-virtual-container .k-virtual-spacer.is-top {
196
+ height: var(--virtual-spacer-top, 0px);
197
+ }
198
+ .k-product-list .k-product-virtual-container .k-virtual-spacer.is-bottom {
199
+ height: var(--virtual-spacer-bottom, 0px);
200
+ }
97
201
  .k-product-list .k-product-empty-state {
98
202
  display: flex;
99
203
  align-items: center;
@@ -1,4 +1,4 @@
1
- import type { KProductItemData, KProductRowAction } from '@katlux/providers';
1
+ import type { KProductRowAction } from '@katlux/providers';
2
2
  import { ADataProvider } from '@katlux/providers';
3
3
  type __VLS_Props = {
4
4
  dataProvider: ADataProvider;
@@ -8,35 +8,34 @@ type __VLS_Props = {
8
8
  gap?: string;
9
9
  locale?: string;
10
10
  emptyText?: string;
11
+ currentPage?: number;
12
+ pageSize?: number;
13
+ virtualScroll?: boolean;
14
+ itemHeight?: number;
15
+ viewportHeight?: number;
16
+ buffer?: number;
11
17
  };
12
- declare var __VLS_1: {
13
- loading: true;
14
- }, __VLS_3: {}, __VLS_5: {
15
- item: KProductItemData;
16
- index: number;
17
- }, __VLS_15: string, __VLS_16: any;
18
- type __VLS_Slots = {} & {
19
- [K in NonNullable<typeof __VLS_15>]?: (props: typeof __VLS_16) => any;
20
- } & {
21
- loading?: (props: typeof __VLS_1) => any;
22
- } & {
23
- empty?: (props: typeof __VLS_3) => any;
24
- } & {
25
- default?: (props: typeof __VLS_5) => any;
26
- };
27
- declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
28
- rowActions: KProductRowAction[];
29
- locale: string;
18
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
19
+ "update:currentPage": (page: number) => any;
20
+ "update:pageSize": (size: number) => any;
21
+ "page-change": (page: number) => any;
22
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
23
+ "onUpdate:currentPage"?: ((page: number) => any) | undefined;
24
+ "onUpdate:pageSize"?: ((size: number) => any) | undefined;
25
+ "onPage-change"?: ((page: number) => any) | undefined;
26
+ }>, {
27
+ currentPage: number;
28
+ pageSize: number;
30
29
  layout: "grid" | "list";
30
+ rowActions: KProductRowAction[];
31
31
  gridColumns: number | string;
32
32
  gap: string;
33
+ locale: string;
33
34
  emptyText: string;
35
+ virtualScroll: boolean;
36
+ itemHeight: number;
37
+ viewportHeight: number;
38
+ buffer: number;
34
39
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
35
- declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
36
40
  declare const _default: typeof __VLS_export;
37
41
  export default _default;
38
- type __VLS_WithSlots<T, S> = T & {
39
- new (): {
40
- $slots: S;
41
- };
42
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@katlux/block-ecommerce",
3
- "version": "0.1.1-alpha.94",
3
+ "version": "0.1.1-alpha.96",
4
4
  "description": "Pre-built eCommerce block components for Katlux toolkit",
5
5
  "author": "Katlux",
6
6
  "license": "Apache-2.0",
@@ -23,8 +23,8 @@
23
23
  "./components/*": "./dist/runtime/components/*"
24
24
  },
25
25
  "dependencies": {
26
- "@katlux/providers": "^0.1.1-alpha.100",
27
- "@katlux/toolkit": "^0.1.1-alpha.111",
26
+ "@katlux/providers": "^0.1.1-alpha.105",
27
+ "@katlux/toolkit": "^0.1.1-alpha.119",
28
28
  "@nuxt/kit": "^3.12.0",
29
29
  "pug": "^3.0.0",
30
30
  "sass": "^1.80.0"