feat(header): add UtilityBar + Marquee + Navigation from v6-nav

- UtilityBar.astro (v6-utility): phone, clock, date, email from site settings
  Clock updated by fxClock() in src/lib/fx-animations.ts
- Marquee.astro (v6-marquee): log ticker with 4 entries (animated horizontally)
  Content duplicated for seamless loop
- Navigation.astro (v6-nav): REPLACED legacy. Adds 'บริการ' dropdown
  (4 services) + 'บทความ' link per plan round 2. Click-to-toggle on mobile.
- src/data/nav.ts: single source of truth for mainLinks + servicesDropdown
- fx-system.css: +27 lines for dropdown styles + active link underline
  (v6-nav originally had no dropdown — we added per spec)

Refs: .hermes/plans/2026-06-13_124000-moreminimore-v7-5-migration.md Task 2.1-2.4
This commit is contained in:
Kunthawat Greethong
2026-06-13 17:47:40 +07:00
parent 582998a340
commit 1f859921cb
5 changed files with 197 additions and 515 deletions

View File

@@ -0,0 +1,30 @@
---
/**
* MOREMINIMORE - Marquee (from v6-marquee)
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 590-609
*
* Log ticker — animates horizontally (marquee 40s linear infinite in fx-system.css)
* Content is hardcoded ticker entries (not dynamic) — same as v7-5 design
*
* Duplicate content to achieve seamless loop (track is rendered twice)
*/
const tickerEntries = [
{ ts: '[2026-06-13]', text: '<span class="cmd">$</span> build/deploy <span class="ok">✓</span>' },
{ ts: '[log]', text: 'Dataroot +373% Impression <span class="ok">✓</span>' },
{ ts: '[2026-06-13]', text: '<span class="cmd">$</span> contact/free <em>30 min</em> <span class="ok">✓</span>' },
{ ts: '[log]', text: '9 case studies, 0 fabricated <span class="ok">✓</span>' },
];
---
<div id="v6-marquee-inner" class="fx-marquee">
<div class="fx-marquee-track">
{/* First copy */}
{tickerEntries.map((entry) => (
<span><span class="ts">{entry.ts}</span> <Fragment set:html={entry.text} /></span>
))}
{/* Second copy (duplicated for seamless loop) */}
{tickerEntries.map((entry) => (
<span><span class="ts">{entry.ts}</span> <Fragment set:html={entry.text} /></span>
))}
</div>
</div>

View File

