@viur/shop-components 0.0.1-dev.58 → 0.0.1-dev.60

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 (86) hide show
  1. package/.editorconfig +16 -0
  2. package/.github/workflows/npm-publish.yml +42 -0
  3. package/.gitmodules +3 -0
  4. package/LICENSE +21 -0
  5. package/README.md +13 -2
  6. package/package.json +19 -32
  7. package/src/components/ShopCart.vue +512 -0
  8. package/src/components/ShopOrderComplete.vue +73 -0
  9. package/src/components/ShopOrderConfirm.vue +291 -0
  10. package/src/components/ShopOrderStepper.vue +264 -0
  11. package/src/components/ShopUserData.vue +232 -0
  12. package/src/components/cart/CartLeaf.vue +277 -0
  13. package/src/components/cart/CartLeafModel.vue +304 -0
  14. package/src/components/cart/CartNode.vue +25 -0
  15. package/src/components/cart/CartTree.vue +54 -0
  16. package/src/components/cart/CartTreeWrapper.vue +73 -0
  17. package/src/components/cart/CartView.vue +723 -0
  18. package/src/components/cart/Discount.vue +91 -0
  19. package/src/components/lib/utils.js +0 -0
  20. package/src/components/order/OrderSidebar.vue +102 -0
  21. package/src/components/order/category/CategoryList.vue +83 -0
  22. package/src/components/order/category/CategoryView.vue +143 -0
  23. package/src/components/order/information/adress/ShippingAdress.vue +143 -0
  24. package/src/components/order/item/ItemCard.vue +168 -0
  25. package/src/components/order/item/ItemView.vue +232 -0
  26. package/src/components/order/process/ConfirmView.vue +312 -0
  27. package/src/components/order/process/ExampleUsage.vue +113 -0
  28. package/src/components/order/process/OrderTabHeader.vue +16 -0
  29. package/src/components/order/process/SelectPaymentProvider.vue +62 -0
  30. package/src/components/order/process/Shipping.vue +46 -0
  31. package/src/components/ui/ShopSummary.vue +145 -0
  32. package/src/components/ui/generic/ArticleList.vue +222 -0
  33. package/src/components/ui/generic/ExamplePagination.vue +236 -0
  34. package/src/components/ui/generic/ShopPriceFormatter.vue +41 -0
  35. package/src/components/ui/generic/alerts/ShopAlert.vue +19 -0
  36. package/src/components/ui/generic/makeData.js +39 -0
  37. package/src/components/ui/stepper/StepperItem.vue +39 -0
  38. package/src/components/ui/stepper/StepperTab.vue +133 -0
  39. package/src/components/ui/stepper/StepperTrigger.vue +35 -0
  40. package/src/components/ui/userdata/AddForm.vue +125 -0
  41. package/src/components/ui/userdata/AddressBox.vue +117 -0
  42. package/src/components/ui/userdata/BaseLayout.vue +94 -0
  43. package/src/components/ui/userdata/CustomBooleanBone.vue +58 -0
  44. package/src/components/ui/userdata/CustomSelectBone.vue +91 -0
  45. package/src/components/ui/userdata/CustomStringBone.vue +71 -0
  46. package/src/components/ui/userdata/DefaultLayout.vue +126 -0
  47. package/src/components/ui/userdata/SelectAddress.vue +21 -0
  48. package/src/components/ui/userdata/multi/ActionBar.vue +38 -0
  49. package/src/components/ui/userdata/multi/CartSelection.vue +42 -0
  50. package/src/main.js +50 -0
  51. package/src/router/index.js +103 -0
  52. package/src/stores/cart.js +336 -0
  53. package/src/style/ignite/.editorconfig +20 -0
  54. package/src/style/ignite/.github/workflows/ignite.yml +64 -0
  55. package/src/style/ignite/.github/workflows/node.yml +30 -0
  56. package/src/style/ignite/.postcssrc.cjs +25 -0
  57. package/src/style/ignite/CHANGELOG.md +244 -0
  58. package/src/style/ignite/LICENSE +21 -0
  59. package/src/style/ignite/README.md +92 -0
  60. package/src/style/ignite/dist/ignite.css +2019 -0
  61. package/src/style/ignite/dist/ignite.min.css +4 -0
  62. package/src/style/ignite/foundation/basic.css +371 -0
  63. package/src/style/ignite/foundation/color.css +323 -0
  64. package/src/style/ignite/foundation/config.css +188 -0
  65. package/src/style/ignite/foundation/grid.css +78 -0
  66. package/src/style/ignite/foundation/mediaqueries.css +71 -0
  67. package/src/style/ignite/foundation/reset.css +261 -0
  68. package/src/style/ignite/ignite.css +29 -0
  69. package/src/style/ignite/ignite.css.map +1 -0
  70. package/src/style/ignite/package-lock.json +5530 -0
  71. package/src/style/ignite/package.json +58 -0
  72. package/src/style/ignite/shoelace.css +19 -0
  73. package/src/style/ignite/themes/dark.css +12 -0
  74. package/src/style/ignite/themes/light.css +11 -0
  75. package/src/style/ignite/utilities/shoelace.css +537 -0
  76. package/src/style/ignite/utilities/utilities.css +24 -0
  77. package/src/views/ViewMissing.vue +20 -0
  78. package/vite.config.js +53 -0
  79. package/dist/CategoryView-Z-tCFNv1.mjs +0 -60
  80. package/dist/ItemCard-CVfih_bz.mjs +0 -64
  81. package/dist/ItemView-cjeQBSTR.mjs +0 -1848
  82. package/dist/ItemView.css +0 -1
  83. package/dist/main-CZfMYx-A.mjs +0 -3167
  84. package/dist/main.css +0 -1
  85. package/dist/viur-shop-components.es.js +0 -10
  86. package/dist/viur-shop-components.umd.js +0 -417
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <div>
3
+
4
+ <span>Haben Sie noch ein Gutschein?</span><br>
5
+ <span v-if="!cartStore.state.basketRootNode.discount">Es befindet sich noch kein Gutschein im Warenkorb.</span>
6
+ <sl-button-group>
7
+ <sl-input placeholder="Rabatt Code" ref="codeInput">
8
+
9
+ </sl-input>
10
+ <sl-button @click="addDiscountCode">Einlösen</sl-button>
11
+ </sl-button-group>
12
+ <sl-alert ref="errorMessageContainer">
13
+ <sl-icon slot="icon" name="info-circle"></sl-icon>
14
+ {{ state.errorMessage }}
15
+ </sl-alert>
16
+ </div>
17
+ <div>
18
+ <div v-if="cartStore.state.basketRootNode.discount">
19
+ <!--Todo bessere texte und translations??-->
20
+ <div v-if="cartStore.state.basketRootNode.discount.dest.discount_type==='absolute'">
21
+ <span>
22
+ Sie haben einen Rabattcode im Wert von {{ cartStore.state.basketRootNode.discount.dest.absolute }} € eingegeben
23
+ </span>
24
+ <sl-icon-button name="x-lg" label="Löschen" @click="removeDiscountCode"></sl-icon-button>
25
+ </div>
26
+ <div v-if="cartStore.state.basketRootNode.discount.dest.discount_type==='percentage'">
27
+ <span>
28
+ Sie haben einen Rabattcode im Wert von {{ cartStore.state.basketRootNode.discount.dest.percentage }} % eingegeben
29
+ </span>
30
+ <sl-icon-button name="x-lg" label="Löschen" @click="removeDiscountCode"></sl-icon-button>
31
+ </div>
32
+
33
+ </div>
34
+ </div>
35
+ <sl-spinner v-show="state.isFetching"></sl-spinner>
36
+
37
+ </template>
38
+ <script setup>
39
+ import {useCartStore} from "../../stores/cart";
40
+ import {computed, reactive, ref} from "vue";
41
+
42
+ const cartStore = useCartStore();
43
+ const codeInput = ref(null);
44
+ const errorMessageContainer = ref(null);
45
+ const state = reactive({
46
+ errorMessage: "",
47
+ isFetching: false,
48
+ });
49
+
50
+ async function addDiscountCode() {
51
+ errorMessageContainer.value.hide();
52
+ const discountCode = codeInput.value.value;
53
+ if (!discountCode) {
54
+ errorMessageContainer.value.show();
55
+ state.errorMessage = "Es wurde kein Rabattcode eingegeben";
56
+ return
57
+ }
58
+ state.isFetching = true;
59
+ console.log("festch", state.isFetching)
60
+ cartStore.addDiscount(discountCode).then((res) => {
61
+ cartStore.init();//TODO muss man alles neuladen ??
62
+ state.isFetching = false;
63
+
64
+ }).catch((e) => {
65
+ console.error("Cant add key");
66
+ state.isFetching = false;
67
+ })
68
+
69
+ }
70
+
71
+ async function removeDiscountCode() {
72
+
73
+ errorMessageContainer.value.hide();
74
+ state.isFetching = true;
75
+ console.log("code ", cartStore.state.basketRootNode.discount.dest.key)
76
+ cartStore.removeDiscount(cartStore.state.basketRootNode.discount.dest.key).then((res) => {
77
+ cartStore.init();//TODO muss man alles neuladen ??
78
+ state.isFetching = false;
79
+
80
+ }).catch((e) => {
81
+ console.error("Cant remove key");
82
+ state.isFetching = false;
83
+ })
84
+
85
+ }
86
+ </script>
87
+
88
+
89
+ <style scoped>
90
+
91
+ </style>
File without changes
@@ -0,0 +1,102 @@
1
+ ,
2
+ <template>
3
+ <div class="viur-shop-sidebar">
4
+ <h2 class="viur-shop-cart-sidebar-headline headline">Zusammenfassung</h2>
5
+ <br />
6
+ <div class="viur-shop-cart-sidebar-info-line">
7
+ <span>Zwischensumme</span>
8
+ 999.99 €
9
+ </div>
10
+ <div class="viur-shop-cart-sidebar-info-line">
11
+ <span>Rabatt</span>
12
+ {{
13
+ cartStore.state.basketRootNode.total -
14
+ cartStore.state.basketRootNode.total_discount_price
15
+ }}
16
+
17
+ </div>
18
+ <div class="viur-shop-cart-sidebar-info-line">
19
+ <span>Versandkosten</span>
20
+ 0 €
21
+ </div>
22
+ <div class="viur-shop-cart-sidebar-info-line total">
23
+ <span>Gesamt:</span>
24
+ <!--{{ cartStore.state.basketRootNode }}-->
25
+
26
+ </div>
27
+ <div class="viur-shop-cart-sidebar-btn-wrap">
28
+
29
+ <!-- TODO: Placement of discount? -->
30
+ <div class="viur-shop-discount-wrap">
31
+ <Discount></Discount>
32
+ </div>
33
+
34
+ <sl-button variant="primary" size="medium"> Jetzt Bestellen</sl-button>
35
+ <sl-button size="medium" variant="info">
36
+ <sl-icon name="paypal" slot="prefix"></sl-icon>
37
+ Paypal
38
+ </sl-button>
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <script setup>
44
+ import { onBeforeMount } from "vue";
45
+ import { useCartStore } from "../../stores/cart.js";
46
+ import Discount from "../cart/Discount.vue";
47
+
48
+ const cartStore = useCartStore();
49
+
50
+ onBeforeMount(() => {
51
+ cartStore.init();
52
+ });
53
+ </script>
54
+
55
+ <style>
56
+ .viur-shop-cart-sidebar-info-line {
57
+ display: flex;
58
+ flex-direction: row;
59
+ flex-wrap: nowrap;
60
+ margin: var(--sl-spacing-2x-small) 0;
61
+
62
+ &.total {
63
+ font-weight: 600;
64
+ border-top: 1px solid var(--sl-color-neutral-300);
65
+ border-bottom: 1px solid var(--sl-color-neutral-300);
66
+ padding: var(--sl-spacing-x-small) 0;
67
+ margin: var(--sl-spacing-small) 0;
68
+ }
69
+
70
+ span {
71
+ margin-right: auto;
72
+ }
73
+ }
74
+
75
+ .viur-shop-cart-sidebar-btn-wrap {
76
+ display: flex;
77
+ flex-direction: column;
78
+ margin-top: var(--sl-spacing-large);
79
+
80
+ sl-button {
81
+ margin-bottom: var(--sl-spacing-x-small);
82
+ }
83
+ }
84
+ .viur-shop-cart-sidebar-headline {
85
+ margin: 0 0 var(--sl-spacing-large) 0;
86
+ }
87
+
88
+ .viur-shop-sidebar {
89
+ display: flex;
90
+ flex-direction: column;
91
+ background-color: var(--shop-sidebar-background);
92
+ padding: var(--sl-spacing-medium);
93
+ overflow: hidden;
94
+ border-radius: var(--sl-border-radius-medium);
95
+
96
+ @media (min-width: 1024px) {
97
+ position: sticky;
98
+ top: 0;
99
+ margin-left: var(--sl-spacing-2x-large);
100
+ }
101
+ }
102
+ </style>
@@ -0,0 +1,83 @@
1
+ <template>
2
+ <RouterLink :to="{ name: 'CategoryView', params: { identifier: 'dk' } }"
3
+ v-for="item in list"
4
+ :key="item"
5
+ class="viur-shop-category-list-item-link"
6
+ >
7
+ <sl-card class="viur-shop-category-list-item">
8
+ <img
9
+ slot="image"
10
+ src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
11
+ alt="A kitten."
12
+ class="viur-shop-category-list-item-img"
13
+ />
14
+ <h2 class="viur-shop-category-list-item-headline"> {{ item }}</h2>
15
+ <div class="viur-shop-category-list-fake-link">Alle anzeigen &raquo;</div>
16
+
17
+ </sl-card>
18
+ </RouterLink>
19
+ </template>
20
+
21
+ <script setup>
22
+ import { onBeforeMount } from "vue";
23
+ import ItemCard from "../item/ItemCard.vue";
24
+ import { useCartStore } from "../../../stores/cart";
25
+
26
+ const cartStore = useCartStore();
27
+ const props = defineProps({
28
+ list: { type: Array, required: true },
29
+ });
30
+
31
+ onBeforeMount(async () => {
32
+ await cartStore.init();
33
+ });
34
+ </script>
35
+
36
+ <style scoped>
37
+
38
+ .viur-shop-category-list-item-link{
39
+ display: flex;
40
+ flex-direction: column;
41
+ width: 100%;
42
+
43
+ &:hover{
44
+ .viur-shop-category-list-fake-link{
45
+ text-decoration: underline;
46
+ }
47
+
48
+ .viur-shop-category-list-item-img{
49
+ transform: scale(1.02);
50
+ }
51
+ }
52
+ }
53
+
54
+ .viur-shop-category-list-item{
55
+ width: 100%;
56
+
57
+ &::part(header){
58
+ padding: var(--sl-spacing-medium) 0;
59
+ }
60
+
61
+ &::part(body){
62
+ padding: var(--sl-spacing-medium) 0;
63
+ }
64
+
65
+ &::part(footer){
66
+ padding: var(--sl-spacing-medium) 0;
67
+ }
68
+ }
69
+
70
+ .viur-shop-category-list-item-headline{
71
+ font-size: 1.4em;
72
+ margin-bottom: var(--sl-spacing-medium);
73
+ color: var(--ignt-basic-color-text);
74
+ }
75
+
76
+ .viur-shop-category-list-item-img{
77
+ aspect-ratio: 1;
78
+ object-fit: cover;
79
+ transition: all ease .3s;
80
+ }
81
+
82
+
83
+ </style>
@@ -0,0 +1,143 @@
1
+ <template>
2
+ <div class="viur-shop-loading-wrap" v-if="state.loading">
3
+ <sl-spinner></sl-spinner>
4
+ </div>
5
+
6
+ <div class="bind" v-else>
7
+ <div class="page-header">
8
+ <h1>{{ pageHeader }}</h1>
9
+ </div>
10
+ <slot name="filter" v-if="filter">
11
+ text-transform text-transform TEST
12
+ </slot>
13
+ <div class="viur-shop-category-view-list">
14
+ <!-- <router-link
15
+ v-for="item in state.skellist"
16
+ :key="item.shop_name"
17
+ :to="{ name: 'itemView', params: { item: item.key } }"
18
+ > -->
19
+
20
+ <ItemCard
21
+ v-for="item in state.skellist"
22
+ :key="item.shop_name"
23
+ :item="item"
24
+ >
25
+ </ItemCard>
26
+ <!-- </router-link> -->
27
+ </div>
28
+
29
+ <sl-button
30
+ @click="loadMore"
31
+ :loading="state.loading"
32
+ :disabled="state.isLastItem"
33
+ class="viur-shop-category-view-more-button"
34
+ >
35
+ Mehr anzeigen
36
+ </sl-button>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup>
41
+ import { onMounted, reactive, computed } from "vue";
42
+ import { useCartStore } from "../../../stores/cart";
43
+ import { useRoute } from "vue-router";
44
+ import { Request, ListRequest } from "@viur/vue-utils";
45
+ // import { ViURShopClient } from "@viur/viur-shop-client";
46
+
47
+ // component imports
48
+ import ItemCard from "../item/ItemCard.vue";
49
+
50
+ const props = defineProps({
51
+ skellist: { type: Array },
52
+ filter: { type: Boolean, default: true },
53
+ pageHeader: { type: String, default: "Artikel Liste" },
54
+ listHandler: { type: Object, required: true },
55
+ });
56
+
57
+ const route = useRoute();
58
+
59
+ const cartStore = useCartStore();
60
+
61
+ const state = reactive({
62
+ skellist: [],
63
+ loading: true,
64
+ currentCursor: "",
65
+ isLastItem: false,
66
+ itemCount: 99,
67
+ itemType: computed(() => route.params.identifier),
68
+ });
69
+
70
+ const categoryList = props.listHandler;
71
+
72
+ function listItems() {
73
+ let params = {
74
+ limit: state.itemCount,
75
+ cursor: state.currentCursor,
76
+ type: state.itemType,
77
+ };
78
+
79
+ state.loading = true;
80
+
81
+ Request.get("/json/variante/list", { dataObj: params }).then(async (resp) => {
82
+ let data = await resp.json();
83
+
84
+ if (data.cursor !== state.currentCursor && !state.isLastItem) {
85
+ state.currentCursor = data.cursor;
86
+ state.skellist.push(...data.skellist);
87
+ state.loading = false;
88
+ } else if (data.cursor === state.currentCursor) {
89
+ state.isLastItem = true;
90
+ state.loading = false;
91
+ }
92
+
93
+ console.log("hier", data);
94
+ });
95
+ }
96
+
97
+ async function loadMore() {
98
+ state.loading = true;
99
+ await categoryList.next();
100
+
101
+ if (state.skellist.length < categoryList.state.skellist.length) {
102
+ state.skellist = categoryList.state.skellist;
103
+ state.loading = false;
104
+ } else {
105
+ state.loading = false;
106
+ state.isLastItem = true;
107
+ }
108
+ }
109
+
110
+ onMounted(async () => {
111
+ await cartStore.init();
112
+ await categoryList.fetch(true);
113
+ state.skellist = categoryList.state.skellist;
114
+ state.loading = false;
115
+
116
+ // await cartStore.init();
117
+ });
118
+ </script>
119
+
120
+ <style scoped>
121
+ .viur-shop-category-view-list {
122
+ display: grid;
123
+ width: 100%;
124
+ grid-gap: var(--sl-spacing-medium);
125
+ grid-template-columns: repeat(4, 1fr);
126
+ }
127
+
128
+ .viur-shop-loading-wrap {
129
+ position: absolute;
130
+ top: 0;
131
+ bottom: 0;
132
+ left: 0;
133
+ right: 0;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+
138
+ sl-spinner {
139
+ font-size: 3.5em;
140
+ --track-width: 4px;
141
+ }
142
+ }
143
+ </style>
@@ -0,0 +1,143 @@
1
+ <template>
2
+ <sl-splinner
3
+ v-if="!cartStore.state.carts[cartStore.state.basket]?.items"
4
+ ></sl-splinner>
5
+ <template v-else>
6
+ <div class="form-wrap">
7
+ <sl-select
8
+ v-if="multiAdress"
9
+ multiple
10
+ clearable
11
+ ref="itemSelection"
12
+ @sl-change="onItemSelect"
13
+ @sl-clear="onItemReset"
14
+ :label="'Lieferadresse für: ' + state.selectedItem"
15
+ class="grid-w-4"
16
+ >
17
+ <sl-option
18
+ v-for="item in cartStore.state.carts[cartStore.state.basket]?.items"
19
+ :value="item.key"
20
+ >
21
+ {{ item.shop_name }}</sl-option
22
+ >
23
+ </sl-select>
24
+ <sl-input
25
+ name="street"
26
+ @sl-change="onInput"
27
+ placeholder="Straße"
28
+ class="grid-w-3"
29
+ :disabled="!state.isItemSelected"
30
+ >
31
+ <label slot="label">Strasse *</label>
32
+ </sl-input>
33
+ <sl-input
34
+ name="street"
35
+ @sl-change="onInput"
36
+ placeholder="Hausnummer"
37
+ type="number"
38
+ :disabled="!state.isItemSelected"
39
+ >
40
+ <label slot="label">Hausnummer *</label>
41
+ </sl-input>
42
+ <sl-input
43
+ name="street"
44
+ @sl-change="onInput"
45
+ placeholder="Postleitzahl"
46
+ type="number"
47
+ class="grid-w-2"
48
+ :disabled="!state.isItemSelected"
49
+ >
50
+ <label slot="label">Postleitzahl *</label>
51
+ </sl-input>
52
+ <sl-input
53
+ name="city"
54
+ @sl-change="onInput"
55
+ placeholder="Stadt"
56
+ class="grid-w-2"
57
+ :disabled="!state.isItemSelected"
58
+ >
59
+ <label slot="label">Stadt*</label>
60
+ </sl-input>
61
+ <sl-input
62
+ name="province"
63
+ @sl-change="onInput"
64
+ placeholder="Bundesland"
65
+ class="grid-w-2"
66
+ :disabled="!state.isItemSelected"
67
+ >
68
+ <label slot="label">Bundesland</label>
69
+ </sl-input>
70
+ </div>
71
+ </template>
72
+ </template>
73
+
74
+ <script setup>
75
+ import { onBeforeMount, reactive, ref } from "vue";
76
+ import { useCartStore } from "../../../../stores/cart";
77
+
78
+ const props = defineProps({
79
+ multiAdress: { type: Boolean, default: false },
80
+ items: { type: Array },
81
+ });
82
+
83
+ const emit = defineEmits(["adressInput", "itemSelection"]);
84
+
85
+ const state = reactive({
86
+ selectedItem: null,
87
+ isItemSelected: false,
88
+ items: {},
89
+ });
90
+
91
+ const itemSelection = ref(null);
92
+ const cartStore = useCartStore();
93
+
94
+ function onInput(e) {
95
+ let key = `${[state.selectedItem]}.${[e.target.name]}`;
96
+ emit("adressInput", {
97
+ [`${[state.selectedItem]}.${[e.target.name]}`]: e.target.value,
98
+ });
99
+ }
100
+
101
+ function onItemSelect(e) {
102
+ console.log(e.target.value);
103
+ if (!e.target.value.length) {
104
+ onItemReset();
105
+ return;
106
+ }
107
+ state.selectedItem = e.target.value;
108
+ state.isItemSelected = true;
109
+ }
110
+
111
+ function onItemReset() {
112
+ console.log("clearing...");
113
+ state.selectedItem = null;
114
+ state.isItemSelected = false;
115
+ }
116
+
117
+ onBeforeMount(() => {
118
+ console.log(props.items);
119
+ });
120
+ </script>
121
+
122
+ <style scoped>
123
+ .form-wrap {
124
+ display: grid;
125
+ grid-template-columns: repeat(4, minmax(0, 1fr));
126
+ gap: 0 var(--sl-spacing-medium);
127
+ margin: var(--sl-spacing-large) 0 var(--sl-spacing-x-large) 0;
128
+ padding-bottom: var(--sl-spacing-medium);
129
+ border-bottom: 1px solid var(--sl-color-neutral-400);
130
+ }
131
+
132
+ .grid-w-2 {
133
+ grid-column: span 2;
134
+ }
135
+
136
+ .grid-w-3 {
137
+ grid-column: span 3;
138
+ }
139
+
140
+ .grid-w-4 {
141
+ grid-column: span 4;
142
+ }
143
+ </style>