<{* Customer input (product personalization): fields the customer fills in — e.g. an engraving name, a yes/no choice, a list or a file upload. Scalar fields carry data-bundle-variant so app.js submit() collects and required-validates them automatically; file inputs carry data-customer-file and are sent as multipart FormData. Values follow the order line. *}> <{$ca_profile = $product->getProfile()}> <{if $ca_profile}> <{$customer_attrs = []}> <{foreach $ca_profile->getAttributeList() as $ca}> <{if $ca->isCustomerInput()}> <{$customer_attrs[] = $ca}> <{/if}> <{/foreach}> <{if $customer_attrs}> <{* x-fragment lets app.js' replaceFragments() swap the server-rendered validation errors into the page after a rejected add-to-cart. Without it the fetch follows the redirect, the flash is consumed by a response nobody looks at, and the failed add looks like a success. The name is per product so two blocks for different products on the same page never overwrite each other. *}> <{* Only one product at a time carries a flash, and the redirect points at #attribute_error. The anchor goes on the FIRST field with an error so the browser lands on the error instead of the top of the page. *}> <{$ca_anchor_used = false}>
<{foreach $customer_attrs as $ca}> <{$ca_id = $ca->getAttributeId()}> <{$ca_error = ''}> <{if $attribute_errors && $attribute_errors.product_id == $product->getProductId() && isset($attribute_errors.errors[$ca_id])}> <{$ca_error = $attribute_errors.errors[$ca_id]}> <{/if}> <{* Refill: the flash belongs to ONE product, so the same product_id check as above — otherwise a value would leak onto a product card on the same page. values holds scalars only (a file cannot be refilled) and is missing entirely from an old flash written before deploy, hence isset(). *}> <{$ca_value = ''}> <{if $attribute_errors && $attribute_errors.product_id == $product->getProductId() && isset($attribute_errors.values[$ca_id])}> <{$ca_value = $attribute_errors.values[$ca_id]}> <{/if}>
id="attribute_error"<{$ca_anchor_used = true}><{/if}> style="margin-bottom:0.75rem;"> <{if $ca->getDataType() == 'boolean'}> <{elseif $ca->getDataType() == 'valuelist'}> <{elseif $ca->getDataType() == 'file'}> getIsRequired()}> required<{/if}> style="width:100%;font-size:0.875rem;">

<{t}>Max 10 MB — JPG, PNG, GIF, WEBP or PDF<{/t}>

<{* A file cannot be refilled by the browser, so the customer must pick it again *}> <{if $ca_error}>

<{t}>Please choose the file again<{/t}>

<{/if}> <{elseif $ca->getDataType() == 'integer'}> value="<{$ca_value|escape}>"<{/if}><{if $ca->getIsRequired()}> required<{/if}><{if $ca->getPlaceholder()}> placeholder="<{$ca->getPlaceholder()|escape}>"<{/if}> class="form-input" style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.375rem;"> <{else}> value="<{$ca_value|escape}>"<{/if}><{if $ca->getIsRequired()}> required<{/if}><{if $ca->getMaxLength()}> maxlength="<{$ca->getMaxLength()}>"<{/if}><{if $ca->getPlaceholder()}> placeholder="<{$ca->getPlaceholder()|escape}>"<{/if}> class="form-input" style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.375rem;"> <{/if}> <{if $ca_error}>

<{if $ca_error == 'required' || $ca_error == 'no_file'}><{t}>This field is required<{/t}> <{elseif $ca_error == 'too_long'}><{t}>The text is too long<{/t}> <{elseif $ca_error == 'too_large'}><{t}>The file is too large (max 10 MB)<{/t}> <{elseif $ca_error == 'invalid_type'}><{t}>File type not supported — use JPG, PNG, GIF, WEBP or PDF<{/t}> <{else}><{t}>Invalid value<{/t}><{/if}>

<{/if}>
<{/foreach}>
<{/if}> <{/if}>