feat(sidebar): fleet sidebar standard (header row, user in sidebar, collapsible groups, mini mode, no-flash transitions)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b73c23bf55
commit
aa55d44406
8 files changed
+654
-225
No files matched your search
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
// Auxiliares da barra lateral do backoffice (incluído por includes/header.php).
|
||||
// Ficam aqui, e não no HTML, para o menu ser só uma lista de itens: o ativo,
|
||||
// o aria-current, o title/aria-label (o modo mini só mostra o ícone) e a
|
||||
// abertura dos grupos decidem-se num sítio.
|
||||
|
||||
/** Painel de cada papel: o admin tem o seu, o staff o dele, o cliente a área de sócio. */
|
||||
function bo_dashboard_path(): string
|
||||
{
|
||||
if (is_admin()) return 'views/admin/dashboard.php';
|
||||
if (is_staff()) return 'views/staff/dashboard.php';
|
||||
return 'views/user/dashboard.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* O pedido atual está nesta página? Compara pelo FIM do caminho (e não com ===):
|
||||
* em dev o site vive num subcaminho do host (/GitTea/repos/fitme3070.pt/...).
|
||||
* Um caminho terminado em «/» é uma pasta inteira (inclui edit.php, create.php...).
|
||||
* Aceita uma lista: basta uma coincidir.
|
||||
*
|
||||
* @param string|string[] $paths
|
||||
*/
|
||||
function bo_nav_active($paths): bool
|
||||
{
|
||||
$script = str_replace('\\', '/', (string) ($_SERVER['SCRIPT_NAME'] ?? ''));
|
||||
foreach ((array) $paths as $p) {
|
||||
$p = '/' . ltrim($p, '/');
|
||||
if (str_ends_with($p, '/') ? str_contains($script, $p) : str_ends_with($script, $p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uma ligação da barra lateral.
|
||||
* Opções: match (caminho/s para o «ativo», por omissão o próprio href), mod
|
||||
* ('sub' | 'sub2' | 'primary'), badge (inteiro; só aparece se > 0).
|
||||
*/
|
||||
function bo_nav_link(string $href, string $icon, string $label, array $o = []): string
|
||||
{
|
||||
$active = array_key_exists('active', $o) ? (bool) $o['active'] : bo_nav_active($o['match'] ?? strtok($href, '?'));
|
||||
$mod = isset($o['mod']) ? ' bo-nav-item--' . $o['mod'] : '';
|
||||
$l = htmlspecialchars($label, ENT_QUOTES, 'UTF-8');
|
||||
$badge = (int) ($o['badge'] ?? 0);
|
||||
return '<a class="nav-link bo-nav-item' . $mod . ($active ? ' active' : '') . '" href="' . htmlspecialchars(base_url($href), ENT_QUOTES, 'UTF-8') . '"'
|
||||
. ' title="' . $l . '" aria-label="' . $l . '"' . ($active ? ' aria-current="page"' : '') . '>'
|
||||
. '<i class="bi ' . htmlspecialchars($icon, ENT_QUOTES, 'UTF-8') . '" aria-hidden="true"></i>'
|
||||
. '<span class="bo-nav-item__label">' . $l . '</span>'
|
||||
. ($badge > 0 ? '<span class="badge bg-danger ms-auto bo-nav-item__badge">' . $badge . '</span>' : '')
|
||||
. '</a>' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Abre um grupo recolhível. O conteúdo fica em buffer até bo_nav_group_end(),
|
||||
* que precisa de saber se algum item é o ativo (o grupo da página atual abre
|
||||
* sempre). $sub = subcategoria dentro de outro grupo (recuada).
|
||||
*/
|
||||
function bo_nav_group_start(string $key, string $label, string $icon, bool $sub = false): void
|
||||
{
|
||||
$GLOBALS['bo_nav_group_stack'][] = [$key, $label, $icon, $sub];
|
||||
ob_start();
|
||||
}
|
||||
|
||||
/** Fecha o grupo aberto por bo_nav_group_start() e escreve o markup final. */
|
||||
function bo_nav_group_end(): void
|
||||
{
|
||||
$items = (string) ob_get_clean();
|
||||
[$key, $label, $icon, $sub] = array_pop($GLOBALS['bo_nav_group_stack']);
|
||||
$hasActive = str_contains($items, 'aria-current="page"');
|
||||
$id = 'bo-grp-' . $key;
|
||||
$k = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
|
||||
$l = htmlspecialchars($label, ENT_QUOTES, 'UTF-8');
|
||||
$i = htmlspecialchars($icon, ENT_QUOTES, 'UTF-8');
|
||||
$short = htmlspecialchars(mb_strtoupper(mb_substr($label, 0, 4)), ENT_QUOTES, 'UTF-8');
|
||||
echo '<div class="bo-nav-group' . ($sub ? ' bo-nav-group--sub' : '') . ($hasActive ? ' has-active' : '') . '" data-group="' . $k . '">'
|
||||
. '<button class="bo-nav-group__label" type="button" data-short="' . $short . '" aria-expanded="true"'
|
||||
. ' aria-controls="' . $id . '" data-group-toggle="' . $k . '" title="' . $l . '" aria-label="' . $l . '">'
|
||||
. '<i class="bi ' . $i . ' bo-nav-group__icon" aria-hidden="true"></i>'
|
||||
. '<span class="bo-nav-group__text">' . $l . '</span>'
|
||||
. '<i class="bi bi-chevron-down bo-nav-group__chevron" aria-hidden="true"></i></button>'
|
||||
. '<div class="bo-nav-group__body" id="' . $id . '"><div class="bo-nav-group__inner">' . $items . '</div></div>'
|
||||
. '</div>' . "\n";
|
||||
}
|
||||
|
||||
/** Iniciais para o avatar (até duas letras: primeiro e último nome). */
|
||||
function bo_initials(string $name): string
|
||||
{
|
||||
// Só palavras que começam por letra: «Staff (dev)» dá «SD»... não «S(».
|
||||
preg_match_all('/\p{L}[\p{L}\p{M}-]*/u', $name, $m);
|
||||
$parts = $m[0];
|
||||
if (!$parts) return '?';
|
||||
$first = mb_substr($parts[0], 0, 1);
|
||||
$last = count($parts) > 1 ? mb_substr($parts[count($parts) - 1], 0, 1) : '';
|
||||
return mb_strtoupper($first . $last);
|
||||
}
|
||||
Reference in new issue
Block a user