separacao de sobre nos no dropdow Tempo
This commit is contained in:
File diff suppressed because one or more lines are too long
+2
-407
@@ -7,413 +7,8 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* Institutional shell adapted from Tempo; page content keeps its own theme. */
|
||||
.tempo-header {
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
background: #090909;
|
||||
color: #fff;
|
||||
}
|
||||
.tempo-header-inner {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
grid-template-rows: 72px;
|
||||
column-gap: 0.5rem;
|
||||
align-items: center;
|
||||
max-width: 1440px;
|
||||
margin-inline: auto;
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
grid-area: 1 / 1;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.tempo-header-logo {
|
||||
width: auto;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.tempo-primary-navigation {
|
||||
display: none;
|
||||
grid-area: 2 / 1 / auto / -1;
|
||||
min-width: 0;
|
||||
border-top: 1px solid #ffffff1a;
|
||||
}
|
||||
.tempo-primary-navigation > ul {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.25rem 0.5rem;
|
||||
padding-block: 0.75rem;
|
||||
}
|
||||
.tempo-primary-navigation .tempo-nav-trigger {
|
||||
font-size: 0.9375rem;
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
.tempo-nav-trigger {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: #eee;
|
||||
transition:
|
||||
color 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
.tempo-nav-trigger:hover,
|
||||
.tempo-nav-trigger[data-state='open'] {
|
||||
background: #ffffff0d;
|
||||
color: #fff;
|
||||
}
|
||||
.tempo-nav-trigger[data-current-group] {
|
||||
color: #fff;
|
||||
box-shadow: inset 0 -2px #ed1c24;
|
||||
}
|
||||
.tempo-nav-trigger[data-state='open'] > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.tempo-primary-navigation .tempo-nav-trigger > svg {
|
||||
transform: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tempo-section-bar {
|
||||
border-top: 1px solid #ffffff14;
|
||||
background: inherit;
|
||||
}
|
||||
.tempo-section-inner {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.25rem 0.75rem;
|
||||
max-width: 1440px;
|
||||
margin-inline: auto;
|
||||
padding: 0 0.75rem 0.5rem;
|
||||
}
|
||||
.tempo-section-navigation {
|
||||
grid-area: 1 / 1 / auto / -1;
|
||||
min-width: 0;
|
||||
}
|
||||
.tempo-section-navigation > ul {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.tempo-section-navigation .tempo-nav-trigger {
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem 0.375rem;
|
||||
border-radius: 0;
|
||||
font-size: 0.8125rem;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.tempo-section-navigation .tempo-nav-trigger {
|
||||
min-height: 48px;
|
||||
gap: 0.5rem;
|
||||
padding-inline: 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.tempo-section-inner {
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
padding: 0.25rem 1.25rem;
|
||||
}
|
||||
.tempo-section-navigation {
|
||||
grid-area: 1 / 2;
|
||||
}
|
||||
.tempo-section-inner .tempo-panel-summary {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
.tempo-section-inner .tempo-interest-button {
|
||||
grid-area: 1 / 3;
|
||||
}
|
||||
}
|
||||
.tempo-header-extras {
|
||||
display: none;
|
||||
grid-area: 3 / 1 / auto / -1;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
padding-block: 0.75rem;
|
||||
border-top: 1px solid #ffffff1a;
|
||||
}
|
||||
.tempo-header[data-expanded] .tempo-primary-navigation {
|
||||
display: block;
|
||||
}
|
||||
.tempo-header[data-expanded] .tempo-header-extras {
|
||||
display: flex;
|
||||
}
|
||||
.tempo-header-utilities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.tempo-utility-button {
|
||||
display: flex;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.tempo-utility-button:hover {
|
||||
background: #ffffff10;
|
||||
}
|
||||
.tempo-header-social {
|
||||
display: flex;
|
||||
width: 30px;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--social-color);
|
||||
transition:
|
||||
transform 0.2s,
|
||||
opacity 0.2s;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.tempo-header-social:hover {
|
||||
transform: translateY(-2px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.tempo-login-button {
|
||||
display: inline-flex;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid #ffffff50;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: #fff;
|
||||
transition:
|
||||
background 0.2s,
|
||||
border-color 0.2s;
|
||||
}
|
||||
.tempo-login-button:hover {
|
||||
background: #ed1c24;
|
||||
border-color: #ed1c24;
|
||||
}
|
||||
.tempo-login-button > span {
|
||||
max-width: clamp(2rem, calc(100vw - 18rem), 6rem);
|
||||
}
|
||||
.tempo-header-actions {
|
||||
grid-area: 1 / 2;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.tempo-interest-button {
|
||||
display: inline-flex;
|
||||
grid-area: 2 / 2;
|
||||
justify-self: end;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 40px;
|
||||
padding: 0.5rem 0.65rem;
|
||||
border: 1px solid #ed1c24;
|
||||
border-radius: 0.4rem;
|
||||
background: #ed1c24;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.tempo-interest-button:hover {
|
||||
background: #c9151c;
|
||||
}
|
||||
.tempo-panel-summary {
|
||||
grid-area: 2 / 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 0.1rem;
|
||||
min-height: 44px;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
color: #fff;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.tempo-panel-summary:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.tempo-panel-count {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
.tempo-header-toggle {
|
||||
grid-area: 1 / 3;
|
||||
display: inline-flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #ffffff33;
|
||||
border-radius: 0.4rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tempo-header-toggle:hover,
|
||||
.tempo-header-toggle[aria-expanded='true'] {
|
||||
background: #ffffff10;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.tempo-header-inner {
|
||||
padding-inline: 1.25rem;
|
||||
}
|
||||
.tempo-header-logo {
|
||||
height: 44px;
|
||||
}
|
||||
.tempo-primary-navigation > ul {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.tempo-header-inner {
|
||||
grid-template-rows: 88px;
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
.tempo-header-logo {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
/* Shared columns keep each portal area below its institutional link. */
|
||||
.tempo-header-layout {
|
||||
display: grid;
|
||||
grid-template-columns: auto repeat(4, max-content) minmax(0, 1fr) auto;
|
||||
column-gap: 0.25rem;
|
||||
max-width: 1440px;
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
.tempo-header-inner {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
min-height: 88px;
|
||||
column-gap: normal;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
.tempo-header-logo {
|
||||
height: 64px;
|
||||
}
|
||||
.tempo-header[data-expanded] .tempo-primary-navigation,
|
||||
.tempo-primary-navigation {
|
||||
display: grid;
|
||||
grid-area: 1 / 2 / 2 / 6;
|
||||
grid-template-columns: subgrid;
|
||||
border: 0;
|
||||
}
|
||||
.tempo-primary-navigation > ul {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
gap: normal;
|
||||
padding: 0;
|
||||
}
|
||||
.tempo-nav-trigger {
|
||||
justify-content: center;
|
||||
padding-inline: 0.875rem;
|
||||
font-size: 1.125rem;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
.tempo-primary-navigation .tempo-nav-trigger {
|
||||
padding-inline: 0.625rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
.tempo-header-extras {
|
||||
display: flex;
|
||||
grid-area: 1 / 6;
|
||||
justify-self: end;
|
||||
flex-shrink: 0;
|
||||
padding: 0 0.75rem;
|
||||
border-top: 0;
|
||||
border-inline: 1px solid #ffffff1a;
|
||||
}
|
||||
.tempo-header-utilities {
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.tempo-header-social {
|
||||
width: 26px;
|
||||
}
|
||||
.tempo-header-toggle {
|
||||
display: none;
|
||||
}
|
||||
.tempo-header-actions {
|
||||
grid-area: 1 / 7;
|
||||
}
|
||||
.tempo-section-bar {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
.tempo-section-inner {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
gap: normal;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding-inline: 0;
|
||||
}
|
||||
.tempo-section-navigation {
|
||||
display: grid;
|
||||
grid-area: 1 / 2 / 2 / 5;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
.tempo-section-navigation > ul {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
gap: normal;
|
||||
}
|
||||
.tempo-section-navigation .tempo-nav-trigger {
|
||||
padding-inline: 0.625rem;
|
||||
}
|
||||
.tempo-primary-navigation .tempo-nav-trigger,
|
||||
.tempo-section-navigation .tempo-nav-trigger {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.tempo-interest-button {
|
||||
padding-inline: 0.85rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.tempo-section-inner .tempo-interest-button {
|
||||
grid-area: 1 / 6 / 2 / -1;
|
||||
}
|
||||
}
|
||||
.tempo-header :is(a, button):focus-visible {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
@import './tempo-header.css';
|
||||
|
||||
.tempo-dropdown {
|
||||
z-index: 60;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
/* Presentation choice for Tempo's 21/10/2026 request: one shared navigation. */
|
||||
.tempo-header {
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
background: #090909;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid #ffffff14;
|
||||
}
|
||||
.tempo-header-inner {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
grid-template-rows: 71px;
|
||||
align-items: center;
|
||||
column-gap: 0.5rem;
|
||||
max-width: 1600px;
|
||||
margin-inline: auto;
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
grid-area: 1 / 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 112px;
|
||||
max-width: 100%;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.tempo-header-logo {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
.tempo-primary-navigation {
|
||||
display: none;
|
||||
grid-area: 2 / 1 / auto / -1;
|
||||
min-width: 0;
|
||||
border-top: 1px solid #ffffff1a;
|
||||
padding-block: 0.75rem;
|
||||
}
|
||||
.tempo-primary-navigation > ul {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.tempo-nav-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.4rem;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
color: #e2e8f0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
color 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
.tempo-nav-trigger:hover,
|
||||
.tempo-nav-trigger[data-state='open'] {
|
||||
background: #ffffff0d;
|
||||
color: #fff;
|
||||
}
|
||||
.tempo-nav-trigger[data-current-group] {
|
||||
color: #fff;
|
||||
background: #ffffff08;
|
||||
box-shadow: inset 0 -2px #ed1c24;
|
||||
}
|
||||
.tempo-nav-trigger > svg {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.tempo-nav-trigger[data-state='open'] > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.tempo-header-extras {
|
||||
display: none;
|
||||
grid-area: 3 / 1 / auto / -1;
|
||||
min-width: 0;
|
||||
padding-block: 0.75rem;
|
||||
border-top: 1px solid #ffffff1a;
|
||||
}
|
||||
.tempo-header-utilities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.tempo-utility-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
min-height: 44px;
|
||||
padding: 0.4rem;
|
||||
border-radius: 0.5rem;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.tempo-utility-button:hover {
|
||||
background: #ffffff10;
|
||||
}
|
||||
.tempo-header-actions {
|
||||
grid-area: 1 / 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.tempo-login-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 44px;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border: 1px solid #ffffff40;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background 0.2s,
|
||||
border-color 0.2s;
|
||||
}
|
||||
.tempo-login-button:hover {
|
||||
background: #ffffff10;
|
||||
border-color: #ffffff80;
|
||||
}
|
||||
.tempo-login-button > span {
|
||||
max-width: clamp(2rem, calc(100vw - 18rem), 6rem);
|
||||
}
|
||||
.tempo-panel-summary {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.5rem;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.tempo-panel-summary:hover {
|
||||
background: #ffffff10;
|
||||
color: #fff;
|
||||
}
|
||||
.tempo-panel-count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -0.125rem;
|
||||
min-width: 17px;
|
||||
padding-inline: 0.2rem;
|
||||
border: 2px solid #090909;
|
||||
border-radius: 999px;
|
||||
background: #ed1c24;
|
||||
color: #fff;
|
||||
font-size: 0.5625rem;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
.tempo-interest-button {
|
||||
display: none;
|
||||
grid-area: 4 / 1 / auto / -1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 44px;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border: 1px solid #ed1c24;
|
||||
border-radius: 0.5rem;
|
||||
background: #ed1c24;
|
||||
color: #fff;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.tempo-interest-button:hover {
|
||||
background: #c9151c;
|
||||
}
|
||||
.tempo-header-toggle {
|
||||
grid-area: 1 / 3;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 1px solid #ffffff33;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tempo-header-toggle:hover,
|
||||
.tempo-header-toggle[aria-expanded='true'] {
|
||||
background: #ffffff10;
|
||||
}
|
||||
.tempo-header[data-expanded] .tempo-primary-navigation {
|
||||
display: block;
|
||||
}
|
||||
.tempo-header[data-expanded] .tempo-header-extras {
|
||||
display: flex;
|
||||
}
|
||||
.tempo-header[data-expanded] .tempo-interest-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
.tempo-header :is(a, button):focus-visible {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.tempo-submenu {
|
||||
max-width: min(
|
||||
calc(100vw - 1.5rem),
|
||||
var(--radix-dropdown-menu-content-available-width)
|
||||
);
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.tempo-header-inner {
|
||||
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
||||
column-gap: 0.75rem;
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
width: 132px;
|
||||
}
|
||||
.tempo-interest-button {
|
||||
display: inline-flex;
|
||||
grid-area: 1 / 3;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.tempo-header-toggle {
|
||||
grid-area: 1 / 4;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.tempo-header-inner {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
|
||||
column-gap: 0.25rem;
|
||||
padding-inline: 1.25rem;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
width: 100px;
|
||||
}
|
||||
.tempo-login-button > span {
|
||||
max-width: 3rem;
|
||||
}
|
||||
.tempo-primary-navigation,
|
||||
.tempo-header[data-expanded] .tempo-primary-navigation {
|
||||
display: block;
|
||||
grid-area: 1 / 2;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
justify-self: center;
|
||||
}
|
||||
.tempo-primary-navigation > ul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
.tempo-nav-trigger {
|
||||
justify-content: center;
|
||||
padding-inline: 0.25rem;
|
||||
font-size: 1.125rem;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
.tempo-header-extras,
|
||||
.tempo-header[data-expanded] .tempo-header-extras {
|
||||
display: flex;
|
||||
grid-area: 1 / 3;
|
||||
padding: 0 0.5rem 0 0;
|
||||
border: 0;
|
||||
border-right: 1px solid #ffffff1a;
|
||||
}
|
||||
.tempo-header-utilities {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.tempo-header-actions {
|
||||
grid-area: 1 / 4;
|
||||
}
|
||||
.tempo-interest-button {
|
||||
grid-area: 1 / 5;
|
||||
}
|
||||
.tempo-header-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1440px) {
|
||||
.tempo-header-inner {
|
||||
column-gap: 0.75rem;
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
.tempo-nav-trigger {
|
||||
padding-inline: 0.375rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
width: 132px;
|
||||
}
|
||||
.tempo-login-button > span {
|
||||
max-width: 5rem;
|
||||
}
|
||||
.tempo-header-extras {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
.tempo-primary-navigation > ul {
|
||||
gap: 0.125rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1536px) {
|
||||
.tempo-header-inner {
|
||||
column-gap: 1rem;
|
||||
}
|
||||
.tempo-nav-trigger {
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
.tempo-login-button > span {
|
||||
max-width: 6rem;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,16 @@ import { Link, router, usePage } from '@inertiajs/react';
|
||||
import {
|
||||
ChevronDown,
|
||||
ArrowUpRight,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Menu,
|
||||
UserRound,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
import { Fragment, useEffect, useId, useRef, useState } from 'react';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { useNavigationContext } from '@/components/navigation-context';
|
||||
import { TempoSocialLinks, useTempoUi } from '@/components/tempo-brand';
|
||||
import { tempoSocialLinks, useTempoUi } from '@/components/tempo-brand';
|
||||
import {
|
||||
TempoLanguageSelector,
|
||||
TempoWeather,
|
||||
@@ -50,6 +52,7 @@ export function PortalGroupedMenu() {
|
||||
opportunityManagement?: OpportunityManagementAccess | null;
|
||||
}>();
|
||||
const { t, language, requestCount } = useTempoUi();
|
||||
const mobile = useIsMobile();
|
||||
const navigation = useNavigationContext();
|
||||
const groups = portalNavigation({
|
||||
context: navigation.view
|
||||
@@ -69,24 +72,34 @@ export function PortalGroupedMenu() {
|
||||
const currentItem = currentNavigationItem(groups, url);
|
||||
const [loggingOut, setLoggingOut] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const institutionalId = useId();
|
||||
const navigationId = useId();
|
||||
const toolsId = useId();
|
||||
const interestId = useId();
|
||||
const toggle = useRef<HTMLButtonElement>(null);
|
||||
useEffect(() => router.on('navigate', () => setExpanded(false)), []);
|
||||
const items = (children: PortalNavItem[]) =>
|
||||
children.map((item) =>
|
||||
item.children ? (
|
||||
<DropdownMenuSub key={item.id}>
|
||||
<DropdownMenuSubTrigger className="tempo-menu-item font-semibold">
|
||||
{t(item.label)}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
collisionPadding={12}
|
||||
className="tempo-dropdown max-h-[min(70vh,34rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
|
||||
>
|
||||
mobile ? (
|
||||
<Fragment key={item.id}>
|
||||
<DropdownMenuLabel className="mt-2 border-t px-3 pt-3 text-xs text-slate-500">
|
||||
{t(item.label)}
|
||||
</DropdownMenuLabel>
|
||||
{items(item.children)}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</Fragment>
|
||||
) : (
|
||||
<DropdownMenuSub key={item.id}>
|
||||
<DropdownMenuSubTrigger className="tempo-menu-item font-semibold">
|
||||
{t(item.label)}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
collisionPadding={12}
|
||||
className="tempo-dropdown tempo-submenu max-h-[min(70vh,34rem)] w-72 overflow-y-auto p-2"
|
||||
>
|
||||
{items(item.children)}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
)
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
key={item.id}
|
||||
@@ -108,6 +121,8 @@ export function PortalGroupedMenu() {
|
||||
(group) =>
|
||||
!['home', 'discovery', 'realization', 'support'].includes(group.id),
|
||||
);
|
||||
const panelLabel = `${t('O meu painel')} — ${requestCount} ${t(requestCount === 1 ? 'pedido recebido' : 'pedidos recebidos')}`;
|
||||
const institutionalCurrent = url.split('?')[0] === '/contacto';
|
||||
return (
|
||||
<header
|
||||
className="tempo-header"
|
||||
@@ -117,251 +132,303 @@ export function PortalGroupedMenu() {
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
expanded &&
|
||||
!event.defaultPrevented
|
||||
!event.defaultPrevented &&
|
||||
!(
|
||||
event.target instanceof Element &&
|
||||
event.target.closest('[role="menu"]')
|
||||
)
|
||||
) {
|
||||
setExpanded(false);
|
||||
toggle.current?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="tempo-header-layout">
|
||||
<div className="tempo-header-inner">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="Tempo — início"
|
||||
className="tempo-logo-link"
|
||||
>
|
||||
<img
|
||||
src="/images/logo/logo-vermelho.png"
|
||||
alt="Tempo"
|
||||
width="1787"
|
||||
height="564"
|
||||
className="tempo-header-logo"
|
||||
/>
|
||||
</Link>
|
||||
<nav
|
||||
id={institutionalId}
|
||||
aria-label="Navegação institucional"
|
||||
className="tempo-primary-navigation"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
<Link
|
||||
href="/"
|
||||
className="tempo-nav-trigger"
|
||||
aria-current={
|
||||
url.split('?')[0] === '/'
|
||||
? 'page'
|
||||
: undefined
|
||||
}
|
||||
data-current-group={
|
||||
url.split('?')[0] === '/' || undefined
|
||||
}
|
||||
>
|
||||
{t('Início')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://revista.tempo.co.mz/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="tempo-nav-trigger"
|
||||
aria-label={`${t('Revista Tempo')} — ${t('Abre num novo separador')}`}
|
||||
>
|
||||
{t('Revista Tempo')}
|
||||
<ArrowUpRight
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
{[
|
||||
['/sobre', 'Sobre nós'],
|
||||
['/contacto', 'Contacto'],
|
||||
].map(([href, label]) => (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
className="tempo-nav-trigger"
|
||||
aria-current={
|
||||
url.split('?')[0] === href
|
||||
? 'page'
|
||||
: undefined
|
||||
}
|
||||
data-current-group={
|
||||
url.split('?')[0] === href ||
|
||||
undefined
|
||||
}
|
||||
>
|
||||
{t(label)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div id={toolsId} className="tempo-header-extras">
|
||||
<div className="tempo-header-utilities">
|
||||
<div className="flex items-center gap-1">
|
||||
<TempoWeather />
|
||||
<TempoLanguageSelector />
|
||||
</div>
|
||||
<TempoSocialLinks />
|
||||
</div>
|
||||
</div>
|
||||
<div className="tempo-header-actions">
|
||||
{props.auth?.user ? (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger className="tempo-login-button">
|
||||
<UserRound size={16} aria-hidden="true" />
|
||||
<span className="max-w-24 truncate">
|
||||
{props.auth.user.name ||
|
||||
t('A minha área')}
|
||||
</span>
|
||||
<ChevronDown size={12} aria-hidden="true" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={12}
|
||||
collisionPadding={12}
|
||||
className="tempo-dropdown max-h-[75vh] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
|
||||
>
|
||||
<DropdownMenuLabel className="truncate text-xs text-slate-500">
|
||||
{props.auth.user.email}
|
||||
</DropdownMenuLabel>
|
||||
{items(accountGroups)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="tempo-menu-item text-red-700"
|
||||
>
|
||||
<Link
|
||||
href={logout()}
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={loggingOut}
|
||||
data-test="portal-logout-button"
|
||||
onClick={() => router.flushAll()}
|
||||
onStart={() => setLoggingOut(true)}
|
||||
onFinish={() =>
|
||||
setLoggingOut(false)
|
||||
<div className="tempo-header-inner">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="Tempo — início"
|
||||
className="tempo-logo-link"
|
||||
>
|
||||
<img
|
||||
src="/images/logo/logo-vermelho.png"
|
||||
alt="Tempo"
|
||||
width="1787"
|
||||
height="564"
|
||||
className="tempo-header-logo"
|
||||
/>
|
||||
</Link>
|
||||
<nav
|
||||
id={navigationId}
|
||||
aria-label="Áreas do portal"
|
||||
className="tempo-primary-navigation"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
<Link
|
||||
href="/"
|
||||
className="tempo-nav-trigger"
|
||||
aria-current={
|
||||
url.split('?')[0] === '/'
|
||||
? 'page'
|
||||
: undefined
|
||||
}
|
||||
data-current-group={
|
||||
url.split('?')[0] === '/' || undefined
|
||||
}
|
||||
>
|
||||
{t('Início')}
|
||||
</Link>
|
||||
</li>
|
||||
{groups
|
||||
.filter((group) =>
|
||||
[
|
||||
'discovery',
|
||||
'realization',
|
||||
'support',
|
||||
].includes(group.id),
|
||||
)
|
||||
.map((group) => (
|
||||
<li key={group.id}>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
className="tempo-nav-trigger"
|
||||
data-current-group={
|
||||
current === group.id ||
|
||||
undefined
|
||||
}
|
||||
>
|
||||
<LogOut
|
||||
size={16}
|
||||
{t(group.label)}
|
||||
<ChevronDown
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{loggingOut
|
||||
? 'A terminar sessão…'
|
||||
: t('Terminar sessão')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Link href="/login" className="tempo-login-button">
|
||||
<UserRound size={15} aria-hidden="true" />
|
||||
{t('Entrar')}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
ref={toggle}
|
||||
type="button"
|
||||
className="tempo-header-toggle"
|
||||
aria-label={
|
||||
expanded
|
||||
? 'Fechar menu e ferramentas'
|
||||
: 'Abrir menu e ferramentas'
|
||||
}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={`${institutionalId} ${toolsId}`}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<X size={22} aria-hidden="true" />
|
||||
) : (
|
||||
<Menu size={22} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="tempo-section-bar">
|
||||
<div className="tempo-section-inner">
|
||||
<Link
|
||||
href="/investidor/manifestacoes"
|
||||
className="tempo-panel-summary"
|
||||
>
|
||||
<span>{t('O meu painel')}</span>
|
||||
<span
|
||||
className="tempo-panel-count"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
{current === group.id && (
|
||||
<span className="sr-only">
|
||||
Grupo actual
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
sideOffset={12}
|
||||
collisionPadding={12}
|
||||
className="tempo-dropdown max-h-[min(75vh,36rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
|
||||
>
|
||||
{items(group.children ?? [])}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<Link
|
||||
href="/sobre"
|
||||
className="tempo-nav-trigger"
|
||||
aria-current={
|
||||
url.split('?')[0] === '/sobre'
|
||||
? 'page'
|
||||
: undefined
|
||||
}
|
||||
data-current-group={
|
||||
url.split('?')[0] === '/sobre' || undefined
|
||||
}
|
||||
>
|
||||
{requestCount}{' '}
|
||||
{t(
|
||||
requestCount === 1
|
||||
? 'pedido recebido'
|
||||
: 'pedidos recebidos',
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
<nav
|
||||
aria-label="Áreas do portal"
|
||||
className="tempo-section-navigation"
|
||||
>
|
||||
<ul>
|
||||
{groups
|
||||
.filter((group) =>
|
||||
[
|
||||
'discovery',
|
||||
'realization',
|
||||
'support',
|
||||
].includes(group.id),
|
||||
)
|
||||
.map((group) => (
|
||||
<li key={group.id}>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
className="tempo-nav-trigger"
|
||||
data-current-group={
|
||||
current === group.id ||
|
||||
undefined
|
||||
{t('Sobre nós')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
className="tempo-nav-trigger"
|
||||
data-current-group={
|
||||
institutionalCurrent || undefined
|
||||
}
|
||||
>
|
||||
Tempo
|
||||
<ChevronDown size={14} aria-hidden="true" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
sideOffset={12}
|
||||
collisionPadding={12}
|
||||
className="tempo-dropdown max-h-[75vh] w-64 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="tempo-menu-item"
|
||||
>
|
||||
<a
|
||||
href="https://revista.tempo.co.mz/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${t('Revista Tempo')} — ${t('Abre num novo separador')}`}
|
||||
>
|
||||
{t('Revista Tempo')}
|
||||
<ArrowUpRight
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
{[['/contacto', 'Contacto']].map(
|
||||
([href, label]) => (
|
||||
<DropdownMenuItem
|
||||
key={href}
|
||||
asChild
|
||||
className="tempo-menu-item"
|
||||
>
|
||||
<Link
|
||||
href={href}
|
||||
aria-current={
|
||||
url.split('?')[0] ===
|
||||
href
|
||||
? 'page'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{t(group.label)}
|
||||
<ChevronDown
|
||||
size={14}
|
||||
{t(label)}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
),
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-slate-500">
|
||||
Revista Tempo
|
||||
</DropdownMenuLabel>
|
||||
{tempoSocialLinks.map(
|
||||
({ name, href, icon: Icon }) => (
|
||||
<DropdownMenuItem
|
||||
key={name}
|
||||
asChild
|
||||
className="tempo-menu-item"
|
||||
>
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`Revista Tempo — ${name}`}
|
||||
>
|
||||
<Icon
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{current === group.id && (
|
||||
<span className="sr-only">
|
||||
Grupo actual
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={12}
|
||||
className="tempo-dropdown max-h-[min(75vh,36rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
|
||||
>
|
||||
{items(
|
||||
group.children ?? [],
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<Link
|
||||
href="/manifestar-interesse"
|
||||
className="tempo-interest-button"
|
||||
>
|
||||
{t('Manifestar interesse')}
|
||||
</Link>
|
||||
{name}
|
||||
<ArrowUpRight
|
||||
size={12}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
),
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div id={toolsId} className="tempo-header-extras">
|
||||
<div className="tempo-header-utilities">
|
||||
<div className="flex items-center gap-1">
|
||||
<TempoWeather />
|
||||
<TempoLanguageSelector />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tempo-header-actions">
|
||||
<Link
|
||||
href="/investidor/manifestacoes"
|
||||
className="tempo-panel-summary"
|
||||
aria-label={panelLabel}
|
||||
title={panelLabel}
|
||||
>
|
||||
<LayoutDashboard size={19} aria-hidden="true" />
|
||||
<span className="tempo-panel-count" aria-hidden="true">
|
||||
{requestCount}
|
||||
</span>
|
||||
</Link>
|
||||
<span
|
||||
className="sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{panelLabel}
|
||||
</span>
|
||||
{props.auth?.user ? (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger className="tempo-login-button">
|
||||
<UserRound size={16} aria-hidden="true" />
|
||||
<span className="max-w-24 truncate">
|
||||
{props.auth.user.name || t('A minha área')}
|
||||
</span>
|
||||
<ChevronDown size={12} aria-hidden="true" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={12}
|
||||
collisionPadding={12}
|
||||
className="tempo-dropdown max-h-[75vh] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
|
||||
>
|
||||
<DropdownMenuLabel className="truncate text-xs text-slate-500">
|
||||
{props.auth.user.email}
|
||||
</DropdownMenuLabel>
|
||||
{items(accountGroups)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="tempo-menu-item text-red-700"
|
||||
>
|
||||
<Link
|
||||
href={logout()}
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={loggingOut}
|
||||
data-test="portal-logout-button"
|
||||
onClick={() => router.flushAll()}
|
||||
onStart={() => setLoggingOut(true)}
|
||||
onFinish={() => setLoggingOut(false)}
|
||||
>
|
||||
<LogOut size={16} aria-hidden="true" />
|
||||
{loggingOut
|
||||
? 'A terminar sessão…'
|
||||
: t('Terminar sessão')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Link href="/login" className="tempo-login-button">
|
||||
<UserRound size={15} aria-hidden="true" />
|
||||
{t('Entrar')}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
id={interestId}
|
||||
href="/manifestar-interesse"
|
||||
className="tempo-interest-button"
|
||||
>
|
||||
{t('Manifestar interesse')}
|
||||
<ArrowUpRight size={15} aria-hidden="true" />
|
||||
</Link>
|
||||
<button
|
||||
ref={toggle}
|
||||
type="button"
|
||||
className="tempo-header-toggle"
|
||||
aria-label={
|
||||
expanded
|
||||
? 'Fechar menu e ferramentas'
|
||||
: 'Abrir menu e ferramentas'
|
||||
}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={`${navigationId} ${toolsId} ${interestId}`}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<X size={22} aria-hidden="true" />
|
||||
) : (
|
||||
<Menu size={22} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
// Isolated UI fixture: real header/providers, synthetic Inertia props, no database.
|
||||
// Requires the local Vite server. No requests to Laravel or external services.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
import { portalNavigation } from '../resources/js/lib/portal-navigation.ts';
|
||||
|
||||
const root = process.cwd();
|
||||
const dir = path.join(root, 'outputs/header-navbar-20261021');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const require = createRequire(import.meta.url);
|
||||
const { chromium } = require(process.env.HEADER_QA_PLAYWRIGHT || 'playwright');
|
||||
const { expect } = require(
|
||||
(process.env.HEADER_QA_PLAYWRIGHT || 'playwright') + '/test',
|
||||
);
|
||||
const base = process.env.HEADER_QA_VITE || 'http://127.0.0.1:5173';
|
||||
assert.equal(new URL(base).hostname, '127.0.0.1');
|
||||
const before = process.argv.includes('--before');
|
||||
if (before && fs.existsSync(path.join(dir, 'before-results.json'))) {
|
||||
throw new Error(
|
||||
'The original baseline already exists and must be preserved.',
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'fixture.html'),
|
||||
`<!doctype html>
|
||||
<html lang="pt"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>InvesteMoz · revisão do header</title></head><body><div id="app"></div>
|
||||
<script type="module" src="/outputs/header-navbar-20261021/fixture.tsx"></script></body></html>`,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'fixture.tsx'),
|
||||
`
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { createInertiaApp } from '@inertiajs/react';
|
||||
import { PortalGroupedMenu } from '../../resources/js/components/portal-grouped-menu';
|
||||
import { TempoUiProvider, useTempoUi } from '../../resources/js/components/tempo-brand';
|
||||
import '../../resources/css/app.css';
|
||||
const role = new URLSearchParams(location.search).get('role') || 'guest';
|
||||
const props = {
|
||||
auth: { user: role === 'guest' ? null : { id: 1, name: 'Investidor de demonstração com nome extenso', email: 'qa@example.invalid' }, canManagePortal: role === 'manager' },
|
||||
existingBusinessEnabled: true, businessReviewUrl: role === 'manager' ? '/management/negocios' : null,
|
||||
educationEnabled: true, aiEnabled: true, legacyContentEnabled: true,
|
||||
};
|
||||
function Fixture() {
|
||||
const ui = useTempoUi();
|
||||
return <><PortalGroupedMenu /><main style={{padding:'48px 24px',maxWidth:1200,margin:'auto'}}>
|
||||
<p style={{color:'#b91c1c',fontSize:12,fontWeight:600,letterSpacing:2}}>INVESTEMOZ · REVISÃO LOCAL</p>
|
||||
<h1 style={{fontSize:36,fontWeight:600,margin:'16px 0'}}>O seu próximo investimento começa aqui</h1>
|
||||
<p style={{color:'#64748b'}}>Navegação real do portal com dados fictícios para testar a apresentação.</p>
|
||||
<button data-test="increment" onClick={ui.registerSubmittedInterest}>Simular pedido no contador</button>
|
||||
</main></>;
|
||||
}
|
||||
await createInertiaApp({page:{component:'fixture',props,url:'/',version:null},resolve:()=>Fixture,
|
||||
setup({el,App,props}) {createRoot(el).render(<TempoUiProvider><App {...props}/></TempoUiProvider>);}});
|
||||
`,
|
||||
);
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(process.env.HEADER_QA_CHROMIUM
|
||||
? { executablePath: process.env.HEADER_QA_CHROMIUM }
|
||||
: {}),
|
||||
});
|
||||
const results = [];
|
||||
const errors = [];
|
||||
try {
|
||||
for (const [width, role, language] of before
|
||||
? [
|
||||
[1440, 'guest', 'pt'],
|
||||
[390, 'guest', 'pt'],
|
||||
]
|
||||
: [
|
||||
[1280, 'manager', 'fr'],
|
||||
[1280, 'manager', 'pt'],
|
||||
[1440, 'manager', 'fr'],
|
||||
[320, 'guest', 'pt'],
|
||||
[320, 'investor', 'pt'],
|
||||
[390, 'guest', 'pt'],
|
||||
[390, 'manager', 'pt'],
|
||||
[768, 'investor', 'pt'],
|
||||
[1024, 'manager', 'pt'],
|
||||
[1440, 'guest', 'pt'],
|
||||
[1920, 'investor', 'en'],
|
||||
]) {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width, height: 900 },
|
||||
});
|
||||
await context.addInitScript(
|
||||
(lang) => localStorage.setItem('tempo-navigation-language', lang),
|
||||
language,
|
||||
);
|
||||
await context.route('**/*', (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
if (url.origin === base && url.pathname.startsWith('/images/')) {
|
||||
const asset = path.resolve(root, 'public', `.${url.pathname}`);
|
||||
if (
|
||||
asset.startsWith(
|
||||
path.join(root, 'public', 'images') + path.sep,
|
||||
) &&
|
||||
fs.existsSync(asset)
|
||||
) {
|
||||
return route.fulfill({ path: asset });
|
||||
}
|
||||
}
|
||||
if (url.origin === base || url.protocol === 'data:')
|
||||
return route.continue();
|
||||
return route.abort();
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.error(e.message);
|
||||
});
|
||||
await page.goto(
|
||||
`${base}/outputs/header-navbar-20261021/fixture.html?role=${role}`,
|
||||
{ waitUntil: 'domcontentloaded', timeout: 60000 },
|
||||
);
|
||||
const header = page.locator('.tempo-header');
|
||||
await header.waitFor();
|
||||
await page.screenshot({
|
||||
path: path.join(
|
||||
dir,
|
||||
`${before ? 'before' : 'after'}-${width}-${role}-${language}.png`,
|
||||
),
|
||||
});
|
||||
if (before) {
|
||||
results.push({
|
||||
width,
|
||||
height: await header.evaluate(
|
||||
(el) => el.getBoundingClientRect().height,
|
||||
),
|
||||
});
|
||||
await context.close();
|
||||
continue;
|
||||
}
|
||||
assert.equal(
|
||||
await header.locator('nav').count(),
|
||||
1,
|
||||
'One navigation landmark in header',
|
||||
);
|
||||
assert.equal(await header.locator('.tempo-section-bar').count(), 0);
|
||||
const collapsedHeight = await header.evaluate(
|
||||
(el) => el.getBoundingClientRect().height,
|
||||
);
|
||||
assert.ok(collapsedHeight <= 80, `Header height ${collapsedHeight}`);
|
||||
if (width < 1280) {
|
||||
const toggle = header.locator('.tempo-header-toggle');
|
||||
await toggle.click();
|
||||
assert.equal(await toggle.getAttribute('aria-expanded'), 'true');
|
||||
await page.screenshot({
|
||||
path: path.join(
|
||||
dir,
|
||||
`expanded-${width}-${role}-${language}.png`,
|
||||
),
|
||||
});
|
||||
await page.keyboard.press('Escape');
|
||||
assert.equal(await toggle.getAttribute('aria-expanded'), 'false');
|
||||
await expect(toggle).toBeFocused();
|
||||
await toggle.click();
|
||||
}
|
||||
const nav = header.locator('nav');
|
||||
if (width >= 1280) {
|
||||
const boxes = await Promise.all(
|
||||
[
|
||||
'.tempo-logo-link',
|
||||
'.tempo-primary-navigation',
|
||||
'.tempo-header-extras',
|
||||
'.tempo-header-actions',
|
||||
'.tempo-interest-button',
|
||||
].map((selector) => header.locator(selector).boundingBox()),
|
||||
);
|
||||
for (let i = 1; i < boxes.length; i++)
|
||||
assert.ok(
|
||||
boxes[i - 1].x + boxes[i - 1].width <= boxes[i].x + 1,
|
||||
`Header elements overlap at ${width}px: ${JSON.stringify(boxes)}`,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
await nav
|
||||
.locator(':scope > ul > li')
|
||||
.evaluateAll((nodes) =>
|
||||
nodes.map(
|
||||
(node) =>
|
||||
node
|
||||
.querySelector('a,button')
|
||||
?.getAttribute('href') ||
|
||||
node.textContent.trim(),
|
||||
),
|
||||
),
|
||||
language === 'pt'
|
||||
? [
|
||||
'/',
|
||||
'Descoberta',
|
||||
'Realização',
|
||||
'Suporte',
|
||||
'/sobre',
|
||||
'Tempo',
|
||||
]
|
||||
: language === 'fr'
|
||||
? [
|
||||
'/',
|
||||
'Découvrir',
|
||||
'Investir',
|
||||
'Assistance',
|
||||
'/sobre',
|
||||
'Tempo',
|
||||
]
|
||||
: ['/', 'Discover', 'Invest', 'Support', '/sobre', 'Tempo'],
|
||||
);
|
||||
assert.equal(await nav.locator('a[href="/sobre"]').count(), 1);
|
||||
assert.equal(
|
||||
await nav
|
||||
.locator('a')
|
||||
.first()
|
||||
.evaluate((el) => getComputedStyle(el).fontSize),
|
||||
width >= 1280 ? '18px' : '16px',
|
||||
);
|
||||
const destinations = new Set(
|
||||
await header
|
||||
.locator('a[href]')
|
||||
.evaluateAll((nodes) =>
|
||||
nodes.map((node) => node.getAttribute('href')),
|
||||
),
|
||||
);
|
||||
async function inspectMenu(menu) {
|
||||
await menu.waitFor();
|
||||
menu = page.locator(`[id="${await menu.getAttribute('id')}"]`);
|
||||
try {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const box = await menu.boundingBox();
|
||||
return (
|
||||
!!box &&
|
||||
box.x >= 0 &&
|
||||
box.x + box.width <= width + 1
|
||||
);
|
||||
})
|
||||
.toBe(true);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
width,
|
||||
role,
|
||||
menu: await menu.getAttribute('aria-labelledby'),
|
||||
box: await menu.boundingBox(),
|
||||
}),
|
||||
);
|
||||
await page.screenshot({
|
||||
path: path.join(dir, 'submenu-failure.png'),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
for (const href of await menu
|
||||
.locator('a[href]')
|
||||
.evaluateAll((nodes) =>
|
||||
nodes.map((node) => node.getAttribute('href')),
|
||||
))
|
||||
destinations.add(href);
|
||||
const subIds = await menu
|
||||
.locator('[role="menuitem"][aria-haspopup="menu"]')
|
||||
.evaluateAll((nodes) => nodes.map((node) => node.id));
|
||||
for (const subId of subIds) {
|
||||
const sub = page.locator(`[id="${subId}"]`);
|
||||
await sub.focus();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(sub).toHaveAttribute('aria-expanded', 'true');
|
||||
await inspectMenu(page.locator('[role="menu"]').last());
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(sub).toBeFocused();
|
||||
}
|
||||
}
|
||||
for (const trigger of await nav
|
||||
.locator(':scope > ul > li > button')
|
||||
.all()) {
|
||||
await trigger.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
assert.equal(await trigger.getAttribute('aria-expanded'), 'true');
|
||||
const menu = page.locator('[role="menu"]').last();
|
||||
await inspectMenu(menu);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(trigger).toBeFocused();
|
||||
}
|
||||
await header
|
||||
.getByRole('button', { name: 'Tempo', exact: true })
|
||||
.click();
|
||||
for (const href of [
|
||||
'/contacto',
|
||||
'https://revista.tempo.co.mz/',
|
||||
'https://www.facebook.com/revistatempo.mz/',
|
||||
'https://www.instagram.com/revistatempo.mz/',
|
||||
'https://www.linkedin.com/company/revistatempo/',
|
||||
'https://www.youtube.com/@RevistaTempoMz/',
|
||||
]) {
|
||||
assert.equal(
|
||||
await page.locator(`[role="menu"] a[href="${href}"]`).count(),
|
||||
1,
|
||||
href,
|
||||
);
|
||||
}
|
||||
await page.keyboard.press('Escape');
|
||||
assert.ok(
|
||||
await header
|
||||
.getByRole('button', { name: 'Idioma da navegação' })
|
||||
.isVisible(),
|
||||
);
|
||||
assert.ok(
|
||||
await header
|
||||
.getByRole('button', { name: 'Temperatura em Maputo' })
|
||||
.isVisible(),
|
||||
);
|
||||
assert.ok(
|
||||
await header.locator('a[href="/manifestar-interesse"]').isVisible(),
|
||||
);
|
||||
const dashboard = header.locator('a[href="/investidor/manifestacoes"]');
|
||||
assert.ok(await dashboard.isVisible());
|
||||
await page.locator('[data-test="increment"]').click();
|
||||
assert.match(await dashboard.getAttribute('aria-label'), /1 /);
|
||||
if (role !== 'guest') {
|
||||
await header.locator('.tempo-login-button').click();
|
||||
await inspectMenu(page.locator('[role="menu"]').last());
|
||||
assert.ok(
|
||||
await page
|
||||
.locator('[data-test="portal-logout-button"]')
|
||||
.isVisible(),
|
||||
);
|
||||
const hasManagement = await page
|
||||
.getByRole('menu')
|
||||
.getByText('Gestão', { exact: true })
|
||||
.count();
|
||||
assert.equal(hasManagement > 0, role === 'manager');
|
||||
await page.keyboard.press('Escape');
|
||||
} else {
|
||||
assert.ok(await header.locator('a[href="/login"]').isVisible());
|
||||
}
|
||||
const catalog = portalNavigation({
|
||||
authenticated: role !== 'guest',
|
||||
manager: role === 'manager',
|
||||
businesses: true,
|
||||
businessReviewUrl:
|
||||
role === 'manager' ? '/management/negocios' : null,
|
||||
education: true,
|
||||
ai: true,
|
||||
legacyContent: true,
|
||||
});
|
||||
function checkDestinations(groups) {
|
||||
for (const group of groups) {
|
||||
if (group.children) checkDestinations(group.children);
|
||||
else if (group.href)
|
||||
assert.ok(
|
||||
destinations.has(group.href),
|
||||
`Preserved destination: ${group.href}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
checkDestinations(
|
||||
role === 'guest'
|
||||
? catalog.filter((group) => group.id !== 'account')
|
||||
: catalog,
|
||||
);
|
||||
const overflow = await header.evaluate((el) =>
|
||||
Array.from(el.querySelectorAll('a,button,img'))
|
||||
.filter((n) => {
|
||||
const r = n.getBoundingClientRect();
|
||||
return r.width && (r.x < -1 || r.right > innerWidth + 1);
|
||||
})
|
||||
.map((n) => n.textContent),
|
||||
);
|
||||
assert.deepEqual(overflow, []);
|
||||
results.push({
|
||||
width,
|
||||
role,
|
||||
language,
|
||||
collapsedHeight,
|
||||
destinations: destinations.size,
|
||||
result: 'passed',
|
||||
});
|
||||
await context.close();
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
fs.writeFileSync(
|
||||
path.join(dir, before ? 'before-results.json' : 'qa-results.json'),
|
||||
JSON.stringify({ results, errors }, null, 2),
|
||||
);
|
||||
console.log(JSON.stringify({ results, errors }));
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user