feat(modalidades): 6 ícones em falta, ícones animados, ícone centrado no cartão aberto
- Ícones novos (Bootcamp, YourFit Core, Run3070, Total Workout, On Ramp, Jump) do mesmo conjunto; só o LPO continua sem ícone no ficheiro de design. - Os ícones fitme-*.svg passam a ir embutidos (modalidade_icon_html): cada traço tem pathLength=1 e desenha-se ao aparecer, no hover e ao abrir o cartão; cada família tem um movimento (levantar, soco, respirar, saltar, chama, pedalar, correr). Outros ícones continuam <img>. - No cartão de detalhe (desktop) o ícone e o nome deslizam até ao centro vertical da coluna. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42f5919915
commit
cb0e4540f2
10 files changed
+186
-8
No files matched your search
@@ -137,3 +137,42 @@ function site_tel(?string $phone): string {
|
|||||||
if ($p[0] !== '+' && str_starts_with($p, '00')) return '+' . substr($p, 2);
|
if ($p[0] !== '+' && str_starts_with($p, '00')) return '+' . substr($p, 2);
|
||||||
return $p;
|
return $p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Modalidade icon markup ───────────────────────────────────────────────────
|
||||||
|
// The FitMe icon set (uploads/modalidades/icons/fitme-*.svg, shipped in the repo
|
||||||
|
// — backoffice uploads are always named upl_*) is inlined so CSS can animate
|
||||||
|
// the strokes: every drawable element gets pathLength="1" for the draw-in, and
|
||||||
|
// the <svg> carries data-anim, a per-family movement (see style.css). Anything
|
||||||
|
// else (legacy PNGs, uploaded SVGs) stays a plain <img>.
|
||||||
|
function modalidade_icon_anim(string $slug): string {
|
||||||
|
$map = [
|
||||||
|
'lift' => ['yourfit-pump', 'crossfit', 'crossfit-teen-s', 'fbb', 'yourfit-core', 'total-workout'],
|
||||||
|
'punch' => ['yourfit-fight', 'bootcamp'],
|
||||||
|
'breathe' => ['yourfit-mind', 'yoga', 'pilates'],
|
||||||
|
'bounce' => ['zumba', 'jump', 'yourfit-pulse', 'hybrid'],
|
||||||
|
'flicker' => ['fitburn'],
|
||||||
|
'ride' => ['cycling'],
|
||||||
|
'run' => ['run3070', 'on-ramp', 'localizada'],
|
||||||
|
];
|
||||||
|
foreach ($map as $anim => $slugs) if (in_array($slug, $slugs, true)) return $anim;
|
||||||
|
return 'lift';
|
||||||
|
}
|
||||||
|
|
||||||
|
function modalidade_icon_html(?string $icon): string {
|
||||||
|
$icon = trim((string) $icon);
|
||||||
|
if ($icon === '') return '';
|
||||||
|
if (preg_match('/^fitme-([a-z0-9-]+)\.svg$/', $icon, $m)) {
|
||||||
|
$file = dirname(__DIR__) . '/uploads/modalidades/icons/' . $icon;
|
||||||
|
$svg = is_file($file) ? (string) file_get_contents($file) : '';
|
||||||
|
if (str_starts_with(ltrim($svg), '<svg')) {
|
||||||
|
// Belt and braces: no scripts / event handlers, whatever the file says.
|
||||||
|
$svg = preg_replace('~<script\b.*?</script>~is', '', $svg);
|
||||||
|
$svg = preg_replace('~\son\w+\s*=\s*("[^"]*"|\'[^\']*\')~i', '', $svg);
|
||||||
|
$svg = preg_replace('~\scolor="[^"]*"~', '', $svg, 1); // colour comes from CSS
|
||||||
|
$svg = preg_replace('~<(path|circle|rect|line|polyline|polygon|ellipse)\b~', '<$1 pathLength="1"', $svg);
|
||||||
|
$svg = preg_replace('~^\s*<svg\b~', '<svg class="mi" data-anim="' . modalidade_icon_anim($m[1]) . '" aria-hidden="true" focusable="false"', $svg, 1);
|
||||||
|
return $svg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '<img loading="lazy" draggable="false" src="' . htmlspecialchars(site_img_url($icon, 'uploads/modalidades/icons')) . '" alt="">';
|
||||||
|
}
|
||||||
@@ -3968,3 +3968,130 @@ h3.contact-title {
|
|||||||
color: var(--fitme-ash);
|
color: var(--fitme-ash);
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════════════════
|
||||||
|
Modalidades — animated icons
|
||||||
|
The FitMe icon set is inlined as <svg class="mi" data-anim="…"> (see
|
||||||
|
modalidade_icon_html()). Every drawable element has pathLength="1", so a
|
||||||
|
stroke-dasharray of 1 lets the lines draw themselves: once when the card
|
||||||
|
scrolls into view, again on hover / focus, and in the detail panel as it
|
||||||
|
opens. On top of that each family has its own small movement:
|
||||||
|
lift — weights going up (Pump, CrossFit, FBB, Core, Total Workout)
|
||||||
|
punch — a jab (Fight, Bootcamp)
|
||||||
|
breathe — slow in / out (Mind, Yoga, Pilates)
|
||||||
|
bounce — hop with squash (Zumba, Jump, Pulse, Hybrid)
|
||||||
|
flicker — flame (FitBurn)
|
||||||
|
ride — pedalling rock (Cycling)
|
||||||
|
run — running bob (Run3070, On Ramp, Localizada)
|
||||||
|
══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
.ib-icon svg.mi {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
color: #141414;
|
||||||
|
overflow: visible;
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi [pathLength] {
|
||||||
|
stroke-dasharray: 1;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mi-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
|
||||||
|
@keyframes mi-draw-h { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
|
||||||
|
|
||||||
|
/* Draw-in once as the card appears (.revealed comes from the scroll observer). */
|
||||||
|
.mod-card.revealed .mi [pathLength] {
|
||||||
|
animation: mi-draw .9s cubic-bezier(.5, 0, .2, 1) both;
|
||||||
|
}
|
||||||
|
.mod-card.revealed .mi [pathLength]:nth-child(2) { animation-delay: .12s; }
|
||||||
|
.mod-card.revealed .mi [pathLength]:nth-child(3) { animation-delay: .24s; }
|
||||||
|
.mod-card.revealed .mi [pathLength]:nth-child(n+4) { animation-delay: .34s; }
|
||||||
|
|
||||||
|
/* Hover / focus / open panel: redraw (a second keyframe name restarts it). */
|
||||||
|
.mod-card:hover .mi [pathLength],
|
||||||
|
.mod-card:focus-visible .mi [pathLength],
|
||||||
|
.mod-detail.is-open .mi [pathLength] {
|
||||||
|
animation: mi-draw-h .6s cubic-bezier(.5, 0, .2, 1) both;
|
||||||
|
}
|
||||||
|
.mod-card:hover .mi [pathLength]:nth-child(2),
|
||||||
|
.mod-detail.is-open .mi [pathLength]:nth-child(2) { animation-delay: .08s; }
|
||||||
|
.mod-card:hover .mi [pathLength]:nth-child(n+3),
|
||||||
|
.mod-detail.is-open .mi [pathLength]:nth-child(n+3) { animation-delay: .16s; }
|
||||||
|
|
||||||
|
@keyframes mi-lift {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
30% { transform: translateY(-6px); }
|
||||||
|
55% { transform: translateY(1px); }
|
||||||
|
75% { transform: translateY(-2px); }
|
||||||
|
}
|
||||||
|
@keyframes mi-punch {
|
||||||
|
0%, 100% { transform: translateX(0) rotate(0); }
|
||||||
|
25% { transform: translateX(-3px) rotate(-6deg); }
|
||||||
|
45% { transform: translateX(7px) rotate(4deg); }
|
||||||
|
65% { transform: translateX(-1px) rotate(0); }
|
||||||
|
}
|
||||||
|
@keyframes mi-breathe {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.13); }
|
||||||
|
}
|
||||||
|
@keyframes mi-bounce {
|
||||||
|
0%, 100% { transform: translateY(0) scale(1, 1); }
|
||||||
|
18% { transform: translateY(0) scale(1.12, .86); }
|
||||||
|
50% { transform: translateY(-9px) scale(.94, 1.07); }
|
||||||
|
80% { transform: translateY(0) scale(1.06, .94); }
|
||||||
|
}
|
||||||
|
@keyframes mi-flicker {
|
||||||
|
0%, 100% { transform: scale(1) skewX(0); }
|
||||||
|
20% { transform: scale(1.06, .93) skewX(-4deg); }
|
||||||
|
40% { transform: scale(.95, 1.09) skewX(3deg); }
|
||||||
|
60% { transform: scale(1.04, .97) skewX(-2deg); }
|
||||||
|
80% { transform: scale(.98, 1.05) skewX(2deg); }
|
||||||
|
}
|
||||||
|
@keyframes mi-ride {
|
||||||
|
0%, 100% { transform: translateX(0) rotate(0); }
|
||||||
|
25% { transform: translateX(-2px) rotate(-5deg); }
|
||||||
|
75% { transform: translateX(3px) rotate(3deg); }
|
||||||
|
}
|
||||||
|
@keyframes mi-run {
|
||||||
|
0%, 100% { transform: translateY(0) rotate(0); }
|
||||||
|
25% { transform: translateY(-4px) rotate(-6deg); }
|
||||||
|
50% { transform: translateY(0) rotate(0); }
|
||||||
|
75% { transform: translateY(-4px) rotate(6deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-card:hover .mi, .mod-card:focus-visible .mi, .mod-detail.is-open .mi {
|
||||||
|
animation-duration: .9s;
|
||||||
|
animation-timing-function: cubic-bezier(.45, 0, .25, 1);
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-delay: .1s;
|
||||||
|
}
|
||||||
|
.mod-card:hover .mi[data-anim="lift"], .mod-card:focus-visible .mi[data-anim="lift"], .mod-detail.is-open .mi[data-anim="lift"] { animation-name: mi-lift; }
|
||||||
|
.mod-card:hover .mi[data-anim="punch"], .mod-card:focus-visible .mi[data-anim="punch"], .mod-detail.is-open .mi[data-anim="punch"] { animation-name: mi-punch; animation-duration: .7s; animation-iteration-count: 2; }
|
||||||
|
.mod-card:hover .mi[data-anim="breathe"], .mod-card:focus-visible .mi[data-anim="breathe"], .mod-detail.is-open .mi[data-anim="breathe"] { animation-name: mi-breathe; animation-duration: 1.8s; animation-iteration-count: 2; }
|
||||||
|
.mod-card:hover .mi[data-anim="bounce"], .mod-card:focus-visible .mi[data-anim="bounce"], .mod-detail.is-open .mi[data-anim="bounce"] { animation-name: mi-bounce; animation-duration: .75s; animation-iteration-count: 2; }
|
||||||
|
.mod-card:hover .mi[data-anim="flicker"], .mod-card:focus-visible .mi[data-anim="flicker"], .mod-detail.is-open .mi[data-anim="flicker"] { animation-name: mi-flicker; animation-iteration-count: 3; transform-origin: 50% 100%; }
|
||||||
|
.mod-card:hover .mi[data-anim="ride"], .mod-card:focus-visible .mi[data-anim="ride"], .mod-detail.is-open .mi[data-anim="ride"] { animation-name: mi-ride; animation-duration: .6s; animation-iteration-count: 3; }
|
||||||
|
.mod-card:hover .mi[data-anim="run"], .mod-card:focus-visible .mi[data-anim="run"], .mod-detail.is-open .mi[data-anim="run"] { animation-name: mi-run; animation-duration: .6s; animation-iteration-count: 3; }
|
||||||
|
|
||||||
|
/* Detail panel (desktop): icon + name start where they sit on the card, then
|
||||||
|
glide to the vertical centre of the taller panel column as it opens
|
||||||
|
(--md-pad-open from main.js). Phones centre it with their own layout. */
|
||||||
|
@media (min-width: 768px) and (hover: hover) {
|
||||||
|
.md-card {
|
||||||
|
transition: padding-top .38s cubic-bezier(.4, 0, .2, 1);
|
||||||
|
}
|
||||||
|
.mod-detail.is-open .md-card {
|
||||||
|
padding-top: var(--md-pad-open, var(--md-pad, 0px));
|
||||||
|
transition: padding-top .55s cubic-bezier(.2, .9, .25, 1) .05s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.ib-icon svg.mi { width: 34px; height: 34px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.mi, .mi [pathLength] { animation: none !important; }
|
||||||
|
}
|
||||||
+12
-6
@@ -220,12 +220,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
function panelOpen() { return panel.classList.contains('is-open'); }
|
function panelOpen() { return panel.classList.contains('is-open'); }
|
||||||
|
|
||||||
function fill(card) {
|
function fill(card) {
|
||||||
var img = card.querySelector('.ib-icon img');
|
|
||||||
var name = card.querySelector('h3').textContent;
|
var name = card.querySelector('h3').textContent;
|
||||||
var tpl = card.querySelector('template.mod-desc');
|
var tpl = card.querySelector('template.mod-desc');
|
||||||
var pImg = panel.querySelector('.md-card img');
|
// Same icon markup as the card (inline animated SVG or <img>), so the
|
||||||
pImg.hidden = !img;
|
// panel's copy replays its animation as it opens.
|
||||||
pImg.src = img ? img.src : '';
|
var icon = card.querySelector('.ib-icon');
|
||||||
|
panel.querySelector('.md-card .ib-icon').innerHTML = icon ? icon.innerHTML : '';
|
||||||
panel.querySelector('.md-card h3').textContent = name;
|
panel.querySelector('.md-card h3').textContent = name;
|
||||||
panel.querySelector('.md-name').textContent = name;
|
panel.querySelector('.md-name').textContent = name;
|
||||||
panel.querySelector('.md-text').innerHTML = tpl ? tpl.innerHTML : '';
|
panel.querySelector('.md-text').innerHTML = tpl ? tpl.innerHTML : '';
|
||||||
@@ -250,6 +250,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
panel.style.setProperty('--md-y', (cr.top - sr.top) + 'px');
|
panel.style.setProperty('--md-y', (cr.top - sr.top) + 'px');
|
||||||
panel.style.setProperty('--md-cx', Math.max(0, w - cr.width) + 'px');
|
panel.style.setProperty('--md-cx', Math.max(0, w - cr.width) + 'px');
|
||||||
panel.style.setProperty('--md-cy', Math.max(0, panel.offsetHeight - cr.height) + 'px');
|
panel.style.setProperty('--md-cy', Math.max(0, panel.offsetHeight - cr.height) + 'px');
|
||||||
|
// Open, the icon + name slide from where they sit on the card to the
|
||||||
|
// vertical centre of the (taller) panel column.
|
||||||
|
var title = card.querySelector('h3');
|
||||||
|
var contentH = icon && title ? (title.offsetTop + title.offsetHeight) - icon.offsetTop : 0;
|
||||||
|
panel.style.setProperty('--md-pad-open', Math.max(0, (panel.offsetHeight - contentH) / 2) + 'px');
|
||||||
}
|
}
|
||||||
|
|
||||||
function flyFrom(card, opening) {
|
function flyFrom(card, opening) {
|
||||||
@@ -280,10 +285,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
panel.style.setProperty('--md-top', Math.max(24, (window.innerHeight - panel.offsetHeight) / 2) + 'px');
|
panel.style.setProperty('--md-top', Math.max(24, (window.innerHeight - panel.offsetHeight) / 2) + 'px');
|
||||||
flyFrom(card, true);
|
flyFrom(card, true);
|
||||||
} else {
|
} else {
|
||||||
panel.style.transition = 'none';
|
var mdCard = panel.querySelector('.md-card');
|
||||||
|
panel.style.transition = mdCard.style.transition = 'none';
|
||||||
place(card);
|
place(card);
|
||||||
void panel.offsetWidth;
|
void panel.offsetWidth;
|
||||||
panel.style.transition = '';
|
panel.style.transition = mdCard.style.transition = '';
|
||||||
void panel.offsetWidth;
|
void panel.offsetWidth;
|
||||||
panel.classList.add('is-open');
|
panel.classList.add('is-open');
|
||||||
card.classList.add('is-source');
|
card.classList.add('is-source');
|
||||||
|
|||||||
+2
-2
@@ -193,7 +193,7 @@ if ($modalidades):
|
|||||||
<?php foreach($modalidades as $d): $desc = trim(strip_tags((string) ($d['descricao'] ?? ''))) !== '' ? trim((string) $d['descricao']) : ''; ?>
|
<?php foreach($modalidades as $d): $desc = trim(strip_tags((string) ($d['descricao'] ?? ''))) !== '' ? trim((string) $d['descricao']) : ''; ?>
|
||||||
<div class="icon-box-item mod-card<?= $desc !== '' ? ' has-info' : '' ?>" data-color="<?= htmlspecialchars((string) $d['cor']) ?>"<?= $desc !== '' ? ' role="button" tabindex="0" aria-haspopup="dialog" aria-label="Ver descrição de ' . htmlspecialchars((string) $d['nome']) . '"' : '' ?>>
|
<div class="icon-box-item mod-card<?= $desc !== '' ? ' has-info' : '' ?>" data-color="<?= htmlspecialchars((string) $d['cor']) ?>"<?= $desc !== '' ? ' role="button" tabindex="0" aria-haspopup="dialog" aria-label="Ver descrição de ' . htmlspecialchars((string) $d['nome']) . '"' : '' ?>>
|
||||||
<div class="ib-icon">
|
<div class="ib-icon">
|
||||||
<?php if (trim((string) $d['icon']) !== ''): ?><img loading="lazy" draggable="false" src="<?= site_img_url($d['icon'], 'uploads/modalidades/icons') ?>" alt=""><?php endif; ?>
|
<?= modalidade_icon_html($d['icon']) /* inline animated SVG for the FitMe set, else <img> */ ?>
|
||||||
</div>
|
</div>
|
||||||
<h3><?= htmlspecialchars((string) $d['nome']) ?></h3>
|
<h3><?= htmlspecialchars((string) $d['nome']) ?></h3>
|
||||||
<?php if ($desc !== ''): ?>
|
<?php if ($desc !== ''): ?>
|
||||||
@@ -208,7 +208,7 @@ if ($modalidades):
|
|||||||
<div class="mod-detail" role="dialog" aria-modal="false" aria-hidden="true">
|
<div class="mod-detail" role="dialog" aria-modal="false" aria-hidden="true">
|
||||||
<button type="button" class="md-close" aria-label="Fechar">×</button>
|
<button type="button" class="md-close" aria-label="Fechar">×</button>
|
||||||
<div class="md-card">
|
<div class="md-card">
|
||||||
<div class="ib-icon"><img src="" alt="" hidden></div>
|
<div class="ib-icon"></div>
|
||||||
<h3></h3>
|
<h3></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="md-body">
|
<div class="md-body">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" color="#141414" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter"><path d="M12 6h12v17l14 5v6H12z"></path><path fill="currentColor" d="M10 36h30v5H10z"></path><path d="M17 12h7M17 18h7"></path></svg>
|
||||||
|
After Width: | Height: | Size: 331 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" color="#141414" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter"><circle fill="currentColor" cx="24" cy="5" r="3.5"></circle><path d="M24 11v9M24 13l-7-5M24 13l7-5M24 20l-5 6M24 20l5 6"></path><path fill="currentColor" d="M5 31h38v3H5z"></path><path d="M9 34l-3 7M39 34l3 7M20 38h8"></path></svg>
|
||||||
|
After Width: | Height: | Size: 429 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" color="#141414" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter"><path fill="currentColor" d="M6 41l36-20v20z"></path><path d="M6 27L25 11M17 11h8v8"></path></svg>
|
||||||
|
After Width: | Height: | Size: 296 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" color="#141414" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter"><circle fill="currentColor" cx="31" cy="8" r="3.5"></circle><path d="M29 14l-6 13M28 17l7 5 3-4M28 17l-8-1-4 5M23 27l9 4-2 10M23 27l-6 8-8-1M4 14h8M3 21h7"></path></svg>
|
||||||
|
After Width: | Height: | Size: 367 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" color="#141414" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter"><circle fill="currentColor" cx="24" cy="11" r="3.5"></circle><path d="M24 17v11M24 20l-9-8M24 20l9-8M24 28l-8 11M24 28l8 11"></path><path d="M4 12V4h8M36 4h8v8M44 36v8h-8M12 44H4v-8"></path></svg>
|
||||||
|
After Width: | Height: | Size: 394 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" color="#141414" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter"><path d="M11 6h26l-3 14 2 22H12l2-22z"></path><path fill="currentColor" stroke="none" d="M17.5 10h5.5v5h-5.5zM25 10h5.5v5H25zM17.5 18h5.5v5h-5.5zM25 18h5.5v5H25zM17.5 26h5.5v5h-5.5zM25 26h5.5v5H25z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 410 B |
Reference in new issue
Block a user