@osimart/create-storefront 0.0.0-stage → 1.0.1
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/README.md +80 -2
- package/THEME_DEV_API.md +80 -0
- package/app/app.vue +154 -0
- package/app/assets/css/osi-controls.css +824 -0
- package/app/components/SectionRenderer.vue +46 -0
- package/app/components/builder/BuilderOverlay.vue +582 -0
- package/app/components/checkout/ExtraFees.vue +329 -0
- package/app/components/checkout/PaymentHandoff.vue +388 -0
- package/app/components/common/Banner.vue +35 -0
- package/app/components/common/ConfirmDeleteModal.vue +117 -0
- package/app/components/common/DropdownSelect.vue +432 -0
- package/app/components/common/Loader.vue +32 -0
- package/app/components/common/OsiField.vue +218 -0
- package/app/components/common/PhoneNumInput.vue +434 -0
- package/app/composables/UseBodyScrollLock.js +122 -0
- package/app/composables/UseThemeGroup.js +575 -0
- package/app/composables/useAddToCart.js +87 -0
- package/app/composables/useAnalytics.js +15 -0
- package/app/composables/useAnnouncements.js +29 -0
- package/app/composables/useApiErrorToast.js +104 -0
- package/app/composables/useAuth.js +99 -0
- package/app/composables/useBanners.js +25 -0
- package/app/composables/useBuilderPreview.js +320 -0
- package/app/composables/useButtonStyle.js +107 -0
- package/app/composables/useCarousel.js +160 -0
- package/app/composables/useCart.js +109 -0
- package/app/composables/useCheckout.js +76 -0
- package/app/composables/useClassificationItems.js +269 -0
- package/app/composables/useClassificationPage.js +545 -0
- package/app/composables/useClassificationUrl.js +63 -0
- package/app/composables/useColorScheme.js +44 -0
- package/app/composables/useContactForm.js +53 -0
- package/app/composables/useContentTree.js +770 -0
- package/app/composables/useDragScroll.js +73 -0
- package/app/composables/useFooterSocials.js +107 -0
- package/app/composables/useInputStyle.js +45 -0
- package/app/composables/useLayoutShell.js +114 -0
- package/app/composables/useLazyVideo.js +36 -0
- package/app/composables/useMediaTypes.js +66 -0
- package/app/composables/useMediaUrl.js +71 -0
- package/app/composables/useMenu.js +100 -0
- package/app/composables/useNavStyle.js +219 -0
- package/app/composables/usePlaceholders.js +294 -0
- package/app/composables/usePolicies.js +26 -0
- package/app/composables/useProductCardMedia.js +135 -0
- package/app/composables/useProductCardStyle.js +132 -0
- package/app/composables/useProductDetail.js +982 -0
- package/app/composables/useProductGallery.js +166 -0
- package/app/composables/useProductItems.js +270 -0
- package/app/composables/useProductUrl.js +60 -0
- package/app/composables/useProductVariants.js +210 -0
- package/app/composables/useQuickShop.js +115 -0
- package/app/composables/useRelatedProducts.js +186 -0
- package/app/composables/useRenderContext.js +45 -0
- package/app/composables/useScopedCss.js +106 -0
- package/app/composables/useSearch.js +35 -0
- package/app/composables/useSectionData.js +462 -0
- package/app/composables/useSectionShell.js +113 -0
- package/app/composables/useSidebar.js +69 -0
- package/app/composables/useStoreData.js +57 -0
- package/app/composables/useStoreInit.js +52 -0
- package/app/composables/useStorefront.js +239 -0
- package/app/composables/useT.js +21 -0
- package/app/composables/useTextStyles.js +84 -0
- package/app/composables/useThemeControls.js +34 -0
- package/app/composables/useThemeSeo.js +91 -0
- package/app/composables/useThemeTokens.js +171 -0
- package/app/composables/useWishlist.js +66 -0
- package/app/logic/components/AnnouncementBar.js +118 -0
- package/app/logic/components/CardGrid.js +125 -0
- package/app/logic/components/CardList.js +98 -0
- package/app/logic/components/CartDrawer.js +273 -0
- package/app/logic/components/CartItem.js +31 -0
- package/app/logic/components/ClassificationBento.js +6 -0
- package/app/logic/components/ClassificationCard.js +6 -0
- package/app/logic/components/ClassificationCarousel.js +115 -0
- package/app/logic/components/ClassificationEditorial.js +41 -0
- package/app/logic/components/ClassificationFeatured.js +57 -0
- package/app/logic/components/ClassificationGrid.js +6 -0
- package/app/logic/components/ClassificationNav.js +61 -0
- package/app/logic/components/ClassificationPart.js +72 -0
- package/app/logic/components/FooterBlock.js +91 -0
- package/app/logic/components/FooterSocials.js +60 -0
- package/app/logic/components/HeaderDropdownList.js +29 -0
- package/app/logic/components/HeaderIcon.js +68 -0
- package/app/logic/components/HeaderLogo.js +39 -0
- package/app/logic/components/HeaderNav.js +268 -0
- package/app/logic/components/MobileMenu.js +280 -0
- package/app/logic/components/ProductBuyButtons.js +66 -0
- package/app/logic/components/ProductCard.js +160 -0
- package/app/logic/components/ProductFilters.js +37 -0
- package/app/logic/components/ProductGallery.js +19 -0
- package/app/logic/components/ProductInfoBlock.js +411 -0
- package/app/logic/components/ProductLightbox.js +12 -0
- package/app/logic/components/ProductQuantity.js +31 -0
- package/app/logic/components/ProductVariantPicker.js +125 -0
- package/app/logic/components/QuickBuy.js +194 -0
- package/app/logic/components/SearchOverlay.js +41 -0
- package/app/logic/components/SidebarMenuItem.js +39 -0
- package/app/logic/components/SiteFooter.js +476 -0
- package/app/logic/components/SiteHeader.js +266 -0
- package/app/logic/components/WhatsAppButton.js +75 -0
- package/app/logic/layouts/default.js +66 -0
- package/app/logic/sections/AnnouncementBarSection.js +6 -0
- package/app/logic/sections/BlockCarousel.js +103 -0
- package/app/logic/sections/ClassificationListSection.js +40 -0
- package/app/logic/sections/ContactFormSection.js +294 -0
- package/app/logic/sections/ContactInfoSection.js +419 -0
- package/app/logic/sections/ContentNode.js +189 -0
- package/app/logic/sections/CountdownSection.js +254 -0
- package/app/logic/sections/CustomHtmlSection.js +51 -0
- package/app/logic/sections/DividerSection.js +31 -0
- package/app/logic/sections/FaqSection.js +53 -0
- package/app/logic/sections/FeatureListSection.js +6 -0
- package/app/logic/sections/FeaturedProductSection.js +40 -0
- package/app/logic/sections/FooterSection.js +16 -0
- package/app/logic/sections/FooterUtilitiesSection.js +24 -0
- package/app/logic/sections/GallerySection.js +232 -0
- package/app/logic/sections/HeaderSection.js +24 -0
- package/app/logic/sections/HeroSection.js +24 -0
- package/app/logic/sections/LogoListSection.js +6 -0
- package/app/logic/sections/MainClassificationSection.js +473 -0
- package/app/logic/sections/MainProductSection.js +211 -0
- package/app/logic/sections/MainSearchSection.js +6 -0
- package/app/logic/sections/MapSection.js +92 -0
- package/app/logic/sections/MediaContentSection.js +38 -0
- package/app/logic/sections/MediaWithTextSection.js +238 -0
- package/app/logic/sections/NewsletterSection.js +126 -0
- package/app/logic/sections/ProductHighlightSection.js +201 -0
- package/app/logic/sections/ProductListSection.js +45 -0
- package/app/logic/sections/ProductRecommendationsSection.js +113 -0
- package/app/logic/sections/RichTextSection.js +6 -0
- package/app/logic/sections/ScrollingTextSection.js +69 -0
- package/app/logic/sections/SearchHeaderSection.js +22 -0
- package/app/logic/sections/SlideshowSection.js +118 -0
- package/app/logic/sections/SpacerSection.js +37 -0
- package/app/logic/sections/SplitShowcaseSection.js +6 -0
- package/app/logic/sections/TestimonialListSection.js +6 -0
- package/app/logic/sections/VideoSection.js +235 -0
- package/app/pages/[category]/[product].vue +196 -0
- package/app/pages/__builder-preview.vue +38 -0
- package/app/pages/categories/[slug].vue +208 -0
- package/app/pages/checkout.vue +1972 -0
- package/app/pages/forgot-password.vue +115 -0
- package/app/pages/index.vue +64 -0
- package/app/pages/login.vue +585 -0
- package/app/pages/order-detail.vue +442 -0
- package/app/pages/pages/[handle].vue +58 -0
- package/app/pages/privacy-policy.vue +46 -0
- package/app/pages/resetpassword.vue +138 -0
- package/app/pages/resetverificationcode.vue +163 -0
- package/app/pages/result.vue +203 -0
- package/app/pages/return-policy.vue +50 -0
- package/app/pages/search.vue +100 -0
- package/app/pages/shipping-policy.vue +46 -0
- package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
- package/app/pages/user-profile/components/Whishlist.vue +137 -0
- package/app/pages/user-profile/components/left-bar.vue +233 -0
- package/app/pages/user-profile/components/orders.vue +162 -0
- package/app/pages/user-profile/components/profile-details.vue +566 -0
- package/app/pages/user-profile/components/settings.vue +194 -0
- package/app/pages/user-profile/index.vue +252 -0
- package/app/pages/verificationcode.vue +188 -0
- package/app/plugins/analytics.client.js +99 -0
- package/app/plugins/geo.js +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.js +5 -0
- package/app/services/ApiServices.js +74 -0
- package/app/services/AuthBus.js +162 -0
- package/app/services/Authentication/AuthenticationService.js +133 -0
- package/app/services/Authentication/TokenService.js +62 -0
- package/app/services/CartServices.js +91 -0
- package/app/services/CartToken.js +26 -0
- package/app/services/CategoryServices.js +54 -0
- package/app/services/CheckoutServices.js +113 -0
- package/app/services/HomeServices.js +133 -0
- package/app/services/NewsletterServices.js +29 -0
- package/app/services/PaymentServices.js +71 -0
- package/app/services/PoliciesServices.js +16 -0
- package/app/services/ProductServices.js +79 -0
- package/app/services/SearchServices.js +18 -0
- package/app/services/UserServices.js +105 -0
- package/app/services/UtilsServices.js +46 -0
- package/app/services/WishlistServices.js +51 -0
- package/app/services/storefront.service.js +57 -0
- package/app/stores/userStore.js +13 -0
- package/app/theme/categoryTree.js +68 -0
- package/app/theme/controls.js +88 -0
- package/app/theme/registry.js +56 -0
- package/app/utils/analytics.js +864 -0
- package/app/utils/device.js +50 -0
- package/app/utils/eventBus.js +24 -0
- package/app/utils/icon-presets.js +24 -0
- package/app/utils/productOffers.js +118 -0
- package/app/utils/requestCache.js +36 -0
- package/app/utils/scrollToError.js +59 -0
- package/app/utils/toPlainError.js +30 -0
- package/bin/osimart-theme.mjs +282 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +44 -4
- package/public/css/input.css +29 -0
- package/public/css/output.css +2716 -0
- package/public/favicon-32x32.png +0 -0
- package/public/robots.txt +2 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { resolveSection } from "#osi/theme/registry";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renders a template's sections in order.
|
|
6
|
+
*
|
|
7
|
+
* data-osi-section / data-osi-block are emitted in production too, not just in
|
|
8
|
+
* preview. They cost nothing, and forgetting them on one section type is an
|
|
9
|
+
* infuriating bug to chase later — click-to-select silently stops working for
|
|
10
|
+
* that section only.
|
|
11
|
+
*/
|
|
12
|
+
defineProps({
|
|
13
|
+
sections: { type: Array, default: () => [] },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Hover / selection outlines are drawn by BuilderOverlay (pages/__builder-preview),
|
|
17
|
+
// not here: a CSS outline gets clipped by overflow, can't carry a name tag or
|
|
18
|
+
// buttons, and a nested block's outline would fight its section's.
|
|
19
|
+
const { isPreview } = useBuilderPreview();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="osi-sections">
|
|
24
|
+
<template v-for="(section, i) in sections" :key="section.key">
|
|
25
|
+
<component
|
|
26
|
+
:is="resolveSection(section.type)"
|
|
27
|
+
v-if="resolveSection(section.type)"
|
|
28
|
+
:section="section"
|
|
29
|
+
:index="i"
|
|
30
|
+
:data-osi-section="section.key"
|
|
31
|
+
:class="{ 'osi-preview-disabled': isPreview && section.disabled }"
|
|
32
|
+
/>
|
|
33
|
+
</template>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<style scoped>
|
|
38
|
+
.osi-sections > :deep(*) + :deep(*) {
|
|
39
|
+
margin-top: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Editor only — never rendered on a live storefront. */
|
|
43
|
+
.osi-preview-disabled {
|
|
44
|
+
opacity: 0.45;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {
|
|
3
|
+
findPreviewElements,
|
|
4
|
+
previewElements,
|
|
5
|
+
previewSelector,
|
|
6
|
+
resolvePreviewElement,
|
|
7
|
+
} from "#osi/composables/useBuilderPreview";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The editor's overlay inside the builder preview — Shopify-style.
|
|
11
|
+
*
|
|
12
|
+
* hover thin blue outline + a name tag with the block's parents
|
|
13
|
+
* ("Rich text › Group › Text"), faint dashed outlines on those
|
|
14
|
+
* parents so the nesting shows
|
|
15
|
+
* copies a block drawn more than once (a card part, once per item in a
|
|
16
|
+
* list) gets its box on the copy under the pointer / the one
|
|
17
|
+
* clicked, and a light border on every other copy — so it reads
|
|
18
|
+
* "this, and all of these".
|
|
19
|
+
* selected 2px blue border, the same tag with clickable crumbs (select a
|
|
20
|
+
* parent), and a toolbar: move up / down, duplicate, delete
|
|
21
|
+
* "+" on the hovered (else selected) block, on the two edges along
|
|
22
|
+
* its parent's direction — insert before / after; centred in an
|
|
23
|
+
* empty group; "Add section" above / below a section
|
|
24
|
+
*
|
|
25
|
+
* One fixed layer over the page, never an outline on the blocks themselves:
|
|
26
|
+
* outlines get clipped by overflow, can't carry a tag or buttons, and a nested
|
|
27
|
+
* block's outline would fight its parents'. The layer ignores the pointer
|
|
28
|
+
* except on its own buttons ([data-osi-overlay] — useBuilderPreview skips
|
|
29
|
+
* those when tracking hover and clicks).
|
|
30
|
+
*
|
|
31
|
+
* Teleported to <body>, so no wrapper's stacking context can put the site's
|
|
32
|
+
* sticky header above it; name tags also stick just below such a header while
|
|
33
|
+
* their block is on screen (topInset / tagStyle).
|
|
34
|
+
*
|
|
35
|
+
* The dashboard clips this iframe to a rounded shell (PreviewFrame.vue). An
|
|
36
|
+
* outline that reaches the top / bottom of the frame rounds its corners to
|
|
37
|
+
* follow that curve (see boxStyle) instead of being cut off square. Name tags
|
|
38
|
+
* are left exactly as they are. The radius arrives in a FRAME message — see
|
|
39
|
+
* frameRadius.
|
|
40
|
+
*
|
|
41
|
+
* Positions come from getBoundingClientRect() in a requestAnimationFrame loop,
|
|
42
|
+
* so boxes follow scrolling, carousels, the marquee and re-renders without a
|
|
43
|
+
* listener per case. The loop only touches reactive state when a box moved.
|
|
44
|
+
*
|
|
45
|
+
* Hidden entirely while the dashboard's top-bar toggle is off (overlayOn).
|
|
46
|
+
*
|
|
47
|
+
* Talks to the dashboard through useBuilderPreview().send():
|
|
48
|
+
* SECTION_CLICK { key, blockKey } select (crumbs)
|
|
49
|
+
* ACTION { kind, key, action } up | down | duplicate | delete
|
|
50
|
+
* ADD_BLOCK { section, parent, index, x, y }
|
|
51
|
+
* ADD_SECTION { index, area } area: header | template | footer
|
|
52
|
+
*/
|
|
53
|
+
const { state, selectedKey, hoveredKey, labels, overlayOn, send, isPreview } = useBuilderPreview();
|
|
54
|
+
|
|
55
|
+
/* ------------------------------------------------------------------ tree */
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Every section on the page: the header group's, the template's, the footer
|
|
59
|
+
* group's — each tagged with its area, positions counted within it.
|
|
60
|
+
*/
|
|
61
|
+
const allSections = computed(() => {
|
|
62
|
+
const s = state.value ?? {};
|
|
63
|
+
const tag = (list, area) => (list ?? []).map((section, position) => ({ section, area, position }));
|
|
64
|
+
return [
|
|
65
|
+
...tag(s.groups?.header, "header"),
|
|
66
|
+
...tag(s.sections, "template"),
|
|
67
|
+
...tag(s.groups?.footer, "footer"),
|
|
68
|
+
];
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** key -> { kind, section, parent, type } for every section and block. */
|
|
72
|
+
const index = computed(() => {
|
|
73
|
+
const map = {};
|
|
74
|
+
allSections.value.forEach(({ section, area, position }) => {
|
|
75
|
+
map[section.key] = { kind: "SECTION", section: section.key, parent: null, type: section.type, position, area };
|
|
76
|
+
for (const block of section.blocks ?? []) {
|
|
77
|
+
map[block.key] = { kind: "BLOCK", section: section.key, parent: block.parent_key || null, type: block.type };
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return map;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function sectionOf(key) {
|
|
84
|
+
return allSections.value.find((e) => e.section.key === index.value[key]?.section)?.section;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Keys of `parent`'s children (null = the section itself), in order. */
|
|
88
|
+
function siblings(sectionKey, parent) {
|
|
89
|
+
const section = allSections.value.find((e) => e.section.key === sectionKey)?.section;
|
|
90
|
+
return (section?.blocks ?? []).filter((b) => (b.parent_key || null) === parent).map((b) => b.key);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** [section, ...parents, key] — outermost first. */
|
|
94
|
+
function chain(key) {
|
|
95
|
+
const info = index.value[key];
|
|
96
|
+
if (!info) return [];
|
|
97
|
+
if (info.kind === "SECTION") return [key];
|
|
98
|
+
const out = [key];
|
|
99
|
+
for (let p = info.parent, guard = 0; p && guard < 20; p = index.value[p]?.parent, guard++) out.unshift(p);
|
|
100
|
+
out.unshift(info.section);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function nameOf(key) {
|
|
105
|
+
const label = labels.value?.[key]?.label;
|
|
106
|
+
if (label) return label;
|
|
107
|
+
const type = index.value[key]?.type ?? "";
|
|
108
|
+
return type.charAt(0) + type.slice(1).toLowerCase().replace(/_/g, " ");
|
|
109
|
+
}
|
|
110
|
+
const hintOf = (key) => labels.value?.[key]?.hint || "";
|
|
111
|
+
const isContainer = (key) => !!labels.value?.[key]?.container;
|
|
112
|
+
// Static blocks (a static_blocks section): no move / duplicate / delete / "+".
|
|
113
|
+
const isFixed = (key) => !!labels.value?.[key]?.fixed;
|
|
114
|
+
// The page's main section (schema `required`): no delete.
|
|
115
|
+
const isRequired = (key) => !!labels.value?.[key]?.required;
|
|
116
|
+
|
|
117
|
+
/* --------------------------------------------------------------- measuring */
|
|
118
|
+
|
|
119
|
+
const view = reactive({
|
|
120
|
+
hover: null, // { key, top, left, width, height, row }
|
|
121
|
+
selected: null,
|
|
122
|
+
parents: [], // rects of the hovered block's parents
|
|
123
|
+
// Other copies of the hovered / selected block (a card part is drawn once
|
|
124
|
+
// per item): a light border on each, so all of them read as one block.
|
|
125
|
+
twins: [],
|
|
126
|
+
// Bottom edge of a fixed / sticky header pinned to the top of the page, if
|
|
127
|
+
// any: name tags stick just below it instead of sliding underneath.
|
|
128
|
+
safeTop: 0,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* How much of the top of the viewport a pinned header covers. Probes what's
|
|
133
|
+
* at the top centre (the overlay itself doesn't count — it ignores the
|
|
134
|
+
* pointer, so hit-testing skips it) and walks up to a fixed / sticky
|
|
135
|
+
* ancestor touching the top. No such header -> 0.
|
|
136
|
+
*/
|
|
137
|
+
function topInset() {
|
|
138
|
+
const toolbar = document.querySelector("[data-osi-toolbar]"); // however your toolbar is marked
|
|
139
|
+
if (!toolbar) return 0;
|
|
140
|
+
const r = toolbar.getBoundingClientRect();
|
|
141
|
+
return r.top <= 1 ? r.bottom : 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The on-screen box of one element, standing for `key`. */
|
|
145
|
+
function rectOf(el, key, id = key) {
|
|
146
|
+
if (!el) return null;
|
|
147
|
+
const r = el.getBoundingClientRect();
|
|
148
|
+
if (!r.width && !r.height) return null;
|
|
149
|
+
// Which way its siblings run: the "+" buttons sit on the edges along it.
|
|
150
|
+
const parent = el.parentElement;
|
|
151
|
+
const cs = parent ? getComputedStyle(parent) : null;
|
|
152
|
+
const row = !!cs && cs.display.includes("flex") && cs.flexDirection.startsWith("row");
|
|
153
|
+
return { key, id, top: r.top, left: r.left, width: r.width, height: r.height, row };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const same = (a, b) =>
|
|
157
|
+
a === b ||
|
|
158
|
+
(!!a && !!b && a.id === b.id && a.row === b.row &&
|
|
159
|
+
Math.abs(a.top - b.top) < 0.5 && Math.abs(a.left - b.left) < 0.5 &&
|
|
160
|
+
Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5);
|
|
161
|
+
const sameList = (a, b) => a.length === b.length && a.every((x, i) => same(x, b[i]));
|
|
162
|
+
|
|
163
|
+
/** Every other live copy of `key`, as boxes (the primary one excluded). */
|
|
164
|
+
function twinsOf(key, primary) {
|
|
165
|
+
if (!key) return [];
|
|
166
|
+
const out = [];
|
|
167
|
+
findPreviewElements(key).forEach((el, i) => {
|
|
168
|
+
if (el === primary) return;
|
|
169
|
+
const box = rectOf(el, key, `${key}#${i}`);
|
|
170
|
+
if (box) out.push(box);
|
|
171
|
+
});
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function measure() {
|
|
176
|
+
const inset = Math.round(topInset());
|
|
177
|
+
if (inset !== view.safeTop) view.safeTop = inset;
|
|
178
|
+
|
|
179
|
+
// The exact copies: the one clicked, the one under the pointer. Selected /
|
|
180
|
+
// hovered from the dashboard's layer tree there's no copy — the first one.
|
|
181
|
+
const selectedEl = resolvePreviewElement(selectedKey.value, previewElements.selected);
|
|
182
|
+
const selected = rectOf(selectedEl, selectedKey.value);
|
|
183
|
+
if (!same(view.selected, selected)) view.selected = selected;
|
|
184
|
+
|
|
185
|
+
const hoverEl = resolvePreviewElement(hoveredKey.value, previewElements.hover);
|
|
186
|
+
const hoverKey = hoveredKey.value && hoveredKey.value !== selectedKey.value ? hoveredKey.value : null;
|
|
187
|
+
const hover = hoverKey ? rectOf(hoverEl, hoverKey) : null;
|
|
188
|
+
if (!same(view.hover, hover)) view.hover = hover;
|
|
189
|
+
|
|
190
|
+
// Parents of the copy under the pointer — its own card, not card #1's.
|
|
191
|
+
const parents = hoveredKey.value
|
|
192
|
+
? chain(hoveredKey.value)
|
|
193
|
+
.slice(0, -1)
|
|
194
|
+
.map((key) => rectOf(hoverEl?.closest(previewSelector(key)) ?? resolvePreviewElement(key), key))
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
: [];
|
|
197
|
+
if (!sameList(parents, view.parents)) view.parents = parents;
|
|
198
|
+
|
|
199
|
+
const twins = [
|
|
200
|
+
...twinsOf(selectedKey.value, selectedEl),
|
|
201
|
+
...(hoverKey ? twinsOf(hoverKey, hoverEl) : []),
|
|
202
|
+
];
|
|
203
|
+
if (!sameList(twins, view.twins)) view.twins = twins;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let raf = 0;
|
|
207
|
+
function loop() {
|
|
208
|
+
measure();
|
|
209
|
+
raf = requestAnimationFrame(loop);
|
|
210
|
+
}
|
|
211
|
+
onMounted(() => {
|
|
212
|
+
raf = requestAnimationFrame(loop);
|
|
213
|
+
window.addEventListener("message", onFrameMessage);
|
|
214
|
+
// The dashboard also pushes FRAME on READY, but this component may mount
|
|
215
|
+
// after that — ask again so the radius is never missed.
|
|
216
|
+
if (window.parent !== window) {
|
|
217
|
+
window.parent.postMessage({ protocol: PROTOCOL, type: "FRAME_REQUEST" }, "*");
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
onBeforeUnmount(() => {
|
|
221
|
+
cancelAnimationFrame(raf);
|
|
222
|
+
window.removeEventListener("message", onFrameMessage);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/* ---------------------------------------------------------- frame corners */
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The dashboard rounds and clips the preview (a rounded shell around this
|
|
229
|
+
* iframe). It can't be read from in here — cross-origin — so PreviewFrame
|
|
230
|
+
* sends the radius of the visible area: { radius } in px, 0 when the shell
|
|
231
|
+
* is square (phone-width dashboard).
|
|
232
|
+
*/
|
|
233
|
+
const PROTOCOL = "osimart-builder/1";
|
|
234
|
+
const frameRadius = ref(0);
|
|
235
|
+
|
|
236
|
+
function onFrameMessage(e) {
|
|
237
|
+
if (e.source !== window.parent) return;
|
|
238
|
+
const d = e.data;
|
|
239
|
+
if (!d || d.protocol !== PROTOCOL || d.type !== "FRAME") return;
|
|
240
|
+
const r = Number(d.payload?.radius);
|
|
241
|
+
frameRadius.value = Number.isFinite(r) && r > 0 ? r : 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* boxStyle: the box's position plus rounded corners near the frame's corners.
|
|
246
|
+
* A box corner within `frameRadius` of both frame edges it's heading for
|
|
247
|
+
* (e.g. top + left) gets a radius that grows from 0 to the full frame radius
|
|
248
|
+
* as it closes in, so the outline eases into the curve as it nears the top
|
|
249
|
+
* and matches it exactly once touching. A corner that's scrolled past the
|
|
250
|
+
* edge is off screen, so it stays square.
|
|
251
|
+
*/
|
|
252
|
+
const EDGE = 1;
|
|
253
|
+
|
|
254
|
+
function cornerRadius(dx, dy) {
|
|
255
|
+
const R = frameRadius.value;
|
|
256
|
+
if (R <= 0 || dx < -EDGE || dy < -EDGE) return 0;
|
|
257
|
+
return Math.max(0, R - Math.max(dx, dy, 0));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function boxStyle(box) {
|
|
261
|
+
const w = window.innerWidth;
|
|
262
|
+
const h = window.innerHeight;
|
|
263
|
+
const right = w - (box.left + box.width);
|
|
264
|
+
const bottom = h - (box.top + box.height);
|
|
265
|
+
const tl = cornerRadius(box.left, box.top);
|
|
266
|
+
const tr = cornerRadius(right, box.top);
|
|
267
|
+
const br = cornerRadius(right, bottom);
|
|
268
|
+
const bl = cornerRadius(box.left, bottom);
|
|
269
|
+
return {
|
|
270
|
+
top: `${box.top}px`,
|
|
271
|
+
left: `${box.left}px`,
|
|
272
|
+
width: `${box.width}px`,
|
|
273
|
+
height: `${box.height}px`,
|
|
274
|
+
borderRadius: `${tl}px ${tr}px ${br}px ${bl}px`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* ------------------------------------------------------------------ tags */
|
|
279
|
+
|
|
280
|
+
const TAG_H = 22;
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Where a box's name tag goes, like Shopify's:
|
|
284
|
+
* room above the box (below any pinned header) -> sitting on the box's top
|
|
285
|
+
* edge like a tab (overlapping the border, so no gap shows): only its top
|
|
286
|
+
* corners are rounded
|
|
287
|
+
* box top visible but no room above -> at the box's top-left
|
|
288
|
+
* corner
|
|
289
|
+
* box top scrolled under the header / off screen -> stuck just below the
|
|
290
|
+
* header, riding down with the box until the box's bottom pushes it up
|
|
291
|
+
* — in both of these it hangs from the edge above it, so only its
|
|
292
|
+
* bottom corners are rounded
|
|
293
|
+
* `border` is the box's outline width (1 hover, 2 selected). The tag is
|
|
294
|
+
* nudged 2px left of the box.
|
|
295
|
+
*/
|
|
296
|
+
const TAG_R = 4;
|
|
297
|
+
function tagStyle(box, border = 1) {
|
|
298
|
+
const safe = view.safeTop;
|
|
299
|
+
let top;
|
|
300
|
+
let above = false;
|
|
301
|
+
if (box.top - TAG_H >= safe) { top = box.top - TAG_H + border; above = true; }
|
|
302
|
+
else if (box.top >= safe) top = box.top;
|
|
303
|
+
else top = Math.min(safe, box.top + box.height - TAG_H - 2);
|
|
304
|
+
return {
|
|
305
|
+
top: `${top}px`,
|
|
306
|
+
left: `${Math.max(0, Math.min(box.left, window.innerWidth - 60) - 2)}px`,
|
|
307
|
+
borderRadius: above ? `${TAG_R}px ${TAG_R}px 0 0` : `0 0 ${TAG_R}px ${TAG_R}px`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const hoverCrumbs = computed(() => (view.hover ? chain(view.hover.key) : []));
|
|
312
|
+
const selectedCrumbs = computed(() => (view.selected ? chain(view.selected.key) : []));
|
|
313
|
+
const hoveringSelected = computed(() => !!selectedKey.value && hoveredKey.value === selectedKey.value);
|
|
314
|
+
function selectKey(key) {
|
|
315
|
+
const info = index.value[key];
|
|
316
|
+
if (!info) return;
|
|
317
|
+
send("SECTION_CLICK", { key: info.section, blockKey: info.kind === "BLOCK" ? key : null });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function act(action) {
|
|
321
|
+
const key = view.selected?.key;
|
|
322
|
+
const info = index.value[key];
|
|
323
|
+
if (!info) return;
|
|
324
|
+
send("ACTION", { kind: info.kind, key, action });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ------------------------------------------------------------- "+" buttons */
|
|
328
|
+
|
|
329
|
+
/** The box the "+" buttons belong to: the hovered one, else the selected one. */
|
|
330
|
+
const active = computed(() => view.hover ?? view.selected);
|
|
331
|
+
|
|
332
|
+
const plusButtons = computed(() => {
|
|
333
|
+
const box = active.value;
|
|
334
|
+
if (!box) return [];
|
|
335
|
+
const info = index.value[box.key];
|
|
336
|
+
if (!info) return [];
|
|
337
|
+
|
|
338
|
+
if (info.kind === "SECTION") {
|
|
339
|
+
const cx = box.left + box.width / 2;
|
|
340
|
+
return [
|
|
341
|
+
{ id: "s-before", label: "Add section", wide: true, x: cx, y: box.top, add: () => send("ADD_SECTION", { index: info.position, area: info.area }) },
|
|
342
|
+
{ id: "s-after", label: "Add section", wide: true, x: cx, y: box.top + box.height, add: () => send("ADD_SECTION", { index: info.position + 1, area: info.area }) },
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (isFixed(box.key)) return [];
|
|
347
|
+
const at = siblings(info.section, info.parent).indexOf(box.key);
|
|
348
|
+
const addAt = (i, x, y) => send("ADD_BLOCK", { section: info.section, parent: info.parent, index: i, x, y });
|
|
349
|
+
const buttons = box.row
|
|
350
|
+
? [
|
|
351
|
+
{ id: "b-before", x: box.left, y: box.top + box.height / 2, add: (e) => addAt(at, e.clientX, e.clientY) },
|
|
352
|
+
{ id: "b-after", x: box.left + box.width, y: box.top + box.height / 2, add: (e) => addAt(at + 1, e.clientX, e.clientY) },
|
|
353
|
+
]
|
|
354
|
+
: [
|
|
355
|
+
{ id: "b-before", x: box.left + box.width / 2, y: box.top, add: (e) => addAt(at, e.clientX, e.clientY) },
|
|
356
|
+
{ id: "b-after", x: box.left + box.width / 2, y: box.top + box.height, add: (e) => addAt(at + 1, e.clientX, e.clientY) },
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
// An empty group: "Add block" in the middle, into it.
|
|
360
|
+
if (isContainer(box.key) && !siblings(info.section, box.key).length) {
|
|
361
|
+
buttons.push({
|
|
362
|
+
id: "b-inside", label: "Add block", wide: true,
|
|
363
|
+
x: box.left + box.width / 2, y: box.top + box.height / 2,
|
|
364
|
+
add: (e) => send("ADD_BLOCK", { section: info.section, parent: box.key, index: 0, x: e.clientX, y: e.clientY }),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return buttons;
|
|
368
|
+
});
|
|
369
|
+
</script>
|
|
370
|
+
|
|
371
|
+
<template>
|
|
372
|
+
<!-- On <body>, not inside the page: the layout's wrappers can form their own
|
|
373
|
+
stacking context, and then no z-index lifts the overlay above the
|
|
374
|
+
site's sticky header. -->
|
|
375
|
+
<Teleport to="body">
|
|
376
|
+
<!-- Off from the dashboard's top bar: the page shows exactly as customers
|
|
377
|
+
see it. Clicking a block still selects it. -->
|
|
378
|
+
<div v-if="isPreview && overlayOn" class="osi-ov" data-osi-overlay>
|
|
379
|
+
<!-- the hovered block's parents -->
|
|
380
|
+
<div v-for="p in view.parents" :key="`p-${p.key}`" class="osi-ov-box is-parent" :style="boxStyle(p)" />
|
|
381
|
+
|
|
382
|
+
<!-- the other copies of the hovered / selected block -->
|
|
383
|
+
<div v-for="tw in view.twins" :key="`t-${tw.id}`" class="osi-ov-box is-twin" :style="boxStyle(tw)" />
|
|
384
|
+
|
|
385
|
+
<!-- hover -->
|
|
386
|
+
<template v-if="view.hover">
|
|
387
|
+
<div class="osi-ov-box is-hover" :style="boxStyle(view.hover)" />
|
|
388
|
+
<div class="osi-ov-tag is-hover" :style="tagStyle(view.hover, 1)">
|
|
389
|
+
<template v-for="(key, i) in hoverCrumbs" :key="`h-${key}`">
|
|
390
|
+
<span v-if="i" class="osi-ov-sep">›</span>
|
|
391
|
+
<span :class="{ 'is-self': i === hoverCrumbs.length - 1 }">{{ nameOf(key) }}</span>
|
|
392
|
+
</template>
|
|
393
|
+
<!-- <span v-if="hintOf(view.hover.key)" class="osi-ov-hint">{{ hintOf(view.hover.key) }}</span> -->
|
|
394
|
+
</div>
|
|
395
|
+
</template>
|
|
396
|
+
|
|
397
|
+
<!-- selected -->
|
|
398
|
+
<template v-if="view.selected">
|
|
399
|
+
<div class="osi-ov-box is-selected" :style="boxStyle(view.selected)" />
|
|
400
|
+
<div v-if="hoveringSelected" class="osi-ov-tag is-selected" :style="tagStyle(view.selected, 2)">
|
|
401
|
+
<template v-for="(key, i) in selectedCrumbs" :key="`s-${key}`">
|
|
402
|
+
<span v-if="i" class="osi-ov-sep">›</span>
|
|
403
|
+
<button
|
|
404
|
+
v-if="i < selectedCrumbs.length - 1"
|
|
405
|
+
type="button"
|
|
406
|
+
class="osi-ov-crumb"
|
|
407
|
+
:title="`Select ${nameOf(key)}`"
|
|
408
|
+
@click.stop="selectKey(key)"
|
|
409
|
+
>{{ nameOf(key) }}</button>
|
|
410
|
+
<span v-else class="is-self">{{ nameOf(key) }}</span>
|
|
411
|
+
</template>
|
|
412
|
+
<span v-if="!isFixed(view.selected.key)" class="osi-ov-tools">
|
|
413
|
+
<button type="button" title="Move up" @click.stop="act('up')">
|
|
414
|
+
<svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7" /></svg>
|
|
415
|
+
</button>
|
|
416
|
+
<button type="button" title="Move down" @click.stop="act('down')">
|
|
417
|
+
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12l7 7 7-7" /></svg>
|
|
418
|
+
</button>
|
|
419
|
+
<button type="button" title="Duplicate" @click.stop="act('duplicate')">
|
|
420
|
+
<svg viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="12" rx="2" /><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" /></svg>
|
|
421
|
+
</button>
|
|
422
|
+
<button
|
|
423
|
+
v-if="!isRequired(view.selected.key)"
|
|
424
|
+
type="button"
|
|
425
|
+
title="Delete"
|
|
426
|
+
class="is-danger"
|
|
427
|
+
@click.stop="act('delete')"
|
|
428
|
+
>
|
|
429
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-[15px] shrink-0"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"></path></svg>
|
|
430
|
+
</button>
|
|
431
|
+
</span>
|
|
432
|
+
</div>
|
|
433
|
+
</template>
|
|
434
|
+
|
|
435
|
+
<!-- "+" -->
|
|
436
|
+
<button
|
|
437
|
+
v-for="b in plusButtons"
|
|
438
|
+
:key="b.id"
|
|
439
|
+
type="button"
|
|
440
|
+
class="osi-ov-plus"
|
|
441
|
+
:class="{ 'is-wide': b.wide }"
|
|
442
|
+
:style="{ top: `${b.y}px`, left: `${b.x}px` }"
|
|
443
|
+
:title="b.label || 'Add block'"
|
|
444
|
+
@click.stop="b.add($event)"
|
|
445
|
+
>
|
|
446
|
+
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14" /></svg>
|
|
447
|
+
<span v-if="b.label">{{ b.label }}</span>
|
|
448
|
+
</button>
|
|
449
|
+
</div>
|
|
450
|
+
</Teleport>
|
|
451
|
+
</template>
|
|
452
|
+
|
|
453
|
+
<style scoped>
|
|
454
|
+
.osi-ov {
|
|
455
|
+
--ov-blue: #2c6ecb;
|
|
456
|
+
position: fixed;
|
|
457
|
+
inset: 0;
|
|
458
|
+
z-index: 2147483000;
|
|
459
|
+
pointer-events: none;
|
|
460
|
+
font: 500 12px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
461
|
+
}
|
|
462
|
+
.osi-ov-box {
|
|
463
|
+
position: fixed;
|
|
464
|
+
box-sizing: border-box;
|
|
465
|
+
pointer-events: none;
|
|
466
|
+
}
|
|
467
|
+
.osi-ov-box.is-parent {
|
|
468
|
+
border: 1px dashed rgba(44, 110, 203, 0.45);
|
|
469
|
+
}
|
|
470
|
+
.osi-ov-box.is-twin {
|
|
471
|
+
border: 1px solid rgba(44, 110, 203, 0.55);
|
|
472
|
+
background: rgba(44, 110, 203, 0.04);
|
|
473
|
+
}
|
|
474
|
+
.osi-ov-box.is-hover {
|
|
475
|
+
border: 1px solid var(--ov-blue);
|
|
476
|
+
}
|
|
477
|
+
.osi-ov-box.is-selected {
|
|
478
|
+
border: 2px solid var(--ov-blue);
|
|
479
|
+
}
|
|
480
|
+
.osi-ov-tag {
|
|
481
|
+
font-family: 'Poppins', sans-serif;
|
|
482
|
+
position: fixed;
|
|
483
|
+
height: 22px;
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: center;
|
|
486
|
+
gap: 4px;
|
|
487
|
+
max-width: calc(100vw - 8px);
|
|
488
|
+
padding: 0 8px;
|
|
489
|
+
border-radius: 4px;
|
|
490
|
+
background: var(--ov-blue);
|
|
491
|
+
color: #fff;
|
|
492
|
+
white-space: nowrap;
|
|
493
|
+
overflow: hidden;
|
|
494
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
495
|
+
}
|
|
496
|
+
.osi-ov-tag.is-hover {
|
|
497
|
+
pointer-events: none;
|
|
498
|
+
opacity: 0.95;
|
|
499
|
+
}
|
|
500
|
+
.osi-ov-tag.is-selected {
|
|
501
|
+
pointer-events: auto;
|
|
502
|
+
}
|
|
503
|
+
.osi-ov-sep {
|
|
504
|
+
opacity: 0.6;
|
|
505
|
+
}
|
|
506
|
+
.osi-ov-tag .is-self {
|
|
507
|
+
font-weight: 600;
|
|
508
|
+
}
|
|
509
|
+
.osi-ov-hint {
|
|
510
|
+
margin-inline-start: 4px;
|
|
511
|
+
opacity: 0.75;
|
|
512
|
+
font-weight: 400;
|
|
513
|
+
overflow: hidden;
|
|
514
|
+
text-overflow: ellipsis;
|
|
515
|
+
max-width: 160px;
|
|
516
|
+
}
|
|
517
|
+
.osi-ov-crumb {
|
|
518
|
+
all: unset;
|
|
519
|
+
cursor: pointer;
|
|
520
|
+
opacity: 0.85;
|
|
521
|
+
}
|
|
522
|
+
.osi-ov-crumb:hover,
|
|
523
|
+
.osi-ov-crumb:focus-visible {
|
|
524
|
+
opacity: 1;
|
|
525
|
+
text-decoration: underline;
|
|
526
|
+
}
|
|
527
|
+
.osi-ov-tools {
|
|
528
|
+
display: inline-flex;
|
|
529
|
+
gap: 2px;
|
|
530
|
+
margin-inline-start: 6px;
|
|
531
|
+
padding-inline-start: 6px;
|
|
532
|
+
border-inline-start: 1px solid rgba(255, 255, 255, 0.35);
|
|
533
|
+
}
|
|
534
|
+
.osi-ov-tools button {
|
|
535
|
+
all: unset;
|
|
536
|
+
display: grid;
|
|
537
|
+
place-items: center;
|
|
538
|
+
width: 18px;
|
|
539
|
+
height: 18px;
|
|
540
|
+
border-radius: 3px;
|
|
541
|
+
cursor: pointer;
|
|
542
|
+
}
|
|
543
|
+
.osi-ov-tools button:hover,
|
|
544
|
+
.osi-ov-tools button:focus-visible {
|
|
545
|
+
background: rgba(255, 255, 255, 0.2);
|
|
546
|
+
}
|
|
547
|
+
.osi-ov-tools button.is-danger:hover {
|
|
548
|
+
background: #d72c0d;
|
|
549
|
+
}
|
|
550
|
+
.osi-ov svg {
|
|
551
|
+
width: 12px;
|
|
552
|
+
height: 12px;
|
|
553
|
+
fill: none;
|
|
554
|
+
stroke: currentColor;
|
|
555
|
+
stroke-width: 2.2;
|
|
556
|
+
stroke-linecap: round;
|
|
557
|
+
stroke-linejoin: round;
|
|
558
|
+
}
|
|
559
|
+
.osi-ov-plus {
|
|
560
|
+
font-family: 'Poppins', sans-serif !important;
|
|
561
|
+
all: unset;
|
|
562
|
+
position: fixed;
|
|
563
|
+
transform: translate(-50%, -50%);
|
|
564
|
+
display: inline-flex;
|
|
565
|
+
align-items: center;
|
|
566
|
+
justify-content: center;
|
|
567
|
+
gap: 4px;
|
|
568
|
+
width: 22px;
|
|
569
|
+
height: 22px;
|
|
570
|
+
border-radius: 999px;
|
|
571
|
+
background: var(--ov-blue);
|
|
572
|
+
color: #fff;
|
|
573
|
+
cursor: pointer;
|
|
574
|
+
pointer-events: auto;
|
|
575
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
|
576
|
+
transition: transform 0.12s ease;
|
|
577
|
+
}
|
|
578
|
+
.osi-ov-plus.is-wide {
|
|
579
|
+
width: auto;
|
|
580
|
+
padding: 0 10px 0 8px;
|
|
581
|
+
}
|
|
582
|
+
</style>
|