@@ -1,529 +1,82 @@
---
/**
* MOREMINIMORE - NAVIGATION COMPONENT (LIGHT THEME)
* Light bg + dark text nav links + white logo banner
* MOREMINIMORE - NAVIGATION (from v6-nav, enhanced)
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 662-684
*
* Per plan 2026-06-13 round 2:
* - v6-nav structure: logo + menu + CTA
* - Enhancement A: dropdown 'บริการ' (4 sub-items from servicesDropdown)
* - Enhancement C: add 'บทความ' link (blog) — not in original v6-nav
* - Social icons: not in v6-nav itself; they live in UtilityBar (per plan)
*
* Props:
* - currentPath: string — for active link highlighting (optional, defaults to Astro.url.pathname)
*/
import { mainLinks, servicesDropdown } from '../data/nav';
const services = [
{ label: 'Website Development', href: '/services/webdev' },
{ label: 'Marketing Automation', href: '/services/marketing' },
{ label: 'AI Automation', href: '/services/automation' },
{ label: 'Tech Consult', href: '/services/ai-consult' },
];
interface Props {
currentPath?: string;
}
const { currentPath = Astro.url.pathname } = Astro.props;
const isActive = (href: string) => {
if (href === '/') return currentPath === '/';
return currentPath.startsWith(href);
};
---
<header class="nav" id="nav">
<div class="nav-container container">
<!-- Logo: white-bg banner with the black-text logo (works on light/yellow) -->
<a href="/" class="nav-logo">
<div class="logo-banner">
<img src="/images/logo-long-black.png" alt="MoreminiMore" class="logo-img" />
</div>
<nav id="v6-nav-inner" class="fx-nav">
<div class="fx-nav-inner">
<a href="/" class="fx-nav-logo">
<span class="fx-nav-logo-text">MOREMI<em>ni</em>MORE</span>
</a>
<!-- Desktop Navigation -->
<nav class="nav-desktop">
<a href="/" class="nav-link" data-magnetic>หน้าแรก</a>
<!-- Services with Submenu -->
<div class="nav-dropdown">
<a href="/services" class="nav-link nav-dropdown-trigger" data-magnetic>
บริการ
<svg class="dropdown-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</a>
<div class="nav-dropdown-menu">
{services.map(service => (
<a href={service.href} class="dropdown-item">
{service.label}
<ul class="fx-nav-menu">
{mainLinks.map((link) => (
<li class={link.hasDropdown ? 'fx-nav-has-dropdown' : ''}>
{link.hasDropdown ? (
<>
<a href={link.href} class:list={[isActive(link.href) && 'active']}>
{link.label}
</a>
<ul class="fx-nav-dropdown">
{servicesDropdown.map((service) => (
<li>
<a href={service.href} class:list={[isActive(service.href) && 'active']}>
{service.label}
</a>
</li>
))}
</ul>
</>
) : (
<a href={link.href} class:list={[isActive(link.href) && 'active']}>
{link.label}
</a>
))}
</div>
</div>
<a href="/portfolio" class="nav-link" data-magnetic>ผลงาน</a>
<a href="/blog" class="nav-link" data-magnetic>บทความ</a>
<a href="/about" class="nav-link" data-magnetic>เกี่ยวกับเรา</a>
<a href="/contact" class="nav-link nav-cta" data-magnetic>ติดต่อเรา</a>
</nav>
<!-- Mobile Menu Toggle (DARK icons on light bg) -->
<button class="nav-toggle" id="nav-toggle" aria-label="Toggle menu">
<span class="toggle-line"></span>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
</button>
)}
</li>
))}
</ul>
<a href="/contact" class="fx-nav-cta">ปรึกษาฟรี →</a>
</div>
<!-- Mobile Navigation Overlay (LIGHT bg) -->
<div class="nav-mobile-overlay" id="nav-mobile">
<nav class="nav-mobile-content">
<a href="/" class="mobile-link">หน้าแรก</a>
<!-- Mobile Services with Submenu -->
<div class="mobile-dropdown">
<button class="mobile-dropdown-trigger">
บริการ
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<div class="mobile-dropdown-content">
{services.map(service => (
<a href={service.href} class="mobile-link mobile-sublink">{service.label}</a>
))}
</div>
</div>
<a href="/portfolio" class="mobile-link">ผลงาน</a>
<a href="/blog" class="mobile-link">บทความ</a>
<a href="/about" class="mobile-link">เกี่ยวกับเรา</a>
<a href="/contact" class="mobile-link mobile-cta">ติดต่อเรา</a>
</nav>
</div>
</header>
<!-- Scroll Progress Bar (yellow) -->
<div class="scroll-progress" id="scroll-progress"></div>
<style>
/* ============================================
NAVIGATION BASE — LIGHT THEME
============================================ */
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
padding: 16px 0;
background: var(--color-white);
border-bottom: 1px solid var(--color-gray-200);
transition: all 0.3s var(--ease-out-expo);
}
.nav.scrolled {
padding: 12px 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
}
/* ============================================
CONTAINER
============================================ */
.nav-container {
display: flex;
align-items: center;
justify-content: space-between;
}
/* ============================================
LOGO BANNER
============================================ */
.nav-logo {
display: flex;
align-items: center;
z-index: 1001;
}
.logo-banner {
background: var(--color-white);
border-radius: 0 0 12px 12px;
padding: 8px 16px;
transition: all 0.3s ease;
}
.logo-banner:hover {
transform: scale(1.02);
}
.nav.scrolled .logo-banner {
border-radius: 0 0 8px 8px;
padding: 6px 12px;
}
.logo-img {
height: 36px;
width: auto;
display: block;
}
.nav.scrolled .logo-img {
height: 28px;
}
/* ============================================
DESKTOP NAVIGATION
============================================ */
.nav-desktop {
display: flex;
align-items: center;
gap: 4px;
}
.nav-link {
position: relative;
padding: 12px 16px;
font-family: var(--font-display);
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--color-black);
transition: color 0.3s ease;
display: flex;
align-items: center;
gap: 4px;
}
.nav-link:hover,
.nav-link.active {
color: var(--color-primary-dark);
}
.nav-cta {
background: var(--color-primary);
color: var(--color-black) !important;
border: 2px solid var(--color-primary);
border-radius: var(--radius-md);
margin-left: 16px;
padding: 12px 24px;
transition: all 0.3s var(--ease-out-expo);
}
.nav-cta:hover {
background: var(--color-primary-dark);
border-color: var(--color-primary-dark);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(254, 212, 0, 0.3);
}
.nav-cta::after { display: none; }
/* ============================================
DROPDOWN — LIGHT CARD ON WHITE BG
============================================ */
.nav-dropdown {
position: relative;
}
.nav-dropdown-trigger {
cursor: pointer;
}
.dropdown-arrow {
width: 16px;
height: 16px;
transition: transform 0.3s ease;
}
.nav-dropdown:hover .dropdown-arrow {
transform: rotate(180deg);
}
.nav-dropdown-menu {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%) translateY(10px);
min-width: 220px;
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
padding: 12px 0;
opacity: 0;
visibility: hidden;
transition: all 0.3s var(--ease-out-expo);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
}
.nav-dropdown:hover .nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
.dropdown-item {
display: block;
padding: 12px 24px;
font-family: var(--font-display);
font-size: 13px;
font-weight: 600;
color: var(--color-gray-700);
transition: all 0.2s ease;
}
.dropdown-item:hover {
background: var(--color-bg-alt);
color: var(--color-primary-dark);
}
/* ============================================
MOBILE TOGGLE — DARK lines (since nav is light)
============================================ */
.nav-toggle {
display: none;
flex-direction: column;
justify-content: center;
gap: 5px;
width: 44px;
height: 44px;
background: none;
border: none;
cursor: pointer;
z-index: 1001;
padding: 10px;
}
.toggle-line {
width: 24px;
height: 2px;
background: var(--color-black);
border-radius: 2px;
transition: all 0.3s var(--ease-out-expo);
}
.nav-toggle.active .toggle-line:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.nav-toggle.active .toggle-line:nth-child(2) {
opacity: 0;
transform: translateX(-10px);
}
.nav-toggle.active .toggle-line:nth-child(3) {
transform: rotate(-45deg) translate(5px, -5px);
}
/* ============================================
MOBILE OVERLAY — LIGHT THEME
============================================ */
.nav-mobile-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--color-white);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: all 0.4s var(--ease-out-expo);
z-index: 999;
}
.nav-mobile-overlay.active {
opacity: 1;
visibility: visible;
}
.nav-mobile-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 40px;
width: 100%;
}
.mobile-link {
font-family: var(--font-display);
font-size: 28px;
font-weight: 800;
color: var(--color-black);
text-transform: uppercase;
letter-spacing: 2px;
padding: 14px 32px;
transition: all 0.3s ease;
opacity: 0;
transform: translateY(20px);
width: 100%;
text-align: center;
}
.nav-mobile-overlay.active .mobile-link {
opacity: 1;
transform: translateY(0);
}
.nav-mobile-overlay.active .mobile-link:nth-child(1) { transition-delay: 0.1s; }
.nav-mobile-overlay.active .mobile-link:nth-child(2) { transition-delay: 0.15s; }
.nav-mobile-overlay.active .mobile-link:nth-child(3) { transition-delay: 0.2s; }
.nav-mobile-overlay.active .mobile-link:nth-child(4) { transition-delay: 0.25s; }
.nav-mobile-overlay.active .mobile-link:nth-child(5) { transition-delay: 0.3s; }
.nav-mobile-overlay.active .mobile-link:nth-child(6) { transition-delay: 0.35s; }
.mobile-link:hover {
color: var(--color-primary-dark);
transform: translateX(10px);
}
.mobile-cta {
background: var(--color-primary);
color: var(--color-black) !important;
border: 2px solid var(--color-primary);
border-radius: var(--radius-lg);
margin-top: 20px;
}
/* ============================================
MOBILE DROPDOWN
============================================ */
.mobile-dropdown {
width: 100%;
text-align: center;
}
.mobile-dropdown-trigger {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 14px 32px;
background: none;
border: none;
font-family: var(--font-display);
font-size: 28px;
font-weight: 800;
color: var(--color-black);
text-transform: uppercase;
letter-spacing: 2px;
cursor: pointer;
transition: color 0.3s ease;
opacity: 0;
transform: translateY(20px);
}
.mobile-dropdown-trigger svg {
width: 20px;
height: 20px;
transition: transform 0.3s ease;
}
.mobile-dropdown.open .mobile-dropdown-trigger {
color: var(--color-primary-dark);
}
.mobile-dropdown.open .mobile-dropdown-trigger svg {
transform: rotate(180deg);
}
.mobile-dropdown-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s var(--ease-out-expo);
}
.mobile-dropdown.open .mobile-dropdown-content {
max-height: 300px;
}
.mobile-sublink {
font-size: 20px !important;
padding: 10px 32px !important;
opacity: 0.7;
}
.mobile-sublink:hover {
opacity: 1;
}
/* ============================================
SCROLL PROGRESS
============================================ */
.scroll-progress {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, var(--color-primary), #ffe066);
width: 0;
z-index: 1001;
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 1024px) {
.nav-desktop {
display: none;
}
.nav-toggle {
display: flex;
}
}
@media (max-width: 640px) {
.mobile-link {
font-size: 24px;
padding: 12px 24px;
}
}
</style>
</nav>
<script>
// Navigation scroll effect
const nav = document.getElementById('nav');
const toggle = document.getElementById('nav-toggle');
const mobileNav = document.getElementById('nav-mobile');
const scrollProgress = document.getElementById('scroll-progress');
// Mobile menu toggle — only relevant on small screens
// Desktop uses hover for dropdown (handled by CSS)
document.addEventListener('DOMContentLoaded', () => {
const nav = document.getElementById('v6-nav-inner');
if (!nav) return;
// Scroll effect
window.addEventListener('scroll', () => {
const scrollY = window.scrollY;
if (scrollY > 80) {
nav?.classList.add('scrolled');
} else {
nav?.classList.remove('scrolled');
}
// Scroll progress
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (scrollY / docHeight) * 100;
if (scrollProgress) {
scrollProgress.style.width = `${progress}%`;
}
});
// Mobile menu toggle
toggle?.addEventListener('click', () => {
toggle.classList.toggle('active');
mobileNav?.classList.toggle('active');
document.body.style.overflow = mobileNav?.classList.contains('active') ? 'hidden' : '';
});
// Mobile dropdown toggle
document.querySelectorAll('.mobile-dropdown-trigger').forEach(btn => {
btn.addEventListener('click', () => {
btn.parentElement?.classList.toggle('open');
});
});
// Close mobile nav on link click
document.querySelectorAll('.nav-mobile-overlay .mobile-link').forEach(link => {
link.addEventListener('click', () => {
toggle?.classList.remove('active');
mobileNav?.classList.remove('active');
document.body.style.overflow = '';
// Find all items with dropdowns and add click-to-toggle for mobile
const dropdownItems = nav.querySelectorAll('.fx-nav-has-dropdown > a');
dropdownItems.forEach((trigger) => {
trigger.addEventListener('click', (e) => {
if (window.innerWidth <= 768) {
e.preventDefault();
const parent = trigger.parentElement;
if (parent) parent.classList.toggle('fx-nav-dropdown-open');
}
});
});
});
</script>

View File

@@ -0,0 +1,30 @@
---
/**
* MOREMINIMORE - UtilityBar (from v6-utility)
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 571-589
*
* Top info bar — phone + clock + date + mode indicator + email
* Phone/email pulled from src/content/settings/site.md (single source of truth)
*
* Clock/date are updated by fx-animations.ts → fxClock() (id="fx-time", id="fx-date")
*/
import { getEntry } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
const site = (await getEntry('settings', 'site')) as CollectionEntry<'settings'>;
const phone = site?.data?.phone ?? '080-995-5945';
const email = site?.data?.email ?? 'contact@moreminimore.com';
---
<div id="v6-utility-inner" class="fx-utility-bar">
<div class="fx-utility-bar-left">
<span class="fx-utility-item">📞 <strong>{phone}</strong></span>
<span class="fx-utility-item" id="fx-time">⏱ — : — : —</span>
<span class="fx-utility-item" id="fx-date">📅 — — —</span>
</div>
<div class="fx-utility-bar-right">
<span class="fx-mode-indicator">system</span>
<span class="fx-theme-toggle" id="fx-theme-toggle">◐ auto</span>
<a href={`mailto:${email}`} class="fx-utility-item" style="text-decoration:none">✉ {email}</a>
</div>
</div>

43
src/data/nav.ts Normal file
View File

@@ -0,0 +1,43 @@
/**
* MOREMINIMORE - Nav data (single source of truth)
* Used by Navigation.astro and Footer.astro
*
* Per plan 2026-06-13 round 2:
* - Main menu: 7 items including 'บทความ' (blog) + 'FAQ' + 'ติดต่อ'
* - Services dropdown: 4 services (matched to content collection slugs)
* - Social: facebook, line, linkedin (from settings collection at runtime)
*
* Slugs match src/content/services/*-new.mdx:
* - ai-consult-new → /services/ai-consult
* - marketing-new → /services/marketing
* - automation-new → /services/automation
* - webdev-new → /services/webdev
*
* Href uses /services/{slug-without-new} to match the [slug].astro route.
*/
export const mainLinks = [
{ label: 'หน้าแรก', href: '/' },
{ label: 'บริการ', href: '/services', hasDropdown: true },
{ label: 'ผลงาน', href: '/portfolio' },
{ label: 'บทความ', href: '/blog' },
{ label: 'เกี่ยวกับ', href: '/about' },
{ label: 'FAQ', href: '/faq' },
{ label: 'ติดต่อ', href: '/contact' },
];
export const servicesDropdown = [
{ label: 'AI Consult', href: '/services/ai-consult' },
{ label: 'Marketing Automation', href: '/services/marketing' },
{ label: 'AI Automation', href: '/services/automation' },
{ label: 'Website Development', href: '/services/webdev' },
];
/**
* Social links are passed as props to the component
* (because they come from the settings collection which is loaded async)
*/
export interface SocialLinks {
facebook?: string;
line?: string;
linkedin?: string;
}

View File

@@ -482,4 +482,30 @@ img{max-width:100%;display:block}
.fx-btn.coral:hover{background:linear-gradient(135deg,var(--ink) 0%,var(--coral) 100%)}
.fx-marquee-track{animation:none}
.fx-faq-a::after{display:none}
.fx-hero::after{display:none}
.fx-hero::after{display:none}
/* ============================================
NAV DROPDOWN ENHANCEMENT (added 2026-06-13, plan round 2)
v6-nav didn't have a dropdown originally; we added
'บริการ' dropdown per user spec. Active link styling too.
============================================ */
.fx-nav-menu{position:relative}
.fx-nav-menu li{position:relative}
.fx-nav-dropdown{position:absolute;top:100%;left:0;min-width:240px;background:var(--paper);border:1.5px solid var(--ink);box-shadow:4px 4px 0 var(--ink);list-style:none;padding:8px 0;margin:0;opacity:0;visibility:hidden;transform:translateY(-4px);transition:opacity 0.18s,transform 0.18s,visibility 0.18s;z-index:60}
.fx-nav-has-dropdown:hover>.fx-nav-dropdown,
.fx-nav-dropdown-open>.fx-nav-dropdown{opacity:1;visibility:visible;transform:translateY(0)}
.fx-nav-dropdown li{margin:0}
.fx-nav-dropdown a{display:block;padding:10px 18px;font:600 12px/1.3 'JetBrains Mono',monospace;text-transform:uppercase;letter-spacing:0.3px;color:var(--ink);text-decoration:none;transition:background 0.15s,color 0.15s}
.fx-nav-dropdown a:hover{background:var(--brand-yellow);color:var(--ink)}
.fx-nav-dropdown a.active{background:var(--coral);color:#FAFAFA}
.fx-nav-menu a.active{color:var(--coral)}
.fx-nav-menu a.active::after{content:'';display:block;height:2px;background:var(--coral);margin-top:4px}
@media (max-width:768px){
.fx-nav-inner{flex-direction:column;align-items:flex-start;padding:12px 16px}
.fx-nav-menu{flex-direction:column;width:100%;gap:0}
.fx-nav-menu>li{width:100%;border-bottom:1px solid var(--line-2)}
.fx-nav-menu>li>a{display:block;padding:12px 0}
.fx-nav-dropdown{position:static;box-shadow:none;border:none;background:var(--paper-2);opacity:1;visibility:visible;transform:none;max-height:0;overflow:hidden;padding:0;transition:max-height 0.25s,padding 0.25s}
.fx-nav-dropdown-open>.fx-nav-dropdown{max-height:400px;padding:8px 0}
.fx-nav-cta{margin-top:12px;align-self:flex-start}
}