@lightspeed/crane 1.2.2 → 1.2.4

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.
@@ -28,7 +28,8 @@ import Account from './component/Account.vue';
28
28
  justify-content: space-between;
29
29
  padding: 5px 10px;
30
30
 
31
- .custom-header__right{
31
+ .custom-header__left,
32
+ .custom-header__right {
32
33
  display: flex;
33
34
  flex: 0 0 auto;
34
35
  gap: 10px;
@@ -40,16 +41,6 @@ import Account from './component/Account.vue';
40
41
  order: 3;
41
42
  text-align: right;
42
43
  }
43
-
44
- .custom-header__left{
45
- display: flex;
46
- flex: 0 0 auto;
47
- gap: 10px;
48
- align-items: center;
49
- justify-content: flex-end;
50
- order: 3;
51
- text-align: left;
52
- }
53
44
  }
54
45
 
55
46
  .header-content {
@@ -0,0 +1,165 @@
1
+ <template>
2
+ <teleport to="body">
3
+ <div
4
+ v-if="categories.length > 0"
5
+ class="categories-dropdown"
6
+ :style="{ left: offsetLeft }"
7
+ @mouseleave="emit('hide')"
8
+ @blur="emit('hide')"
9
+ >
10
+ <ul class="category-list">
11
+ <li
12
+ v-if="categoryId === undefined"
13
+ class="category-item"
14
+ >
15
+ <a
16
+ :href="getCategoryLink('/products')"
17
+ class="category-link"
18
+ >
19
+ <span>View all</span>
20
+ </a>
21
+ </li>
22
+ <li
23
+ v-for="category in categories"
24
+ :key="category.id"
25
+ class="category-item"
26
+ >
27
+ <a
28
+ :href="getCategoryLink(category.urlPath)"
29
+ class="category-link"
30
+ >
31
+ <span>{{ category.name }}</span>
32
+ <span v-if="category.hasChildren" class="arrow">▶</span>
33
+ </a>
34
+ <ul v-if="category.hasChildren" class="subcategory-list">
35
+ <li
36
+ v-for="child in category.children"
37
+ :key="child.id"
38
+ class="subcategory-item"
39
+ >
40
+ <a
41
+ :href="getCategoryLink(child.urlPath)"
42
+ class="subcategory-link"
43
+ >
44
+ {{ child.name }}
45
+ </a>
46
+ </li>
47
+ </ul>
48
+ </li>
49
+ </ul>
50
+ </div>
51
+ </teleport>
52
+ </template>
53
+
54
+ <script setup lang="ts">
55
+ import { computed } from 'vue';
56
+ import { useVueBaseProps } from '@lightspeed/crane';
57
+ import { Design } from '../type.ts';
58
+
59
+ interface Props {
60
+ element?: HTMLElement;
61
+ categoryId?: number;
62
+ }
63
+
64
+ const props = defineProps<Props>();
65
+ const emit = defineEmits(['hide']);
66
+
67
+ const baseProps = useVueBaseProps<Props, Design>();
68
+
69
+ const isPreviewMode = computed(() => Boolean(baseProps.site?.value?.isPreviewMode));
70
+ const categoryTree = computed(() => baseProps.category?.value?.categoryTree ?? []);
71
+ const categories = computed(() => categoryTree.value
72
+ .filter(filterUniqueCategories)
73
+ .filter((category) => filterByCategoryId(category, props.categoryId))
74
+ .map((category) => ({
75
+ ...category,
76
+ hasChildren: category.children?.length > 0,
77
+ }))
78
+ );
79
+ const offsetLeft = computed(() => `${props.element?.offsetLeft ?? 0}px`);
80
+
81
+ function filterUniqueCategories(category: CategoryTree, index: number, categoryTree: CategoryTree[]) {
82
+ return categoryTree.findIndex((c) => c.id === category.id) === index;
83
+ }
84
+
85
+ function filterByCategoryId(category: CategoryTree, categoryId?: number) {
86
+ if (categoryId !== undefined) {
87
+ return category.id === categoryId;
88
+ }
89
+ return true;
90
+ }
91
+
92
+ const getCategoryLink = (urlPath: string) => {
93
+ if (isPreviewMode.value) {
94
+ return undefined;
95
+ }
96
+ return urlPath;
97
+ };
98
+ </script>
99
+
100
+ <style scoped lang="scss">
101
+ .categories-dropdown {
102
+ position: absolute;
103
+ top: 48px;
104
+ left: 12px;
105
+ background-color: #ffffff;
106
+ border: 1px solid #ddd;
107
+ border-radius: 6px;
108
+ min-width: 200px;
109
+ padding: 8px;
110
+ z-index: 1000;
111
+ }
112
+
113
+ .category-list,
114
+ .subcategory-list {
115
+ list-style-type: none;
116
+ margin: 0;
117
+ padding: 0;
118
+ }
119
+
120
+ .category-item,
121
+ .subcategory-item {
122
+ cursor: pointer;
123
+ position: relative;
124
+ margin: 0;
125
+ padding: 6px 10px;
126
+ }
127
+
128
+ .category-link,
129
+ .subcategory-link {
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: space-between;
133
+ color: #080d12;
134
+ text-decoration: none;
135
+ width: 100%;
136
+ transition: color 0.35s ease;
137
+
138
+ &:hover {
139
+ color: #0056b3;
140
+ }
141
+ }
142
+
143
+ .arrow {
144
+ margin-left: 8px;
145
+ font-size: 10px;
146
+ color: #b7b7b7;
147
+ }
148
+
149
+ .subcategory-list {
150
+ display: none;
151
+ position: absolute;
152
+ top: 0;
153
+ left: 100%;
154
+ background-color: #ffffff;
155
+ border: 1px solid #ddd;
156
+ border-radius: 6px;
157
+ min-width: 200px;
158
+ padding: 8px;
159
+ z-index: 1001;
160
+ }
161
+
162
+ .category-item:hover > .subcategory-list {
163
+ display: block;
164
+ }
165
+ </style>
@@ -3,42 +3,109 @@
3
3
  v-if="navigationMenu.hasContent"
4
4
  role="navigation"
5
5
  >
6
- <ul>
6
+ <ul class="navigation-menu">
7
7
  <li
8
8
  v-for="item in navigationMenu.items"
9
9
  :key="item.id"
10
+ class="menu-item"
11
+ @mouseenter="onMouseEnter(item, $event)"
12
+ @mouseleave="onMouseLeave(item)"
13
+ @focusin="onMouseEnter(item, $event)"
14
+ @focusout="onMouseLeave(item)"
10
15
  >
11
- <a @click="item.performAction">
16
+ <a
17
+ class="menu-link"
18
+ @click="item.performAction"
19
+ >
12
20
  {{ item.title }}
13
21
  </a>
22
+ <span
23
+ v-if="item.showStoreCategories && isItemWithCategoriesDropdown(item)"
24
+ class="arrow"
25
+ >
26
+
27
+ </span>
14
28
  </li>
15
29
  </ul>
30
+ <CategoriesDropdown
31
+ v-if="showCategoriesDropdown"
32
+ :element="activeItem?.element"
33
+ :category-id="categoryId"
34
+ @hide="hideDropdown"
35
+ />
16
36
  </nav>
17
37
  </template>
18
38
 
19
39
  <script setup lang="ts">
40
+ import { ref, computed } from 'vue';
20
41
  import { useNavigationMenuElementContent } from '@lightspeed/crane';
42
+ import CategoriesDropdown from './CategoriesDropdown.vue';
43
+
44
+ interface ActiveItem {
45
+ actionLink: ActionLink;
46
+ element?: HTMLElement;
47
+ }
21
48
 
22
49
  const navigationMenu = useNavigationMenuElementContent();
50
+ const activeItem = ref<ActiveItem | null>();
51
+
52
+ const showCategoriesDropdown = computed(() => {
53
+ const item = activeItem.value?.actionLink;
54
+ if (isItemWithCategoriesDropdown(item)) {
55
+ return item?.showStoreCategories ?? true;
56
+ }
57
+ return false;
58
+ });
59
+
60
+ const categoryId = computed(() => {
61
+ const item = activeItem.value?.actionLink;
62
+ if (item?.type === 'GO_TO_CATEGORY') {
63
+ return item.categoryId;
64
+ }
65
+ return undefined;
66
+ });
67
+
68
+ function isItemWithCategoriesDropdown(item?: ActionLink) {
69
+ return item?.type === 'GO_TO_STORE' || item?.type === 'GO_TO_CATEGORY';
70
+ }
71
+
72
+ function onMouseEnter(item: ActionLink, event: MouseEvent | FocusEvent) {
73
+ activeItem.value = {
74
+ actionLink: item,
75
+ element: event.target as HTMLElement,
76
+ };
77
+ }
78
+
79
+ function onMouseLeave(item: ActionLink | null) {
80
+ if (activeItem.value?.actionLink === item) {
81
+ activeItem.value = null;
82
+ }
83
+ }
84
+
85
+ function hideDropdown() {
86
+ activeItem.value = null;
87
+ }
23
88
  </script>
24
89
 
25
- <style lang="scss" scoped>
26
- ul {
27
- gap: 16px;
90
+ <style scoped lang="scss">
91
+ .navigation-menu {
28
92
  display: flex;
93
+ gap: 16px;
29
94
  overflow-x: auto;
30
95
  white-space: nowrap;
31
96
  height: 48px;
97
+ list-style-type: none;
98
+ margin: 0;
99
+ padding: 0;
32
100
  }
33
101
 
34
- li {
35
- list-style: none;
102
+ .menu-item {
103
+ position: relative;
36
104
  display: flex;
37
105
  align-items: center;
38
- min-width: fit-content;
39
106
  }
40
107
 
41
- a {
108
+ .menu-link {
42
109
  cursor: pointer;
43
110
  font-size: 16px;
44
111
  font-weight: 500;
@@ -47,7 +114,13 @@ a {
47
114
  transition: color 0.35s ease;
48
115
 
49
116
  &:hover {
50
- color: #0056b3
117
+ color: #0056b3;
51
118
  }
52
119
  }
120
+
121
+ .arrow {
122
+ margin-left: 8px;
123
+ font-size: 8px;
124
+ color: #cacaca;
125
+ }
53
126
  </style>
@@ -0,0 +1,10 @@
1
+ <template>
2
+ <div>
3
+ <slot :name="Slot.PRODUCT_LIST" />
4
+ <slot :name="Slot.BOTTOM_BAR" />
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { CatalogLayoutSlot as Slot } from '@lightspeed/crane';
10
+ </script>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div>
3
+ <slot :name="Slot.CATEGORY_TITLE" />
4
+ <slot :name="Slot.PRODUCT_LIST" />
5
+ <slot :name="Slot.BOTTOM_BAR" />
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { CategoryLayoutSlot as Slot } from '@lightspeed/crane';
11
+ </script>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <div :class="{ 'product-details--top-title-navigation': isProductNameAlwaysFirstOnMobile }">
3
+ <slot :name="Slot.TOP_BAR" />
4
+ <div
5
+ :class="availableProductClasses"
6
+ itemtype="http://schema.org/Product"
7
+ :itemscope="showProductDetailsProductPrice"
8
+ >
9
+ <slot :name="Slot.GALLERY" />
10
+ <slot :name="Slot.SIDEBAR" />
11
+ <slot :name="Slot.DESCRIPTION" />
12
+ <div class="clearboth" />
13
+ </div>
14
+ <slot :name="Slot.REVIEW_LIST" />
15
+ <slot :name="Slot.RELATED_PRODUCTS" />
16
+ <slot :name="Slot.BOTTOM_BAR" />
17
+ </div>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import { HTMLAttributes } from 'vue';
22
+ import { ProductLayoutSlot as Slot } from '@lightspeed/crane';
23
+
24
+ interface Props {
25
+ availableProductClasses?: HTMLAttributes['class'],
26
+ showProductDetailsProductPrice?: boolean,
27
+ isProductNameAlwaysFirstOnMobile?: boolean,
28
+ }
29
+
30
+ withDefaults(defineProps<Props>(), {
31
+ isProductNameAlwaysFirstOnMobile: false,
32
+ availableProductClasses: '',
33
+ showProductDetailsProductPrice: true,
34
+ });
35
+ </script>
@@ -2,7 +2,8 @@ export default {
2
2
  sections: [
3
3
  {
4
4
  type: 'default',
5
- id: 'product-browser',
5
+ id: 'catalog',
6
+ layout_id: 'example-catalog',
6
7
  },
7
8
  ],
8
9
  };
@@ -2,7 +2,8 @@ export default {
2
2
  sections: [
3
3
  {
4
4
  type: 'default',
5
- id: 'product-browser',
5
+ id: 'category',
6
+ layout_id: 'example-category',
6
7
  },
7
8
  ],
8
9
  };
@@ -2,7 +2,8 @@ export default {
2
2
  sections: [
3
3
  {
4
4
  type: 'default',
5
- id: 'product-browser',
5
+ id: 'product',
6
+ layout_id: 'example-product',
6
7
  },
7
8
  ],
8
9
  };
package/types.d.ts CHANGED
@@ -8,13 +8,14 @@ declare module '*.vue' {
8
8
  }
9
9
 
10
10
  type ActionLinkType
11
- = 'SCROLL_TO_TILE'
11
+ = 'SCROLL_TO_TILE'
12
12
  | 'HYPER_LINK'
13
13
  | 'MAIL_LINK'
14
14
  | 'TEL_LINK'
15
15
  | 'GO_TO_STORE'
16
16
  | 'GO_TO_STORE_LINK'
17
- | 'GO_TO_PAGE';
17
+ | 'GO_TO_PAGE'
18
+ | 'GO_TO_CATEGORY';
18
19
 
19
20
  interface ButtonContentData {
20
21
  readonly title: string;
@@ -56,7 +57,7 @@ interface ToggleContentData {
56
57
  interface ActionLink {
57
58
  id: string;
58
59
  title?: string;
59
- type?: string;
60
+ type?: ActionLinkType;
60
61
  link?: string;
61
62
  email?: string;
62
63
  phone?: string;
@@ -77,24 +78,24 @@ type LogoType = 'TEXT' | 'IMAGE';
77
78
  interface LogoContentData {
78
79
  readonly type: LogoType;
79
80
  readonly text: string;
80
- readonly image: ImageContentData
81
+ readonly image: ImageContentData;
81
82
  }
82
83
 
83
84
  type GlobalColorsString =
84
- 'global.color.title' |
85
- 'global.color.body' |
86
- 'global.color.button' |
87
- 'global.color.link' |
88
- 'global.color.background';
85
+ 'global.color.title' |
86
+ 'global.color.body' |
87
+ 'global.color.button' |
88
+ 'global.color.link' |
89
+ 'global.color.background';
89
90
 
90
91
  type GlobalFontsString =
91
- 'global.fontFamily.title' |
92
- 'global.fontFamily.body';
92
+ 'global.fontFamily.title' |
93
+ 'global.fontFamily.body';
93
94
 
94
95
  type GlobalTextSizeString =
95
- 'global.textSize.title' |
96
- 'global.textSize.subtitle' |
97
- 'global.textSize.body';
96
+ 'global.textSize.title' |
97
+ 'global.textSize.subtitle' |
98
+ 'global.textSize.body';
98
99
 
99
100
  interface HSLColor {
100
101
  h: number;
@@ -140,9 +141,9 @@ interface TextareaDesignData extends TextDesignData {
140
141
  }
141
142
 
142
143
  type CapitalizationType =
143
- 'none'
144
- | 'all'
145
- | 'small';
144
+ 'none'
145
+ | 'all'
146
+ | 'small';
146
147
 
147
148
  interface Frame {
148
149
  visible: boolean;
@@ -163,19 +164,19 @@ interface LogoDesignData {
163
164
  }
164
165
 
165
166
  type ButtonAppearance =
166
- 'solid-button'
167
- | 'outline-button'
168
- | 'text-link';
167
+ 'solid-button'
168
+ | 'outline-button'
169
+ | 'text-link';
169
170
 
170
171
  type ButtonSize =
171
- 'small'
172
- | 'medium'
173
- | 'large';
172
+ 'small'
173
+ | 'medium'
174
+ | 'large';
174
175
 
175
176
  type ButtonStyle =
176
- 'round-corner'
177
- | 'rectangle'
178
- | 'pill';
177
+ 'round-corner'
178
+ | 'rectangle'
179
+ | 'pill';
179
180
 
180
181
  interface ButtonDesignData {
181
182
  appearance: ButtonAppearance | undefined;
@@ -187,9 +188,9 @@ interface ButtonDesignData {
187
188
  }
188
189
 
189
190
  type OverlayType =
190
- 'solid'
191
- | 'gradient'
192
- | 'none';
191
+ 'solid'
192
+ | 'gradient'
193
+ | 'none';
193
194
 
194
195
  interface Overlay {
195
196
  type: OverlayType | undefined;
@@ -211,8 +212,8 @@ interface ToggleDesignData {
211
212
  }
212
213
 
213
214
  type BackgroundType =
214
- 'solid'
215
- | 'gradient';
215
+ 'solid'
216
+ | 'gradient';
216
217
 
217
218
  interface Background {
218
219
  type: BackgroundType | undefined;
@@ -295,8 +296,33 @@ interface SiteContent {
295
296
  readonly legalPages?: LegalPage[];
296
297
  }
297
298
 
299
+ interface CategoryListComponentItem {
300
+ readonly id: number;
301
+ readonly name: string;
302
+ readonly url: string;
303
+ readonly imageUrl?: string;
304
+ readonly thumbnailImageUrl?: string;
305
+ readonly imageBorderInfo?: ImageBorderInfo;
306
+ readonly alt?: string;
307
+ }
308
+
309
+ interface CategoryTree {
310
+ readonly id: number;
311
+ readonly name: string;
312
+ readonly nameTranslated: Record<string, string>;
313
+ readonly urlPath: string;
314
+ readonly enabled: boolean;
315
+ readonly children: CategoryTree[];
316
+ }
317
+
318
+ interface Category {
319
+ readonly categories?: CategoryListComponentItem[];
320
+ readonly categoryTree?: CategoryTree[];
321
+ }
322
+
298
323
  interface ExternalContentData {
299
- readonly site?: SiteContent;
324
+ readonly site?: SiteContent;
325
+ readonly category?: Category;
300
326
  }
301
327
 
302
328
  interface ButtonContentEditor {
@@ -344,19 +370,19 @@ interface LogoContentEditor {
344
370
  }
345
371
 
346
372
  type ContentEditor =
347
- TextContentEditor
348
- | MultilineTextContentEditor
349
- | ButtonContentEditor
350
- | ImageContentEditor
351
- | ToggleContentEditor
352
- | SelectboxContentEditor
353
- | MenuContentEditor
354
- | NavigationMenuContentEditor
355
- | LogoContentEditor;
373
+ TextContentEditor
374
+ | MultilineTextContentEditor
375
+ | ButtonContentEditor
376
+ | ImageContentEditor
377
+ | ToggleContentEditor
378
+ | SelectboxContentEditor
379
+ | MenuContentEditor
380
+ | NavigationMenuContentEditor
381
+ | LogoContentEditor;
356
382
 
357
383
  type InferContentType<T extends Record<string, ContentEditor>> = {
358
384
  readonly [P in keyof T]: MapEditorContentTypes[T[P]['type']]
359
- }
385
+ };
360
386
 
361
387
  type MapEditorDesignTypes = {
362
388
  readonly TEXT: string;
@@ -418,8 +444,8 @@ interface LogoDesignEditor {
418
444
  }
419
445
 
420
446
  interface DividerDesignEditor {
421
- readonly type: 'DIVIDER';
422
- readonly label: string | Record<string, string>;
447
+ readonly type: 'DIVIDER';
448
+ readonly label: string | Record<string, string>;
423
449
  }
424
450
 
425
451
  type DesignEditor =
@@ -435,6 +461,6 @@ type DesignEditor =
435
461
 
436
462
  type InferDesignType<T extends Record<string, DesignEditor>> = {
437
463
  readonly [P in keyof T]: MapEditorDesignTypes[T[P]['type']]
438
- }
464
+ };
439
465
 
440
466
  type SettingsEditor = DesignEditor | ContentEditor;