@osimart/create-storefront 0.0.0-stage → 1.0.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/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.ts +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.ts +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 +259 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +43 -3
- 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,294 @@
|
|
|
1
|
+
import { SECTION_WIDTHS } from "#osi/composables/useSectionShell";
|
|
2
|
+
import { buttonClasses, buttonVars } from "#osi/composables/useButtonStyle";
|
|
3
|
+
import { shadow } from "#osi/theme/controls";
|
|
4
|
+
|
|
5
|
+
export default function useLogic({ props }) {
|
|
6
|
+
const { s, t, shellStyle, innerStyle, align, customClass, heading } =
|
|
7
|
+
useSectionShell(() => props.section);
|
|
8
|
+
const { form, isLoading, submitContact, autofillUser } = useContactForm();
|
|
9
|
+
const { showApiError } = useApiErrorToast();
|
|
10
|
+
|
|
11
|
+
const PAYLOAD_KEYS = ["first_name", "last_name", "email", "subject", "message"];
|
|
12
|
+
|
|
13
|
+
const DEFAULT_FIELDS = [
|
|
14
|
+
{ key: "first_name", label: "First name", type: "TEXT", width: "HALF", required: true },
|
|
15
|
+
{ key: "last_name", label: "Last name", type: "TEXT", width: "HALF", required: true },
|
|
16
|
+
{ key: "email", label: "Email", type: "EMAIL", width: "FULL", required: true },
|
|
17
|
+
{ key: "subject", label: "Subject", type: "TEXT", width: "FULL", required: false },
|
|
18
|
+
{ key: "message", label: "Message", type: "TEXTAREA", width: "FULL", required: true },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const fields = computed(() => {
|
|
22
|
+
const blocks = props.section.blocks ?? [];
|
|
23
|
+
if (!blocks.length) return DEFAULT_FIELDS;
|
|
24
|
+
|
|
25
|
+
return blocks.map((block, i) => ({
|
|
26
|
+
key: block.settings?.KEY || `custom_${i}`,
|
|
27
|
+
blockKey: block.key,
|
|
28
|
+
label: t(block.settings?.LABEL) || block.settings?.KEY || "Field",
|
|
29
|
+
placeholder: t(block.settings?.PLACEHOLDER),
|
|
30
|
+
type: block.settings?.FIELD_TYPE || "TEXT",
|
|
31
|
+
width: block.settings?.WIDTH || "FULL",
|
|
32
|
+
required: !!block.settings?.REQUIRED,
|
|
33
|
+
// OPTIONS has always been in the schema; nothing rendered it until now.
|
|
34
|
+
options: String(block.settings?.OPTIONS ?? "")
|
|
35
|
+
.split(",")
|
|
36
|
+
.map((option) => option.trim())
|
|
37
|
+
.filter(Boolean),
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Extra fields live here, not on `form` — useContactForm's state is shared and
|
|
42
|
+
// shaped by the API.
|
|
43
|
+
const extras = ref({});
|
|
44
|
+
const sent = ref(false);
|
|
45
|
+
const errors = ref({});
|
|
46
|
+
|
|
47
|
+
// Local, synchronous guard against double submission. Doesn't rely on
|
|
48
|
+
// useContactForm's `isLoading` timing — that's owned by the composable and
|
|
49
|
+
// there's no guarantee it flips to true before a second call to submit()
|
|
50
|
+
// can pass its own check, e.g. on a fast double-click.
|
|
51
|
+
const submitting = ref(false);
|
|
52
|
+
|
|
53
|
+
// PHONE is the FormFieldType schema actually stores; TEL is accepted too in
|
|
54
|
+
// case any existing block settings still carry the old (incorrect) token.
|
|
55
|
+
const isPhoneField = (type) => type === "PHONE" || type === "TEL";
|
|
56
|
+
|
|
57
|
+
function valueOf(field) {
|
|
58
|
+
return PAYLOAD_KEYS.includes(field.key)
|
|
59
|
+
? form.value[field.key]
|
|
60
|
+
: (extras.value[field.key] ?? "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function setValue(field, value) {
|
|
64
|
+
if (PAYLOAD_KEYS.includes(field.key)) form.value[field.key] = value;
|
|
65
|
+
else extras.value[field.key] = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const inputType = (type) =>
|
|
69
|
+
({ EMAIL: "email", NUMBER: "number", DATE: "date" })[type] ?? "text";
|
|
70
|
+
|
|
71
|
+
function validate() {
|
|
72
|
+
const next = {};
|
|
73
|
+
for (const field of fields.value) {
|
|
74
|
+
const value = String(valueOf(field) ?? "").trim();
|
|
75
|
+
if (field.required && !value) next[field.key] = `${field.label} is required.`;
|
|
76
|
+
else if (field.type === "EMAIL" && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
|
77
|
+
next[field.key] = "Enter a valid email address.";
|
|
78
|
+
}
|
|
79
|
+
errors.value = next;
|
|
80
|
+
return Object.keys(next).length === 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function submit() {
|
|
84
|
+
if (submitting.value || isLoading.value || !validate()) return;
|
|
85
|
+
submitting.value = true;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Append unmapped answers to the message so they reach the merchant.
|
|
89
|
+
const appended = Object.entries(extras.value)
|
|
90
|
+
.filter(([, value]) => String(value ?? "").trim())
|
|
91
|
+
.map(([key, value]) => {
|
|
92
|
+
const field = fields.value.find((f) => f.key === key);
|
|
93
|
+
return `${field?.label ?? key}: ${value}`;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const original = form.value.message;
|
|
97
|
+
if (appended.length) {
|
|
98
|
+
form.value.message = [original, "", ...appended].filter(Boolean).join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await submitContact();
|
|
103
|
+
sent.value = true;
|
|
104
|
+
extras.value = {};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// useContactForm already toasts a generic line; this surfaces field errors.
|
|
107
|
+
showApiError(error, "Couldn't send your message.");
|
|
108
|
+
form.value.message = original;
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
submitting.value = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onMounted(() => autofillUser());
|
|
116
|
+
|
|
117
|
+
/* ------------------------------------------------------------- appearance */
|
|
118
|
+
|
|
119
|
+
function num(value, fallback) {
|
|
120
|
+
const n = Number(value);
|
|
121
|
+
return Number.isFinite(n) ? n : fallback;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Empty string, null and undefined mean "not set" — 0 does not. */
|
|
125
|
+
function has(value) {
|
|
126
|
+
return value !== undefined && value !== null && value !== "";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Strips absent keys so an unset setting emits no CSS variable at all. */
|
|
130
|
+
function compact(object) {
|
|
131
|
+
return Object.fromEntries(Object.entries(object).filter(([, v]) => v !== undefined));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function pxVar(value) {
|
|
135
|
+
return has(value) && Number.isFinite(Number(value)) ? `${Number(value)}px` : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Picks value when it's one of allowed, else the first entry as the default. */
|
|
139
|
+
function oneOf(value, allowed) {
|
|
140
|
+
return allowed.includes(value) ? value : allowed[0];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const TEXT_TRANSFORMS = {
|
|
144
|
+
NONE: "none",
|
|
145
|
+
UPPERCASE: "uppercase",
|
|
146
|
+
LOWERCASE: "lowercase",
|
|
147
|
+
CAPITALIZE: "capitalize",
|
|
148
|
+
};
|
|
149
|
+
const FONT_WEIGHTS = { LIGHT: 300, REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
|
|
150
|
+
const FLEX_ALIGNMENTS = { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" };
|
|
151
|
+
const MARGINS = {
|
|
152
|
+
LEFT: { marginInline: "0 auto" },
|
|
153
|
+
CENTER: { marginInline: "auto" },
|
|
154
|
+
RIGHT: { marginInline: "auto 0" },
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Empty section settings follow the theme's Inputs group.
|
|
158
|
+
const { controls } = useThemeControls();
|
|
159
|
+
|
|
160
|
+
/** Old CONTACT_FORM ids -> the unified INPUT_* ids. */
|
|
161
|
+
const LEGACY_KEYS = {
|
|
162
|
+
INPUT_LABEL_STYLE: ["LABEL_STYLE"],
|
|
163
|
+
INPUT_SHOW_REQUIRED_MARK: ["SHOW_REQUIRED_MARK"],
|
|
164
|
+
INPUT_GAP: ["FIELD_GAP"],
|
|
165
|
+
INPUT_PLACEHOLDER_COLOR: ["PLACEHOLDER_COLOR"],
|
|
166
|
+
INPUT_ERROR_COLOR: ["ERROR_COLOR"],
|
|
167
|
+
INPUT_TEXT_COLOR: ["FIELD_TEXT_COLOR"],
|
|
168
|
+
INPUT_FONT_SIZE: ["FIELD_TEXT_SIZE"],
|
|
169
|
+
INPUT_FONT_SIZE_MOBILE: ["FIELD_TEXT_SIZE_MOBILE"],
|
|
170
|
+
INPUT_LABEL_SIZE: ["FIELD_LABEL_SIZE"],
|
|
171
|
+
INPUT_LABEL_WEIGHT: ["FIELD_LABEL_WEIGHT"],
|
|
172
|
+
INPUT_LABEL_TRANSFORM: ["FIELD_LABEL_TRANSFORM"],
|
|
173
|
+
INPUT_LABEL_LETTER_SPACING: ["FIELD_LABEL_LETTER_SPACING"],
|
|
174
|
+
INPUT_LABEL_COLOR: ["FIELD_LABEL_COLOR"],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** A field-styling setting: the unified id, else its pre-unification id. */
|
|
178
|
+
function field(key) {
|
|
179
|
+
for (const k of [key, ...(LEGACY_KEYS[key] ?? [])]) {
|
|
180
|
+
if (has(s.value[k])) return s.value[k];
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const pickOr = (value, allowed, fallback) => (allowed.includes(value) ? value : fallback);
|
|
185
|
+
const labelStyle = computed(() =>
|
|
186
|
+
pickOr(field("INPUT_LABEL_STYLE"), ["ABOVE", "FLOATING", "PLACEHOLDER"], controls.value.labelStyle)
|
|
187
|
+
);
|
|
188
|
+
const inputStyle = computed(() =>
|
|
189
|
+
pickOr(s.value.INPUT_STYLE, ["OUTLINE", "FILLED", "UNDERLINE"], controls.value.inputStyle)
|
|
190
|
+
);
|
|
191
|
+
const floating = computed(() => labelStyle.value === "FLOATING");
|
|
192
|
+
const showRequiredMark = computed(() => {
|
|
193
|
+
const value = field("INPUT_SHOW_REQUIRED_MARK");
|
|
194
|
+
return has(value) ? value !== false && value !== "false" : controls.value.showRequiredMark;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/** PhoneNumInput's own variant vocabulary; '' is its default boxed look. */
|
|
198
|
+
const phoneVariant = computed(() => (inputStyle.value === "UNDERLINE" ? "underline" : ""));
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A floating label needs to know whether the field is empty, which CSS reads
|
|
202
|
+
* from :placeholder-shown — so the placeholder has to be a single space, not
|
|
203
|
+
* the merchant's text. In the other two modes the placeholder behaves normally.
|
|
204
|
+
*/
|
|
205
|
+
function placeholderFor(field) {
|
|
206
|
+
if (floating.value) return " ";
|
|
207
|
+
if (labelStyle.value === "PLACEHOLDER") return field.placeholder || field.label;
|
|
208
|
+
return field.placeholder;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Selects and dates never report :placeholder-shown, so they float always. */
|
|
212
|
+
const alwaysFilled = (field) => field.type === "SELECT" || field.type === "DATE";
|
|
213
|
+
|
|
214
|
+
const outerMaxWidth = computed(
|
|
215
|
+
() => SECTION_WIDTHS[s.value.OUTER_CONTAINER] ?? SECTION_WIDTHS.FULL
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const styleVars = computed(() =>
|
|
219
|
+
compact({
|
|
220
|
+
"--osi-cf-outer-max-w": outerMaxWidth.value,
|
|
221
|
+
"--osi-cf-outer-my": `${num(s.value.OUTER_MARGIN_Y, 0)}px`,
|
|
222
|
+
// Inline margin can't be a real margin here: margin-inline:auto is what
|
|
223
|
+
// centres a constrained section, and a length on the same axis kills it.
|
|
224
|
+
// It shrinks the width instead, which auto then re-centres.
|
|
225
|
+
"--osi-cf-outer-mx": `${num(s.value.OUTER_MARGIN_X, 0)}px`,
|
|
226
|
+
|
|
227
|
+
// Field styling — same ids and meaning as the theme's Inputs group.
|
|
228
|
+
// Anything unset emits nothing, so the CSS falls through to --osi-input-*.
|
|
229
|
+
"--osi-cf-gap": pxVar(field("INPUT_GAP")),
|
|
230
|
+
|
|
231
|
+
"--osi-cf-pad-y": pxVar(field("INPUT_PADDING_Y")),
|
|
232
|
+
"--osi-cf-pad-x": pxVar(field("INPUT_PADDING_X")),
|
|
233
|
+
"--osi-cf-pad-y-mobile": pxVar(field("INPUT_PADDING_Y_MOBILE")),
|
|
234
|
+
"--osi-cf-pad-x-mobile": pxVar(field("INPUT_PADDING_X_MOBILE")),
|
|
235
|
+
|
|
236
|
+
"--osi-cf-radius": pxVar(field("INPUT_RADIUS")),
|
|
237
|
+
"--osi-cf-border-w": pxVar(field("INPUT_BORDER_WIDTH")),
|
|
238
|
+
"--osi-cf-shadow": has(field("INPUT_SHADOW"))
|
|
239
|
+
? shadow(field("INPUT_SHADOW"), field("INPUT_SHADOW_COLOR"))
|
|
240
|
+
: undefined,
|
|
241
|
+
|
|
242
|
+
"--osi-cf-text-fs": pxVar(field("INPUT_FONT_SIZE")),
|
|
243
|
+
"--osi-cf-text-fs-mobile": pxVar(field("INPUT_FONT_SIZE_MOBILE")),
|
|
244
|
+
|
|
245
|
+
"--osi-cf-input-bg": field("INPUT_BG_COLOR") || undefined,
|
|
246
|
+
"--osi-cf-text": field("INPUT_TEXT_COLOR") || undefined,
|
|
247
|
+
"--osi-cf-border": field("INPUT_BORDER_COLOR") || undefined,
|
|
248
|
+
"--osi-cf-focus": field("INPUT_FOCUS_COLOR") || undefined,
|
|
249
|
+
"--osi-cf-placeholder": field("INPUT_PLACEHOLDER_COLOR") || undefined,
|
|
250
|
+
"--osi-cf-error": field("INPUT_ERROR_COLOR") || undefined,
|
|
251
|
+
|
|
252
|
+
"--osi-cf-label-fs": pxVar(field("INPUT_LABEL_SIZE")),
|
|
253
|
+
"--osi-cf-label-fw": has(field("INPUT_LABEL_WEIGHT"))
|
|
254
|
+
? FONT_WEIGHTS[field("INPUT_LABEL_WEIGHT")] ?? num(field("INPUT_LABEL_WEIGHT"), 500)
|
|
255
|
+
: undefined,
|
|
256
|
+
"--osi-cf-label-tt": TEXT_TRANSFORMS[field("INPUT_LABEL_TRANSFORM")],
|
|
257
|
+
"--osi-cf-label-ls": has(field("INPUT_LABEL_LETTER_SPACING"))
|
|
258
|
+
? `${num(field("INPUT_LABEL_LETTER_SPACING"), 0) / 100}em`
|
|
259
|
+
: undefined,
|
|
260
|
+
"--osi-cf-label-color": field("INPUT_LABEL_COLOR") || undefined,
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const sectionStyle = computed(() => ({ ...shellStyle.value, ...styleVars.value }));
|
|
265
|
+
|
|
266
|
+
// FORM_MAX_WIDTH unset means "follow the section width" — the old stylesheet
|
|
267
|
+
// pinned 40rem here, which made CONTAINER do nothing on this section.
|
|
268
|
+
const formBoxStyle = computed(() =>
|
|
269
|
+
compact({
|
|
270
|
+
maxWidth: pxVar(s.value.FORM_MAX_WIDTH),
|
|
271
|
+
...MARGINS[oneOf(s.value.FORM_ALIGNMENT, ["CENTER", "LEFT", "RIGHT"])],
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const actionsStyle = computed(() => ({
|
|
276
|
+
justifyContent:
|
|
277
|
+
s.value.SUBMIT_WIDTH === "FULL"
|
|
278
|
+
? "stretch"
|
|
279
|
+
: FLEX_ALIGNMENTS[oneOf(s.value.SUBMIT_ALIGNMENT, ["LEFT", "CENTER", "RIGHT"])],
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
// SUBMIT_* are this section's own button settings. Empty ones follow the
|
|
283
|
+
// theme's Buttons group through .osi-btn.
|
|
284
|
+
const submitClasses = computed(() => buttonClasses(s.value, "SUBMIT", ["osi-cf-submit"]));
|
|
285
|
+
const submitStyle = computed(() => buttonVars(s.value, "SUBMIT"));
|
|
286
|
+
|
|
287
|
+
const rootClasses = computed(() => [
|
|
288
|
+
customClass.value,
|
|
289
|
+
`osi-cf--label-${labelStyle.value.toLowerCase()}`,
|
|
290
|
+
`osi-cf--input-${inputStyle.value.toLowerCase()}`,
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
return { s, t, shellStyle, innerStyle, align, customClass, heading, form, isLoading, submitContact, autofillUser, showApiError, PAYLOAD_KEYS, DEFAULT_FIELDS, fields, extras, sent, errors, submitting, isPhoneField, valueOf, setValue, inputType, validate, submit, num, has, compact, pxVar, oneOf, TEXT_TRANSFORMS, FONT_WEIGHTS, FLEX_ALIGNMENTS, MARGINS, controls, LEGACY_KEYS, field, pickOr, labelStyle, inputStyle, floating, showRequiredMark, phoneVariant, placeholderFor, alwaysFilled, outerMaxWidth, styleVars, sectionStyle, formBoxStyle, actionsStyle, submitClasses, submitStyle, rootClasses, shadow };
|
|
294
|
+
}
|