diff --git a/assets/alpine.min.js b/assets/alpine.min.js new file mode 100644 index 000000000..2ca48278f --- /dev/null +++ b/assets/alpine.min.js @@ -0,0 +1,5 @@ +(()=>{var rt=!1,nt=!1,U=[],it=-1;function qt(e){Cn(e)}function Cn(e){U.includes(e)||U.push(e),Tn()}function Ee(e){let t=U.indexOf(e);t!==-1&&t>it&&U.splice(t,1)}function Tn(){!nt&&!rt&&(rt=!0,queueMicrotask(Rn))}function Rn(){rt=!1,nt=!0;for(let e=0;ee.effect(t,{scheduler:r=>{ot?qt(r):r()}}),st=e.raw}function at(e){D=e}function Gt(e){let t=()=>{};return[n=>{let i=D(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),L(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=D(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>L(i)}var Jt=[],Yt=[],Xt=[];function Zt(e){Xt.push(e)}function ee(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Yt.push(t))}function Ae(e){Jt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function ct(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function Qt(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups.pop()()}var lt=new MutationObserver(pt),ut=!1;function le(){lt.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ut=!0}function ft(){Mn(),lt.disconnect(),ut=!1}var ce=[];function Mn(){let e=lt.takeRecords();ce.push(()=>e.length>0&&pt(e));let t=ce.length;queueMicrotask(()=>{if(ce.length===t)for(;ce.length>0;)ce.shift()()})}function _(e){if(!ut)return e();ft();let t=e();return le(),t}var dt=!1,Se=[];function er(){dt=!0}function tr(){dt=!1,pt(Se),Se=[]}function pt(e){if(dt){Se=Se.concat(e);return}let t=new Set,r=new Set,n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.add(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.add(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ct(s,o)}),n.forEach((o,s)=>{Jt.forEach(a=>a(s,o))});for(let o of r)t.has(o)||Yt.forEach(s=>s(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.has(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,Xt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Ce(e){return F(j(e))}function P(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?j(e.host):e.parentNode?j(e.parentNode):[]}function F(e){return new Proxy({objects:e},Nn)}var Nn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Dn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Dn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>Pn(n,i),s=>mt(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function Pn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function mt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),mt(e[t[0]],t.slice(1),r)}}var rr={};function y(e,t){rr[e]=t}function ue(e,t){return Object.entries(rr).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=_t(t);return i={interceptor:Re,...s},ee(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function nr(e,t,r,...n){try{return r(...n)}catch(i){te(i,e,t)}}function te(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} + +${r?'Expression: "'+r+`" + +`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function De(e){let t=Me;Me=!1;let r=e();return Me=t,r}function M(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return ir(...e)}var ir=gt;function or(e){ir=e}function gt(e,t){let r={};ue(r,e);let n=[r,...j(e)],i=typeof t=="function"?In(n,t):Ln(n,t,e);return nr.bind(null,e,t,i)}function In(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);Ne(r,o)}}var ht={};function kn(e,t){if(ht[e])return ht[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return te(s,t,e),Promise.resolve()}})();return ht[e]=o,o}function Ln(e,t,r){let n=kn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>te(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>te(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>te(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var bt="x-";function C(e=""){return bt+e}function sr(e){bt=e}var Pe={};function d(e,t){return Pe[e]=t,{before(r){if(!Pe[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=W.indexOf(r);W.splice(n>=0?n:W.indexOf("DEFAULT"),0,e)}}}function ar(e){return Object.keys(Pe).includes(e)}function de(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=wt(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(ur((o,s)=>n[o]=s)).filter(dr).map(jn(n,r)).sort(Fn).map(o=>$n(e,o))}function wt(e){return Array.from(e).map(ur()).filter(t=>!dr(t))}var xt=!1,fe=new Map,cr=Symbol();function lr(e){xt=!0;let t=Symbol();cr=t,fe.set(t,[]);let r=()=>{for(;fe.get(t).length;)fe.get(t).shift()();fe.delete(t)},n=()=>{xt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Gt(e);return t.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:M.bind(M,e)},()=>t.forEach(a=>a())]}function $n(e,t){let r=()=>{},n=Pe[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),xt?fe.get(cr).push(n):n())};return s.runCleanups=o,s}var Ie=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),ke=e=>e;function ur(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=fr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var fr=[];function re(e){fr.push(e)}function dr({name:e}){return pr().test(e)}var pr=()=>new RegExp(`^${bt}([^:^.]+)\\b`);function jn(e,t){return({name:r,value:n})=>{let i=r.match(pr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var yt="DEFAULT",W=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",yt,"teleport"];function Fn(e,t){let r=W.indexOf(e.type)===-1?yt:e.type,n=W.indexOf(t.type)===-1?yt:t.type;return W.indexOf(r)-W.indexOf(n)}function G(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function T(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>T(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)T(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var mr=!1;function _r(){mr&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),mr=!0,document.body||E("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `` **antes** de `alpine.min.js` (mismo orden que `orn-hero.js`). + +### 2. Schema de la sección (`orn-services`) + +Settings de sección (todos metafield-safe): + +| id | type | notas | +|----|------|-------| +| `anchor_id` | `text` | id del `
`; usado como `#anchor`. Default vacío → fallback a `section.id`. | +| `section_title` | `inline_richtext` | "Servicios" (H2). | +| `section_subtitle` | `inline_richtext` | "Un acompañamiento de principio a fin". | +| `cta_text` | `text` | Texto del CTA inferior ("Hablemos de tu proyecto"). | +| `cta_url` | `url` | Destino del CTA. | +| `cta_target` | `select` (`same` / `new` / `contact_form`) | Reemplaza el `checkbox` para cumplir metafields. `contact_form` abre modal/anchor a `#contact`. | +| `cta_variant` | `select` (`primary` / `secondary` / `tertiary`) | Estilo del DS. | +| `cta_icon_svg` | `image_picker` | SVG opcional como imagen (evita `textarea`). | +| `color_scheme` | `color_scheme` | Default `scheme-1`. | +| `enable_swipe_mobile` | `select` (`yes` / `no`) | Reemplaza `checkbox`. Default `yes`. | + +`max_blocks: 4`. `presets` carga 4 tabs vacíos para que el editor los vea de entrada. + +`enabled_on.templates: ["index","page","product","collection","blog","article"]` para reutilización. + +### 3. Schema del bloque (`_orn-services-tab`) + +Settings (metafield-safe): + +| id | type | notas | +|----|------|-------| +| `tab_label_desktop` | `text` | "SEO", "Performance"… | +| `tab_number_icon` | `image_picker` | SVG `01`, `02`… (mobile y badge desktop). | +| `tab_title` | `inline_richtext` | Título grande izquierdo (Hn configurable). | +| `tab_heading_level` | `select` (`h2`/`h3`/`h4`) | Default `h3`. | +| `tab_description` | `richtext` | Máx 1 línea desktop / 3 mobile (CSS `-webkit-line-clamp`). | +| `tab_image` | `image_picker` | Ratio 1:1, círculo grande izquierdo. | +| `panel_title` | `inline_richtext` | "Consultoría y estrategia". | +| `panel_description` | `richtext` | Una línea desktop / 3 mobile. | +| **Items 1..8** (grupo repetido por número) | — | El schema declara 8 sets de settings con `header` separadores. | +| `item_N_icon` | `image_picker` | SVG icono. | +| `item_N_text` | `richtext` | 2 líneas desktop, 2 mobile (clamp). | +| `item_N_cta_text` | `text` | Opcional. | +| `item_N_cta_url` | `url` | Opcional. | +| `item_N_cta_target` | `select` (`same`/`new`/`contact_form`) | Reemplaza `checkbox`. | +| `item_N_cta_variant` | `select` (`primary`/`secondary`/`tertiary`) | Estilo DS. | +| `item_N_cta_icon` | `image_picker` | SVG. | + +> **Justificación del item-as-flat-settings**: Shopify no soporta bloques anidados dentro de un block de sección con `content_for 'blocks'` de manera limpia (los `@theme` blocks rompen la compatibilidad con presets metafield). 8 grupos planos con `header` separadores son el patrón standard del theme y mantienen DX aceptable para 8 ítems máx. + +### 4. Markup (resumen) + +```liquid +
+

{{ section_title }}

+

{{ section_subtitle }}

+ +
+ + + + {%- for block in section.blocks -%} + + {%- endfor -%} +
+ +
+ {% content_for 'blocks' %} +
+ + {%- if cta_text != blank -%} + + {{ cta_text }} + {%- if cta_icon_svg -%}{{ cta_icon_svg | image_url: width: 24 | image_tag: alt: '' }}{%- endif -%} + + {%- endif -%} +
+``` + +Cada panel (block): +```liquid +
+ ... +
+``` + +### 5. Componente Alpine `ornServices` + +State: +```js +{ + active: 0, + prevActive: 0, + flashing: false, + indicatorStyle: '', + flashStyle: '', + total: 0, + swipeMobile: true, + touchStartX: 0, + touchDeltaX: 0, +} +``` + +API: +- `init()` → mide la posición/ancho del tab activo, hidrata `indicatorStyle`, registra `ResizeObserver` para recalcular en breakpoints. +- `goTo(i)` → actualiza `prevActive`, `active`, recalcula `indicatorStyle`, dispara `flash(i)`, mueve foco al tab si la acción vino de teclado, y llama `scrollPanelIntoView()` en mobile si hubo swipe. +- `flash(i)` → setea `flashStyle` con coords del tab destino, `flashing = true`, y a los 350ms `flashing = false` (animación CSS de opacidad+escala, ver §6). +- `onTabKey(e, i)` → soporta `ArrowLeft`/`ArrowRight` (con wrap), `Home`, `End`. Llama `goTo` y mueve foco con `x-ref`. +- `onTouchStart/Move/End` → en `swipeMobile && viewport <= md`. Threshold 50px. `End` decide `goTo(active±1)` con clamp. +- `prefersReducedMotion()` → si `matchMedia('(prefers-reduced-motion: reduce)').matches`, salta la transición del indicador (set instantáneo) y omite el flash. + +### 6. CSS — Magic Indicator + +- `.orn-services__tablist` es `position: relative` con `display: flex`, `border-radius: 999px`, borde 1px primary. +- `.orn-services__indicator`: `position: absolute; inset: 4px auto 4px 0; border-radius: 999px; background: var(--color-primary, #0D4F4A); transition: transform .35s cubic-bezier(.65,.05,.35,1), width .35s cubic-bezier(.65,.05,.35,1);`. Su `transform: translateX(N px)` y `width` se setean inline desde `indicatorStyle`. +- `.orn-services__flash`: `position: absolute; pointer-events: none; border-radius: 50%; background: radial-gradient(circle, rgba(78,192,222,.55) 0%, rgba(78,192,222,0) 70%); opacity: 0; transform: scale(.4);`. Clase `.is-flashing` aplica `@keyframes orn-flash` (200ms in, 150ms out): opacidad 0→.9→0, scale .4→1.1. +- `.orn-services__tab.is-active` invierte color de texto al neutro 01. +- Tabs: `.orn-services__tab-label` visible ≥ md; `.orn-services__tab-number` visible < md (toggle por `display` en media query). +- Paneles: `.orn-services__panel` con `opacity .25s ease, transform .25s ease`; activo `opacity:1; transform:none`; inactivos `opacity:0; transform:translateY(8px); pointer-events:none`. `[hidden]` aplica tras 250ms vía `setTimeout` opcional (o usamos `aria-hidden`+`visibility:hidden` para que la transición de salida se vea). +- `prefers-reduced-motion`: anula transitions del indicador, panel y flash. +- `scroll-margin-top: var(--header-height, 80px)` en `.orn-services` para anclas. + +### 7. Accesibilidad + +- `role="tablist"` con `aria-label`, hijos `role="tab"` con `aria-selected`, `aria-controls`, `tabindex="0/-1"` (roving tabindex). +- Paneles `role="tabpanel"`, `aria-labelledby`, `hidden` cuando inactivo. +- Foco visible: outline 2px `#4ec0de` (acento) sobre el tab. +- Teclado: ←/→ navega tabs (con wrap), Home/End saltan al primero/último, Enter/Space activan (default de ` + {%- endfor -%} + + {%- endif -%} + + +
+{%- endif -%} + +{% stylesheet %} + /* intentionally empty — see assets/orn-hero.css for full styles */ +{% endstylesheet %} + +{% schema %} +{ + "name": "Ornevo hero", + "class": "orn-hero-section", + "tag": "div", + "blocks": [ + { "type": "_orn-hero-slide" } + ], + "max_blocks": 5, + "settings": [ + { + "type": "text", + "id": "anchor_id", + "label": "ID de ancla", + "info": "Se puede añadir a la URL como #id. Ej: ornevo.fr#hero" + }, + { + "type": "number", + "id": "autoplay_interval", + "label": "Temporizador autoplay (ms)", + "info": "Vacío o 0 desactiva el cambio automático." + }, + { + "type": "text", + "id": "aria_label", + "label": "ARIA label de la sección", + "default": "Hero carousel" + }, + { + "type": "color_scheme", + "id": "color_scheme", + "label": "Esquema de color", + "default": "scheme-3" + } + ], + "presets": [ + { + "name": "Ornevo hero", + "blocks": [ + { "type": "_orn-hero-slide" } + ] + } + ] +} +{% endschema %} diff --git a/sections/orn-services.liquid b/sections/orn-services.liquid new file mode 100644 index 000000000..c89ad0eed --- /dev/null +++ b/sections/orn-services.liquid @@ -0,0 +1,717 @@ +{%- liquid + assign valid_blocks_count = section.blocks.size + + assign anchor_id = section.settings.anchor_id | handleize + if anchor_id == blank + assign anchor_id = section.id + endif + + assign color_scheme = section.settings.color_scheme | default: 'scheme-1' + + assign enable_swipe = true + if section.settings.enable_swipe_mobile == 'no' + assign enable_swipe = false + endif + + assign cta_resolved_url = section.settings.cta_url + if section.settings.cta_target == 'contact_form' + assign cta_resolved_url = '#contact' + endif + + capture inline_color_vars + if section.settings.nav_bg != blank + echo '--orn-services-nav-bg:' | append: section.settings.nav_bg | append: ';' + endif + if section.settings.nav_border != blank + echo '--orn-services-nav-border:' | append: section.settings.nav_border | append: ';' + endif + if section.settings.nav_active_bg != blank + echo '--orn-services-indicator-bg:' | append: section.settings.nav_active_bg | append: ';' + endif + if section.settings.nav_active_text != blank + echo '--orn-services-active-tab-color:' | append: section.settings.nav_active_text | append: ';' + endif + if section.settings.nav_inactive_text != blank + echo '--orn-services-inactive-tab-color:' | append: section.settings.nav_inactive_text | append: ';' + endif + if section.settings.flash_color != blank + echo '--orn-services-flash-color:' | append: section.settings.flash_color | append: ';' + endif + if section.settings.item_bullet_bg != blank + echo '--orn-services-item-bullet-bg:' | append: section.settings.item_bullet_bg | append: ';' + endif + if section.settings.progress_active != blank + echo '--orn-services-progress-active:' | append: section.settings.progress_active | append: ';' + endif + if section.settings.progress_inactive != blank + echo '--orn-services-progress-inactive:' | append: section.settings.progress_inactive | append: ';' + endif + endcapture +-%} + +{%- if valid_blocks_count > 0 -%} +
+
+ + {%- if section.settings.section_title != blank or section.settings.section_subtitle != blank -%} +
+ {%- if section.settings.section_title != blank -%} +

+ {{ section.settings.section_title }} +

+ {%- endif -%} + {%- if section.settings.section_subtitle != blank -%} +

{{ section.settings.section_subtitle }}

+ {%- endif -%} +
+ {%- endif -%} + + {%- comment -%} Tablist (buttons) — forloop.index0 is the source of truth for indexing {%- endcomment -%} +
+ + + + {%- for block in section.blocks -%} + {%- liquid + assign idx = forloop.index0 + assign pos = forloop.index + assign num = pos | prepend: '0' + assign tab_label = block.settings.tab_label_desktop | default: num + -%} + + {%- endfor -%} +
+ + {%- comment -%} Panels — same forloop scope, so forloop.index0 matches the buttons {%- endcomment -%} +
+ {%- for block in section.blocks -%} + {%- liquid + assign idx = forloop.index0 + assign pos = forloop.index + assign num = pos | prepend: '0' + + assign panel_title = block.settings.panel_title + assign panel_description = block.settings.panel_description + assign heading_tag = block.settings.panel_heading_level | default: 'h3' + assign panel_image = block.settings.panel_image + -%} +
+
+ + {%- if panel_title != blank or panel_description != blank -%} +
+ {%- if panel_title != blank -%} + <{{ heading_tag }} class="orn-services__service-title">{{ panel_title }} + {%- endif -%} + {%- if panel_description != blank -%} +
{{ panel_description }}
+ {%- endif -%} +
+ {%- endif -%} + + {%- if panel_image != blank -%} +
+ {{ + panel_image + | image_url: width: 600 + | image_tag: + class: 'orn-services__panel-image', + alt: panel_title, + loading: 'lazy', + width: panel_image.width, + height: panel_image.height, + widths: '300, 450, 600', + sizes: '(min-width: 750px) 280px, 90vw' + }} +
+ {%- endif -%} + +
+ {%- for i in (1..8) -%} + {%- liquid + assign txt_key = 'item_' | append: i | append: '_text' + assign icon_key = 'item_' | append: i | append: '_icon' + assign cta_text_key = 'item_' | append: i | append: '_cta_text' + assign cta_url_key = 'item_' | append: i | append: '_cta_url' + assign cta_tgt_key = 'item_' | append: i | append: '_cta_target' + assign cta_var_key = 'item_' | append: i | append: '_cta_variant' + assign cta_icon_key = 'item_' | append: i | append: '_cta_icon' + + assign item_text = block.settings[txt_key] + assign item_icon = block.settings[icon_key] + assign item_cta_text = block.settings[cta_text_key] + assign item_cta_url = block.settings[cta_url_key] + assign item_cta_tgt = block.settings[cta_tgt_key] + assign item_cta_var = block.settings[cta_var_key] | default: 'tertiary' + assign item_cta_icon = block.settings[cta_icon_key] + + assign resolved_url = item_cta_url + if item_cta_tgt == 'contact_form' + assign resolved_url = '#contact' + endif + -%} + + {%- if item_text != blank -%} +
+ {%- if item_icon != blank -%} + + {%- endif -%} + +
+ {%- endif -%} + {%- endfor -%} +
+ +
+
+ {%- endfor -%} +
+ + {%- comment -%} Mobile-only swipe progress (pill bars). Hidden on desktop via CSS. {%- endcomment -%} + + + {%- if section.settings.cta_text != blank -%} + + {%- endif -%} + +
+
+{%- endif -%} + +{% stylesheet %} + /* intentionally empty — see assets/orn-services.css for full styles */ +{% endstylesheet %} + +{% schema %} +{ + "name": "Ornevo services", + "class": "orn-services-section", + "tag": "div", + "blocks": [ + { + "type": "tab", + "name": "Ornevo services tab", + "settings": [ + { "type": "header", "content": "Tab (navegación)" }, + { + "type": "text", + "id": "tab_label_desktop", + "label": "Etiqueta accesible", + "info": "Texto leído por lectores de pantalla. Visualmente el tab muestra el número (01, 02…)." + }, + { + "type": "image_picker", + "id": "tab_icon_number", + "label": "Icono SVG del número (opcional)", + "info": "SVG con 01/02/03/04. Si está vacío se usa el número de posición." + }, + + { "type": "header", "content": "Panel — encabezado" }, + { + "type": "inline_richtext", + "id": "panel_title", + "label": "Título del panel", + "info": "Ej: Consultoría y estrategia" + }, + { + "type": "select", + "id": "panel_heading_level", + "label": "Nivel de encabezado", + "options": [ + { "value": "h2", "label": "H2" }, + { "value": "h3", "label": "H3" }, + { "value": "h4", "label": "H4" } + ], + "default": "h3" + }, + { + "type": "richtext", + "id": "panel_description", + "label": "Descripción del panel", + "info": "Máx 1 línea desktop / 3 líneas móvil (clamp por CSS)." + }, + { + "type": "image_picker", + "id": "panel_image", + "label": "Imagen circular (ratio 1:1)", + "info": "Mín. 600×600 px recomendado." + }, + + { "type": "header", "content": "Servicio 1" }, + { "type": "image_picker", "id": "item_1_icon", "label": "Icono SVG (1:1)" }, + { "type": "richtext", "id": "item_1_text", "label": "Texto", "info": "Máx 2 líneas." }, + { "type": "text", "id": "item_1_cta_text", "label": "CTA · Texto" }, + { "type": "url", "id": "item_1_cta_url", "label": "CTA · URL" }, + { + "type": "select", + "id": "item_1_cta_target", + "label": "CTA · Abrir en", + "options": [ + { "value": "same", "label": "Misma pestaña" }, + { "value": "new", "label": "Nueva pestaña" }, + { "value": "contact_form", "label": "Formulario de contacto" } + ], + "default": "same" + }, + { + "type": "select", + "id": "item_1_cta_variant", + "label": "CTA · Estilo", + "options": [ + { "value": "primary", "label": "Primary" }, + { "value": "secondary", "label": "Secondary" }, + { "value": "tertiary", "label": "Tertiary" } + ], + "default": "tertiary" + }, + { "type": "image_picker", "id": "item_1_cta_icon", "label": "CTA · Icono SVG" }, + + { "type": "header", "content": "Servicio 2" }, + { "type": "image_picker", "id": "item_2_icon", "label": "Icono SVG (1:1)" }, + { "type": "richtext", "id": "item_2_text", "label": "Texto" }, + { "type": "text", "id": "item_2_cta_text", "label": "CTA · Texto" }, + { "type": "url", "id": "item_2_cta_url", "label": "CTA · URL" }, + { + "type": "select", + "id": "item_2_cta_target", + "label": "CTA · Abrir en", + "options": [ + { "value": "same", "label": "Misma pestaña" }, + { "value": "new", "label": "Nueva pestaña" }, + { "value": "contact_form", "label": "Formulario de contacto" } + ], + "default": "same" + }, + { + "type": "select", + "id": "item_2_cta_variant", + "label": "CTA · Estilo", + "options": [ + { "value": "primary", "label": "Primary" }, + { "value": "secondary", "label": "Secondary" }, + { "value": "tertiary", "label": "Tertiary" } + ], + "default": "tertiary" + }, + { "type": "image_picker", "id": "item_2_cta_icon", "label": "CTA · Icono SVG" }, + + { "type": "header", "content": "Servicio 3" }, + { "type": "image_picker", "id": "item_3_icon", "label": "Icono SVG (1:1)" }, + { "type": "richtext", "id": "item_3_text", "label": "Texto" }, + { "type": "text", "id": "item_3_cta_text", "label": "CTA · Texto" }, + { "type": "url", "id": "item_3_cta_url", "label": "CTA · URL" }, + { + "type": "select", + "id": "item_3_cta_target", + "label": "CTA · Abrir en", + "options": [ + { "value": "same", "label": "Misma pestaña" }, + { "value": "new", "label": "Nueva pestaña" }, + { "value": "contact_form", "label": "Formulario de contacto" } + ], + "default": "same" + }, + { + "type": "select", + "id": "item_3_cta_variant", + "label": "CTA · Estilo", + "options": [ + { "value": "primary", "label": "Primary" }, + { "value": "secondary", "label": "Secondary" }, + { "value": "tertiary", "label": "Tertiary" } + ], + "default": "tertiary" + }, + { "type": "image_picker", "id": "item_3_cta_icon", "label": "CTA · Icono SVG" }, + + { "type": "header", "content": "Servicio 4" }, + { "type": "image_picker", "id": "item_4_icon", "label": "Icono SVG (1:1)" }, + { "type": "richtext", "id": "item_4_text", "label": "Texto" }, + { "type": "text", "id": "item_4_cta_text", "label": "CTA · Texto" }, + { "type": "url", "id": "item_4_cta_url", "label": "CTA · URL" }, + { + "type": "select", + "id": "item_4_cta_target", + "label": "CTA · Abrir en", + "options": [ + { "value": "same", "label": "Misma pestaña" }, + { "value": "new", "label": "Nueva pestaña" }, + { "value": "contact_form", "label": "Formulario de contacto" } + ], + "default": "same" + }, + { + "type": "select", + "id": "item_4_cta_variant", + "label": "CTA · Estilo", + "options": [ + { "value": "primary", "label": "Primary" }, + { "value": "secondary", "label": "Secondary" }, + { "value": "tertiary", "label": "Tertiary" } + ], + "default": "tertiary" + }, + { "type": "image_picker", "id": "item_4_cta_icon", "label": "CTA · Icono SVG" }, + + { "type": "header", "content": "Servicio 5" }, + { "type": "image_picker", "id": "item_5_icon", "label": "Icono SVG (1:1)" }, + { "type": "richtext", "id": "item_5_text", "label": "Texto" }, + { "type": "text", "id": "item_5_cta_text", "label": "CTA · Texto" }, + { "type": "url", "id": "item_5_cta_url", "label": "CTA · URL" }, + { + "type": "select", + "id": "item_5_cta_target", + "label": "CTA · Abrir en", + "options": [ + { "value": "same", "label": "Misma pestaña" }, + { "value": "new", "label": "Nueva pestaña" }, + { "value": "contact_form", "label": "Formulario de contacto" } + ], + "default": "same" + }, + { + "type": "select", + "id": "item_5_cta_variant", + "label": "CTA · Estilo", + "options": [ + { "value": "primary", "label": "Primary" }, + { "value": "secondary", "label": "Secondary" }, + { "value": "tertiary", "label": "Tertiary" } + ], + "default": "tertiary" + }, + { "type": "image_picker", "id": "item_5_cta_icon", "label": "CTA · Icono SVG" }, + + { "type": "header", "content": "Servicio 6" }, + { "type": "image_picker", "id": "item_6_icon", "label": "Icono SVG (1:1)" }, + { "type": "richtext", "id": "item_6_text", "label": "Texto" }, + { "type": "text", "id": "item_6_cta_text", "label": "CTA · Texto" }, + { "type": "url", "id": "item_6_cta_url", "label": "CTA · URL" }, + { + "type": "select", + "id": "item_6_cta_target", + "label": "CTA · Abrir en", + "options": [ + { "value": "same", "label": "Misma pestaña" }, + { "value": "new", "label": "Nueva pestaña" }, + { "value": "contact_form", "label": "Formulario de contacto" } + ], + "default": "same" + }, + { + "type": "select", + "id": "item_6_cta_variant", + "label": "CTA · Estilo", + "options": [ + { "value": "primary", "label": "Primary" }, + { "value": "secondary", "label": "Secondary" }, + { "value": "tertiary", "label": "Tertiary" } + ], + "default": "tertiary" + }, + { "type": "image_picker", "id": "item_6_cta_icon", "label": "CTA · Icono SVG" }, + + { "type": "header", "content": "Servicio 7" }, + { "type": "image_picker", "id": "item_7_icon", "label": "Icono SVG (1:1)" }, + { "type": "richtext", "id": "item_7_text", "label": "Texto" }, + { "type": "text", "id": "item_7_cta_text", "label": "CTA · Texto" }, + { "type": "url", "id": "item_7_cta_url", "label": "CTA · URL" }, + { + "type": "select", + "id": "item_7_cta_target", + "label": "CTA · Abrir en", + "options": [ + { "value": "same", "label": "Misma pestaña" }, + { "value": "new", "label": "Nueva pestaña" }, + { "value": "contact_form", "label": "Formulario de contacto" } + ], + "default": "same" + }, + { + "type": "select", + "id": "item_7_cta_variant", + "label": "CTA · Estilo", + "options": [ + { "value": "primary", "label": "Primary" }, + { "value": "secondary", "label": "Secondary" }, + { "value": "tertiary", "label": "Tertiary" } + ], + "default": "tertiary" + }, + { "type": "image_picker", "id": "item_7_cta_icon", "label": "CTA · Icono SVG" }, + + { "type": "header", "content": "Servicio 8" }, + { "type": "image_picker", "id": "item_8_icon", "label": "Icono SVG (1:1)" }, + { "type": "richtext", "id": "item_8_text", "label": "Texto" }, + { "type": "text", "id": "item_8_cta_text", "label": "CTA · Texto" }, + { "type": "url", "id": "item_8_cta_url", "label": "CTA · URL" }, + { + "type": "select", + "id": "item_8_cta_target", + "label": "CTA · Abrir en", + "options": [ + { "value": "same", "label": "Misma pestaña" }, + { "value": "new", "label": "Nueva pestaña" }, + { "value": "contact_form", "label": "Formulario de contacto" } + ], + "default": "same" + }, + { + "type": "select", + "id": "item_8_cta_variant", + "label": "CTA · Estilo", + "options": [ + { "value": "primary", "label": "Primary" }, + { "value": "secondary", "label": "Secondary" }, + { "value": "tertiary", "label": "Tertiary" } + ], + "default": "tertiary" + }, + { "type": "image_picker", "id": "item_8_cta_icon", "label": "CTA · Icono SVG" } + ] + } + ], + "max_blocks": 4, + "enabled_on": { + "templates": ["index", "page", "product", "collection", "blog", "article"] + }, + "settings": [ + { + "type": "header", + "content": "Ancla y accesibilidad" + }, + { + "type": "text", + "id": "anchor_id", + "label": "ID de ancla", + "info": "Se añade a la URL como #id. Ej: servicios → ornevo.fr#servicios" + }, + { + "type": "header", + "content": "Encabezado" + }, + { + "type": "inline_richtext", + "id": "section_title", + "label": "Título de la sección", + "default": "Servicios" + }, + { + "type": "inline_richtext", + "id": "section_subtitle", + "label": "Subtítulo de la sección", + "default": "Un acompañamiento de principio a fin" + }, + { + "type": "header", + "content": "CTA sección" + }, + { + "type": "text", + "id": "cta_text", + "label": "Texto del CTA", + "default": "Hablemos de tu proyecto" + }, + { + "type": "url", + "id": "cta_url", + "label": "URL del CTA" + }, + { + "type": "select", + "id": "cta_target", + "label": "Abrir en", + "options": [ + { "value": "same", "label": "Misma pestaña" }, + { "value": "new", "label": "Nueva pestaña" }, + { "value": "contact_form", "label": "Formulario de contacto (#contact)" } + ], + "default": "same" + }, + { + "type": "select", + "id": "cta_variant", + "label": "Estilo del CTA", + "options": [ + { "value": "primary", "label": "Primary" }, + { "value": "secondary", "label": "Secondary" }, + { "value": "tertiary", "label": "Tertiary" } + ], + "default": "primary" + }, + { + "type": "image_picker", + "id": "cta_icon_svg", + "label": "Icono del CTA (SVG)" + }, + { + "type": "header", + "content": "Comportamiento" + }, + { + "type": "select", + "id": "enable_swipe_mobile", + "label": "Navegación por swipe en móvil", + "options": [ + { "value": "yes", "label": "Activado" }, + { "value": "no", "label": "Desactivado" } + ], + "default": "yes" + }, + { + "type": "header", + "content": "Apariencia" + }, + { + "type": "color_scheme", + "id": "color_scheme", + "label": "Esquema de color", + "default": "scheme-1" + }, + { + "type": "header", + "content": "Colores · Navegación de tabs" + }, + { + "type": "color", + "id": "nav_bg", + "label": "Fondo del nav", + "info": "Vacío = transparente." + }, + { + "type": "color", + "id": "nav_border", + "label": "Borde del nav" + }, + { + "type": "color", + "id": "nav_active_bg", + "label": "Fondo de la píldora activa" + }, + { + "type": "color", + "id": "nav_active_text", + "label": "Texto del tab activo" + }, + { + "type": "color", + "id": "nav_inactive_text", + "label": "Texto de los tabs inactivos" + }, + { + "type": "color", + "id": "flash_color", + "label": "Color del destello (flash)" + }, + { + "type": "header", + "content": "Colores · Items y progress" + }, + { + "type": "color", + "id": "item_bullet_bg", + "label": "Fondo del círculo del icono (items)" + }, + { + "type": "color", + "id": "progress_active", + "label": "Indicador de swipe · activo" + }, + { + "type": "color", + "id": "progress_inactive", + "label": "Indicador de swipe · inactivo" + } + ], + "presets": [ + { + "name": "Ornevo services", + "blocks": [ + { "type": "tab", "settings": { "tab_label_desktop": "Consultoría" } }, + { "type": "tab", "settings": { "tab_label_desktop": "SEO" } }, + { "type": "tab", "settings": { "tab_label_desktop": "Desarrollo" } }, + { "type": "tab", "settings": { "tab_label_desktop": "Soporte" } } + ] + } + ] +} +{% endschema %} diff --git a/snippets/orn-hero-cta.liquid b/snippets/orn-hero-cta.liquid new file mode 100644 index 000000000..931f17a35 --- /dev/null +++ b/snippets/orn-hero-cta.liquid @@ -0,0 +1,22 @@ +{%- comment -%} + Ornevo CTA button renderer. Reusable across sections. + Accepts: + text — label + url — href + variant — primary | secondary | tertiary + new_tab — boolean + icon — raw SVG string (optional) + extra_class — string (optional) +{%- endcomment -%} +{%- if text != blank and url != blank -%} + + {{ text }} + {%- if icon != blank -%} + + {%- endif -%} + +{%- endif -%} diff --git a/snippets/orn-services-item.liquid b/snippets/orn-services-item.liquid new file mode 100644 index 000000000..4314511e2 --- /dev/null +++ b/snippets/orn-services-item.liquid @@ -0,0 +1,40 @@ +{%- comment -%} + Renders one item in the services grid. + Params: icon (image), text (richtext), cta_text, cta_url, cta_target, cta_variant, cta_icon (image) +{%- endcomment -%} + +{%- if text != blank -%} + {%- liquid + assign resolved_url = cta_url + if cta_target == 'contact_form' + assign resolved_url = '#contact' + endif + -%} + +
+ + + +
+{%- endif -%} diff --git a/snippets/scripts.liquid b/snippets/scripts.liquid index 514fdbe70..057fb0ac1 100644 --- a/snippets/scripts.liquid +++ b/snippets/scripts.liquid @@ -272,6 +272,20 @@ defer="defer" > +{%- comment -%} Ornevo — Alpine.js + components. Order matters: register components before Alpine boots. {%- endcomment -%} + + + +