@katlux/block-ecommerce 0.1.0-beta.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.
- package/build.config.ts +4 -0
- package/package.json +31 -0
- package/src/module.ts +25 -0
- package/src/runtime/components/KProductList/KProductList.logic.ts +64 -0
- package/src/runtime/components/KProductList/KProductList.vue +70 -0
- package/src/runtime/components/KProductListItem/KProductListItem.logic.ts +36 -0
- package/src/runtime/components/KProductListItem/KProductListItem.vue +129 -0
package/build.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@katlux/block-ecommerce",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Pre-built eCommerce block components for Katlux toolkit",
|
|
5
|
+
"author": "Katlux",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/module.mjs",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "nuxt-module-build build",
|
|
13
|
+
"dev": "nuxt-module-build build --stub"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/types.d.ts",
|
|
18
|
+
"import": "./dist/module.mjs",
|
|
19
|
+
"require": "./dist/module.cjs"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json",
|
|
22
|
+
"./components/*": "./dist/runtime/components/*"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@katlux/providers": "*",
|
|
26
|
+
"@katlux/toolkit": "*",
|
|
27
|
+
"@nuxt/kit": "^3.20.1",
|
|
28
|
+
"pug": "^3.0.0",
|
|
29
|
+
"sass": "^1.80.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/module.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addComponentsDir } from '@nuxt/kit'
|
|
2
|
+
|
|
3
|
+
export interface ModuleOptions { }
|
|
4
|
+
|
|
5
|
+
export default defineNuxtModule<ModuleOptions>({
|
|
6
|
+
meta: {
|
|
7
|
+
name: '@katlux/block-ecommerce',
|
|
8
|
+
configKey: 'katluxEcommerceBlocks',
|
|
9
|
+
compatibility: {
|
|
10
|
+
nuxt: '^3.0.0'
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
defaults: {},
|
|
14
|
+
async setup(options, nuxt) {
|
|
15
|
+
const resolver = createResolver(import.meta.url)
|
|
16
|
+
|
|
17
|
+
// Add components directory
|
|
18
|
+
addComponentsDir({
|
|
19
|
+
path: resolver.resolve('./runtime/components'),
|
|
20
|
+
pathPrefix: false,
|
|
21
|
+
prefix: '',
|
|
22
|
+
global: true
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ADataProvider, KProductItemData, KProductRowAction } from '@katlux/providers'
|
|
2
|
+
import { computed, type PropType } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface KProductListProps {
|
|
5
|
+
dataProvider: ADataProvider
|
|
6
|
+
layout?: 'grid' | 'list'
|
|
7
|
+
rowActions?: KProductRowAction[]
|
|
8
|
+
gridColumns?: number | string
|
|
9
|
+
gap?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const KProductListDefaultProps = {
|
|
13
|
+
dataProvider: {
|
|
14
|
+
type: Object as PropType<ADataProvider>,
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
layout: {
|
|
18
|
+
type: String as PropType<'grid' | 'list'>,
|
|
19
|
+
default: 'grid',
|
|
20
|
+
},
|
|
21
|
+
rowActions: {
|
|
22
|
+
type: Array as PropType<KProductRowAction[]>,
|
|
23
|
+
default: () => [],
|
|
24
|
+
},
|
|
25
|
+
gridColumns: {
|
|
26
|
+
type: [Number, String],
|
|
27
|
+
default: 4,
|
|
28
|
+
},
|
|
29
|
+
gap: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: '1rem',
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useKProductListLogic(props: KProductListProps) {
|
|
36
|
+
const isGrid = computed(() => props.layout !== 'list')
|
|
37
|
+
|
|
38
|
+
const gridStyles = computed(() => {
|
|
39
|
+
if (!isGrid.value) return {}
|
|
40
|
+
|
|
41
|
+
const cols = props.gridColumns
|
|
42
|
+
return {
|
|
43
|
+
display: 'grid',
|
|
44
|
+
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
45
|
+
gap: props.gap
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const listStyles = computed(() => {
|
|
50
|
+
if (isGrid.value) return {}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
display: 'flex',
|
|
54
|
+
flexDirection: 'column' as const,
|
|
55
|
+
gap: props.gap
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
isGrid,
|
|
61
|
+
gridStyles,
|
|
62
|
+
listStyles
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="k-product-list" :class="{ 'is-grid': isGrid, 'is-list': !isGrid }">
|
|
3
|
+
<!-- If there's a custom template provided via default slot -->
|
|
4
|
+
<template v-if="$slots.default">
|
|
5
|
+
<div class="list-container" :style="isGrid ? gridStyles : listStyles">
|
|
6
|
+
<slot v-for="(item, index) in props.dataProvider.pageData.value" :item="item" :index="index"></slot>
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<!-- Otherwise, use the default KProductListItem -->
|
|
11
|
+
<template v-else>
|
|
12
|
+
<div class="list-container" :style="isGrid ? gridStyles : listStyles">
|
|
13
|
+
<KProductListItem
|
|
14
|
+
v-for="(item, index) in props.dataProvider.pageData.value"
|
|
15
|
+
:key="item.id || index"
|
|
16
|
+
:item="item"
|
|
17
|
+
:row-actions="rowActions"
|
|
18
|
+
>
|
|
19
|
+
<template v-for="(_, slot) in $slots" #[slot]="scope">
|
|
20
|
+
<slot :name="slot" v-bind="scope"></slot>
|
|
21
|
+
</template>
|
|
22
|
+
</KProductListItem>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<div class="footer">
|
|
27
|
+
<KPagination class="pagination" :dataProvider="props.dataProvider" />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script lang="ts" setup>
|
|
33
|
+
import type { KProductItemData, KProductRowAction } from '@katlux/providers'
|
|
34
|
+
import { ADataProvider } from '@katlux/providers'
|
|
35
|
+
import { useKProductListLogic } from './KProductList.logic'
|
|
36
|
+
|
|
37
|
+
const props = withDefaults(defineProps<{
|
|
38
|
+
dataProvider: ADataProvider
|
|
39
|
+
layout?: 'grid' | 'list'
|
|
40
|
+
rowActions?: KProductRowAction[]
|
|
41
|
+
gridColumns?: number | string
|
|
42
|
+
gap?: string
|
|
43
|
+
}>(), {
|
|
44
|
+
layout: 'grid',
|
|
45
|
+
rowActions: () => [],
|
|
46
|
+
gridColumns: 4,
|
|
47
|
+
gap: '1rem'
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const { isGrid, gridStyles, listStyles } = useKProductListLogic(props as any)
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<style lang="scss" scoped>
|
|
54
|
+
.k-product-list {
|
|
55
|
+
width: 100%;
|
|
56
|
+
|
|
57
|
+
.list-container {
|
|
58
|
+
width: 100%;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.footer {
|
|
62
|
+
display: flex;
|
|
63
|
+
justify-content: flex-end;
|
|
64
|
+
margin-top: var(--gap-lg, 24px);
|
|
65
|
+
.pagination {
|
|
66
|
+
flex: 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { computed, type PropType } from 'vue'
|
|
2
|
+
import type { KProductItemData, KProductRowAction } from '@katlux/providers'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export interface KProductListItemProps {
|
|
6
|
+
item: KProductItemData
|
|
7
|
+
rowActions?: KProductRowAction[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const KProductListItemDefaultProps = {
|
|
11
|
+
item: {
|
|
12
|
+
type: Object as PropType<KProductItemData>,
|
|
13
|
+
required: true as const,
|
|
14
|
+
},
|
|
15
|
+
rowActions: {
|
|
16
|
+
type: Array as PropType<KProductRowAction[]>,
|
|
17
|
+
default: () => [],
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useKProductListItemLogic(props: KProductListItemProps) {
|
|
22
|
+
const hasActions = computed(() => props.rowActions && props.rowActions.length > 0)
|
|
23
|
+
|
|
24
|
+
const formattedPrice = computed(() => {
|
|
25
|
+
if (props.item.price === undefined) return ''
|
|
26
|
+
return new Intl.NumberFormat('en-US', {
|
|
27
|
+
style: 'currency',
|
|
28
|
+
currency: props.item.currency || 'USD'
|
|
29
|
+
}).format(props.item.price)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
hasActions,
|
|
34
|
+
formattedPrice
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.k-product-list-item
|
|
3
|
+
.item-image(v-if="item.image")
|
|
4
|
+
img(:src="item.image" :alt="item.title")
|
|
5
|
+
.item-image-placeholder(v-else)
|
|
6
|
+
KIcon(name="tabler:photo" class="placeholder-icon")
|
|
7
|
+
.item-details
|
|
8
|
+
h3.item-title {{ item.title }}
|
|
9
|
+
p.item-description(v-if="item.description") {{ item.description }}
|
|
10
|
+
.item-price(v-if="formattedPrice") {{ formattedPrice }}
|
|
11
|
+
.item-actions(v-if="hasActions")
|
|
12
|
+
KButton(
|
|
13
|
+
v-for="(action, index) in rowActions"
|
|
14
|
+
:key="index"
|
|
15
|
+
:class="action.color || 'primary'"
|
|
16
|
+
@click="action.action(item)"
|
|
17
|
+
)
|
|
18
|
+
template(v-if="action.icon" #icon)
|
|
19
|
+
KIcon(:name="action.icon")
|
|
20
|
+
| {{ action.label }}
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script lang="ts" setup>
|
|
24
|
+
import type { KProductItemData, KProductRowAction } from '@katlux/providers'
|
|
25
|
+
|
|
26
|
+
const props = defineProps<{
|
|
27
|
+
item: KProductItemData
|
|
28
|
+
rowActions?: KProductRowAction[]
|
|
29
|
+
}>()
|
|
30
|
+
|
|
31
|
+
import { useKProductListItemLogic } from './KProductListItem.logic'
|
|
32
|
+
const { hasActions, formattedPrice } = useKProductListItemLogic(props)
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<style lang="scss" scoped>
|
|
36
|
+
.k-product-list-item {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
border: 1px solid var(--border-color-light, #e9ecef);
|
|
40
|
+
border-radius: var(--border-radius-md, 8px);
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
background: var(--bg-color-surface, #ffffff);
|
|
43
|
+
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
|
44
|
+
height: 100%;
|
|
45
|
+
|
|
46
|
+
&:hover {
|
|
47
|
+
box-shadow: var(--shadow-md, 0 4px 12px rgba(0,0,0,0.08));
|
|
48
|
+
transform: translateY(-2px);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.item-image, .item-image-placeholder {
|
|
52
|
+
width: 100%;
|
|
53
|
+
aspect-ratio: 1;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
background: var(--bg-color-muted, #f8f9fa);
|
|
59
|
+
border-bottom: 1px solid var(--border-color-light, #e9ecef);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.item-image img {
|
|
63
|
+
width: 100%;
|
|
64
|
+
height: 100%;
|
|
65
|
+
object-fit: cover;
|
|
66
|
+
transition: transform 0.3s ease;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
&:hover .item-image img {
|
|
70
|
+
transform: scale(1.05);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.item-image-placeholder {
|
|
74
|
+
.placeholder-icon {
|
|
75
|
+
font-size: 3rem;
|
|
76
|
+
color: var(--text-color-muted, #adb5bd);
|
|
77
|
+
opacity: 0.5;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.item-details {
|
|
82
|
+
padding: var(--gap-md, 16px);
|
|
83
|
+
flex-grow: 1;
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.item-title {
|
|
89
|
+
margin: 0 0 var(--gap-xs, 4px) 0;
|
|
90
|
+
font-size: var(--font-size-lg, 1.1rem);
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
color: var(--text-color-primary, #212529);
|
|
93
|
+
line-height: 1.3;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.item-description {
|
|
97
|
+
margin: 0 0 var(--gap-sm, 8px) 0;
|
|
98
|
+
font-size: var(--font-size-sm, 0.9rem);
|
|
99
|
+
color: var(--text-color-secondary, #6c757d);
|
|
100
|
+
line-height: 1.4;
|
|
101
|
+
flex-grow: 1;
|
|
102
|
+
display: -webkit-box;
|
|
103
|
+
-webkit-line-clamp: 2;
|
|
104
|
+
-webkit-box-orient: vertical;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.item-price {
|
|
109
|
+
font-size: var(--font-size-xl, 1.25rem);
|
|
110
|
+
font-weight: 700;
|
|
111
|
+
color: var(--color-primary, #0d6efd);
|
|
112
|
+
margin-top: auto;
|
|
113
|
+
padding-top: var(--gap-sm, 8px);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.item-actions {
|
|
117
|
+
padding: var(--gap-md, 16px);
|
|
118
|
+
padding-top: 0;
|
|
119
|
+
display: flex;
|
|
120
|
+
gap: var(--gap-sm, 8px);
|
|
121
|
+
flex-wrap: wrap;
|
|
122
|
+
|
|
123
|
+
:deep(.KButton) {
|
|
124
|
+
flex: 1;
|
|
125
|
+
min-width: 100px;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
</style>
|