Commit 7d260d1c by krds-arun

feat(panel,listing): add MAF-themed panel UI and reusable listings

Includes panel layout (sidebar/header/footer/bottom tab/quick actions), improved login flow hooks, and a mobile-first MAF-themed listing system with Storybook stories.

Made-with: Cursor
parent 6b577681
......@@ -13,6 +13,7 @@ html {
}
body {
margin: 0;
background: var(--background);
color: var(--foreground);
font-feature-settings:
......
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Manrope, Playfair_Display, Syne } from "next/font/google";
import "./globals.css";
......@@ -23,6 +23,12 @@ export const metadata: Metadata = {
description: "Welcome to MAF Revamp",
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
};
export default function RootLayout({
children,
}: Readonly<{
......
......@@ -19,11 +19,12 @@ import {
IconTraining,
} from "@/components/page-sections/CapabilitiesShowcaseSection/capabilityIcons";
import { CapabilitiesShowcaseSection } from "@/components/page-sections/CapabilitiesShowcaseSection/CapabilitiesShowcaseSection";
import { useRouter } from "next/navigation";
export default function Home() {
const [loginOpen, setLoginOpen] = React.useState(false);
const [supportOpen, setSupportOpen] = React.useState(false);
const router = useRouter();
return (
<>
<Header
......@@ -148,6 +149,14 @@ export default function Home() {
onMicrosoftSignIn={() => {
setLoginOpen(false);
}}
onVerifyTwoFactor={
(code: string) => {
console.log(code);
setLoginOpen(false);
router.push("/panel");
}
}
onContactSupport={() => {
setLoginOpen(false);
setSupportOpen(true);
......
"use client";
import { useState } from "react";
import { Sidebar } from "@/components/layout/Sidebar/Sidebar";
import { Header } from "@/components/layout/Header/Header";
import { Footer } from "@/components/layout/Footer/Footer";
import { BottomTab } from "@/components/layout/BottomTab/BottomTab";
import { QuickAction } from "@/components/widgets/QuickAction/QuickAction";
import "./panel.css";
export default function PanelLayout({
children,
}: {
children: React.ReactNode;
}) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
return (
<div className={`maf-panel ${sidebarCollapsed ? "maf-panel--sidebar-collapsed" : ""}`.trim()}>
<aside className="maf-panel__sidebarRail">
<Sidebar />
</aside>
<div className="maf-panel__main">
<Header hideNavMenu hideActions logoSrc="/panel-monogram-logo.png" />
<button
type="button"
className="maf-panel__mobileSidebarButton"
onClick={() => setMobileSidebarOpen(true)}
aria-label="Open sidebar"
>
</button>
<button
type="button"
className="maf-panel__sidebarToggle"
onClick={() => setSidebarCollapsed((value) => !value)}
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{sidebarCollapsed ? "›" : "‹"}
</button>
<main className="maf-panel__content">
<div className="maf-panel__contentInner">{children}</div>
</main>
<Footer
logoSrc="/panel-monogram-logo.png"
hideLinkGroups
panelBar
supportCtaLabel="Support"
supportCtaHref="#"
legalLinks={[
{ label: "Privacy Policy", href: "#" },
{ label: "Terms of Use", href: "#" },
{ label: "Child Privacy Policy", href: "#" },
]}
copyright="Gateway© 2020 - 2026 · Majid Al Futtaim"
/>
</div>
{mobileSidebarOpen ? (
<>
<button
type="button"
className="maf-panel__mobileSidebarBackdrop"
aria-label="Close sidebar backdrop"
onClick={() => setMobileSidebarOpen(false)}
/>
<aside className="maf-panel__mobileSidebarDrawer">
<button
type="button"
className="maf-panel__mobileSidebarClose"
onClick={() => setMobileSidebarOpen(false)}
aria-label="Close sidebar"
>
×
</button>
<Sidebar />
</aside>
</>
) : null}
<BottomTab />
<QuickAction />
</div>
);
}
\ No newline at end of file
export default function Page() {
return (
<div>
<h1>Panel</h1>
</div>
);
}
\ No newline at end of file
.maf-panel {
height: 100dvh;
min-height: 100dvh;
display: grid;
grid-template-columns: 280px 1fr;
background: #070a12;
overflow: hidden;
}
.maf-panel__sidebarRail {
height: 100dvh;
position: sticky;
top: 0;
}
.maf-panel__sidebarRail .maf-sidebar {
min-height: 100dvh;
height: 100dvh;
}
.maf-panel__main {
min-width: 0;
height: 100dvh;
min-height: 100dvh;
display: grid;
grid-template-rows: auto 1fr auto;
position: relative;
overflow: visible;
background: #05070d;
}
.maf-panel .landing-footer {
padding-top: 10px;
padding-bottom: 8px;
}
.maf-panel .landing-footer__inner {
padding: 0 12px;
}
.maf-panel .landing-footer__brandRow {
margin-bottom: 4px;
}
.maf-panel .landing-footer__logoWrap {
width: 146px;
height: 34px;
}
.maf-panel .landing-footer__bottom {
margin-top: 0;
font-size: 0.7rem;
}
.maf-panel .landing-header__logoWrap {
width: 34px;
height: 34px;
}
.maf-panel .landing-header__brand {
gap: 0.4rem;
}
/* Panel footer should not show any refresh action/icon */
.maf-panel .landing-footer [aria-label*="refresh" i],
.maf-panel .landing-footer [title*="refresh" i],
.maf-panel .landing-footer [class*="refresh" i] {
display: none !important;
}
.maf-panel .maf-quick-action__floatingWrap {
right: 1.2rem;
bottom: calc(5.5rem + env(safe-area-inset-bottom));
}
.maf-panel__sidebarToggle {
position: absolute;
top: 78px;
left: 0;
transform: translate(-50%, -50%);
width: 30px;
height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.3);
background: rgba(255, 255, 255, 0.96);
color: rgba(110, 90, 68, 0.9);
box-shadow: 0 8px 20px rgba(110, 90, 68, 0.18);
z-index: 20;
cursor: pointer;
}
.maf-panel__content {
min-width: 0;
min-height: 0;
padding: 1.35rem 1.5rem 2rem;
background: transparent;
color: rgba(255, 255, 255, 0.9);
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.maf-panel__contentInner {
width: min(1180px, 100%);
min-height: fit-content;
margin: 0 auto;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.07);
background:
radial-gradient(880px 320px at 10% 0%, rgba(196, 165, 116, 0.08), transparent 58%),
rgba(7, 10, 18, 0.62);
padding: 1rem 1.1rem;
}
@media (max-width: 1366px) and (orientation: landscape) {
.maf-panel .landing-footer {
padding-top: 6px;
padding-bottom: 6px;
}
.maf-panel .landing-footer__logoWrap {
width: 122px;
height: 28px;
}
.maf-panel .landing-footer__bottom {
font-size: 0.66rem;
}
}
.maf-panel__mobileSidebarButton,
.maf-panel__mobileSidebarBackdrop,
.maf-panel__mobileSidebarDrawer {
display: none;
}
.maf-panel--sidebar-collapsed {
grid-template-columns: 64px 1fr;
}
.maf-panel--sidebar-collapsed .maf-sidebar {
width: 64px;
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.maf-panel--sidebar-collapsed .maf-sidebar__item {
justify-content: center;
}
.maf-panel--sidebar-collapsed .maf-sidebar__item > span:last-child {
display: none;
}
@media (max-width: 1024px) {
.maf-panel {
grid-template-columns: 1fr;
}
.maf-panel.maf-panel--sidebar-collapsed {
grid-template-columns: 1fr;
}
.maf-panel__sidebarRail {
display: none;
}
.maf-panel__sidebarToggle {
display: none;
}
.maf-panel__mobileSidebarButton {
display: inline-flex;
align-items: center;
justify-content: center;
position: absolute;
top: 10px;
left: 8px;
width: 32px;
height: 32px;
border-radius: 9px;
border: 1px solid rgba(110, 90, 68, 0.3);
background: rgba(255, 255, 255, 0.96);
color: rgba(110, 90, 68, 0.9);
z-index: 22;
cursor: pointer;
font-size: 1rem;
line-height: 1;
}
.maf-panel .landing-header__inner {
min-height: 52px;
padding-left: 48px;
padding-right: 10px;
}
.maf-panel .landing-header {
z-index: 46;
}
.maf-panel .landing-header__logoWrap {
width: 32px;
height: 32px;
}
.maf-panel__mobileSidebarBackdrop {
display: block;
position: fixed;
top: 52px;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, 0.32);
border: 0;
z-index: 44;
}
.maf-panel__mobileSidebarDrawer {
display: block;
position: fixed;
left: 0;
top: 52px;
bottom: 0;
width: min(280px, 86vw);
z-index: 45;
}
.maf-panel__mobileSidebarDrawer .maf-sidebar {
min-height: 100%;
height: 100%;
width: 100%;
}
.maf-panel__mobileSidebarClose {
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.3);
background: rgba(255, 255, 255, 0.96);
color: rgba(110, 90, 68, 0.9);
z-index: 2;
cursor: pointer;
}
.maf-panel__content {
padding: 0.8rem 0.65rem 6.2rem;
}
.maf-panel__contentInner {
border-radius: 10px;
padding: 0.75rem 0.8rem;
}
.maf-panel .landing-footer {
display: none;
}
.maf-panel .maf-quick-action__floatingWrap {
right: 0.9rem;
bottom: calc(5.7rem + env(safe-area-inset-bottom));
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { BottomTab } from "./BottomTab";
const meta = {
title: "Layout/Bottom Tab",
component: BottomTab,
parameters: {
layout: "fullscreen",
},
decorators: [
(Story) => (
<div style={{ minHeight: "100vh", background: "var(--ink)" }}>
<Story />
</div>
),
],
} satisfies Meta<typeof BottomTab>;
export default meta;
type Story = StoryObj<typeof meta>;
export const MobileAndTablet: Story = {
render: () => (
<div style={{ minHeight: "100vh", position: "relative", padding: "1rem" }}>
<BottomTab />
</div>
),
parameters: {
viewport: { defaultViewport: "mobile1" },
},
};
"use client";
import "./bottom-tab.css";
export type BottomTabItem = {
label: string;
href?: string;
active?: boolean;
};
export type BottomTabProps = {
items?: BottomTabItem[];
};
function Icon({ label }: { label: string }) {
const key = label.toLowerCase();
if (key === "home") {
return (
<svg className="maf-bottom-tab__iconSvg" viewBox="0 0 24 24" aria-hidden>
<path d="M4 10.4 12 4l8 6.4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path d="M6.4 9.8v9.2h11.2V9.8" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
if (key === "modules") {
return (
<svg className="maf-bottom-tab__iconSvg" viewBox="0 0 24 24" aria-hidden>
<rect x="4" y="4" width="6.5" height="6.5" rx="1.1" fill="none" stroke="currentColor" strokeWidth="1.8" />
<rect x="13.5" y="4" width="6.5" height="6.5" rx="1.1" fill="none" stroke="currentColor" strokeWidth="1.8" />
<rect x="4" y="13.5" width="6.5" height="6.5" rx="1.1" fill="none" stroke="currentColor" strokeWidth="1.8" />
<rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.1" fill="none" stroke="currentColor" strokeWidth="1.8" />
</svg>
);
}
if (key === "incidents") {
return (
<svg className="maf-bottom-tab__iconSvg" viewBox="0 0 24 24" aria-hidden>
<rect x="5" y="3.8" width="14" height="16.4" rx="1.8" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M8.4 9h7.2M8.4 12.5h7.2M8.4 16h5.4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
if (key === "alerts") {
return (
<svg className="maf-bottom-tab__iconSvg" viewBox="0 0 24 24" aria-hidden>
<path d="M12 4.5a4.2 4.2 0 0 0-4.2 4.2v2.4c0 1.4-.4 2.8-1.2 3.9L5.6 16h12.8l-1.1-1c-.8-1.1-1.2-2.5-1.2-3.9V8.7A4.2 4.2 0 0 0 12 4.5Z" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M9.8 18a2.4 2.4 0 0 0 4.4 0" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
return (
<svg className="maf-bottom-tab__iconSvg" viewBox="0 0 24 24" aria-hidden>
<circle cx="12" cy="8" r="3.2" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M6.2 19c.6-2.7 2.8-4.4 5.8-4.4s5.2 1.7 5.8 4.4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
export function BottomTab({
items = [
{ label: "Home", href: "#" },
{ label: "Modules", href: "#" },
{ label: "Incidents", href: "#" },
{ label: "Alerts", href: "#" },
{ label: "Profile", href: "#", active: true },
],
}: BottomTabProps) {
const homeItem = items.find((item) => item.label.toLowerCase() === "home");
const sideItems = items.filter((item) => item.label.toLowerCase() !== "home");
const leftItems = sideItems.slice(0, 2);
const rightItems = sideItems.slice(2, 4);
return (
<nav className="maf-bottom-tab" aria-label="Bottom tab navigation">
<div className="maf-bottom-tab__inner">
<div className="maf-bottom-tab__side maf-bottom-tab__side--left">
{leftItems.map((item) => (
<a key={item.label} href={item.href ?? "#"} className={`maf-bottom-tab__item ${item.active ? "is-active" : ""}`.trim()}>
<span className="maf-bottom-tab__icon" aria-hidden>
<Icon label={item.label} />
</span>
<span className="maf-bottom-tab__label">{item.label}</span>
</a>
))}
</div>
<div className="maf-bottom-tab__centerSpace" aria-hidden />
<div className="maf-bottom-tab__side maf-bottom-tab__side--right">
{rightItems.map((item) => (
<a key={item.label} href={item.href ?? "#"} className={`maf-bottom-tab__item ${item.active ? "is-active" : ""}`.trim()}>
<span className="maf-bottom-tab__icon" aria-hidden>
<Icon label={item.label} />
</span>
<span className="maf-bottom-tab__label">{item.label}</span>
</a>
))}
</div>
{homeItem ? (
<a
key={homeItem.label}
href={homeItem.href ?? "#"}
className={`maf-bottom-tab__item maf-bottom-tab__item--home ${homeItem.active ? "is-active" : ""}`.trim()}
>
<span className="maf-bottom-tab__icon maf-bottom-tab__icon--home" aria-hidden>
<Icon label={homeItem.label} />
</span>
<span className="maf-bottom-tab__label">{homeItem.label}</span>
</a>
) : null}
</div>
</nav>
);
}
.maf-bottom-tab {
--maf-brown: #6e5a44;
--maf-brown-soft: #a78a68;
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 42;
padding: 0;
background: transparent;
}
.maf-bottom-tab__inner {
position: relative;
border-top: 1px solid rgba(110, 90, 68, 0.26);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(244, 238, 228, 0.98) 100%);
border-radius: 24px 24px 0 0;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: end;
min-height: 70px;
padding: 0.3rem 0.4rem max(0.5rem, env(safe-area-inset-bottom));
box-shadow: 0 -10px 28px rgba(110, 90, 68, 0.18);
}
.maf-bottom-tab__inner::before {
content: none;
}
.maf-bottom-tab__side {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: end;
justify-items: center;
}
.maf-bottom-tab__centerSpace {
width: 84px;
}
.maf-bottom-tab__item {
display: grid;
place-items: center;
gap: 0.2rem;
text-decoration: none;
color: rgba(110, 90, 68, 0.72);
padding: 0.12rem 0.15rem 0.4rem;
min-width: 0;
width: 100%;
}
.maf-bottom-tab__item.is-active {
color: rgba(110, 90, 68, 1);
}
.maf-bottom-tab__icon {
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.maf-bottom-tab__iconSvg {
width: 20px;
height: 20px;
stroke-width: 1.7;
}
.maf-bottom-tab__item--home {
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, -38%);
z-index: 2;
width: auto;
}
.maf-bottom-tab__icon--home {
width: 56px;
height: 56px;
border-radius: 999px;
background: linear-gradient(145deg, rgba(255, 255, 255, 1) 0%, rgba(244, 238, 228, 1) 100%);
border: 1px solid rgba(110, 90, 68, 0.34);
box-shadow:
0 10px 24px rgba(110, 90, 68, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.75);
color: rgba(110, 90, 68, 0.95);
}
.maf-bottom-tab__item--home .maf-bottom-tab__iconSvg {
width: 21px;
height: 21px;
}
.maf-bottom-tab__label {
font-size: 0.72rem;
font-weight: 500;
line-height: 1;
}
.maf-bottom-tab__item--home .maf-bottom-tab__label {
color: rgba(110, 90, 68, 1);
font-weight: 700;
margin-top: 0.18rem;
text-align: center;
}
@media (max-width: 430px) {
.maf-bottom-tab__inner {
min-height: 68px;
padding-left: 0.2rem;
padding-right: 0.2rem;
}
.maf-bottom-tab__centerSpace {
width: 74px;
}
.maf-bottom-tab__label {
font-size: 0.67rem;
}
}
/* Bottom tab is only for mobile and tablet */
@media (min-width: 1025px) {
.maf-bottom-tab {
display: none;
}
}
......@@ -13,6 +13,10 @@ export type FooterProps = {
supportLinks?: FooterLink[];
legalLinks?: FooterLink[];
copyright?: string;
hideLinkGroups?: boolean;
panelBar?: boolean;
supportCtaLabel?: string;
supportCtaHref?: string;
};
export function Footer({
......@@ -35,9 +39,13 @@ export function Footer({
{ label: "Security Policy", href: "#" },
],
copyright = "Copyright © MAF Revamp",
hideLinkGroups = false,
panelBar = false,
supportCtaLabel = "Support",
supportCtaHref = "#",
}: FooterProps) {
return (
<footer className="landing-footer">
<footer className={`landing-footer ${panelBar ? "landing-footer--panelBar" : ""}`.trim()}>
<div className="landing-footer__inner">
<div className="landing-footer__brandRow">
<span className="landing-footer__logoWrap" aria-hidden="true">
......@@ -51,54 +59,83 @@ export function Footer({
</span>
</div>
<nav className="landing-footer__cols" aria-label="Footer links">
<div className="landing-footer__col">
<div className="landing-footer__heading">Quick Links</div>
<div className="landing-footer__links">
{quickLinks.map((l) => (
<a
key={l.label}
className="landing-footer__link"
href={l.href ?? "#"}
>
{l.label}
</a>
))}
{!hideLinkGroups ? (
<nav className="landing-footer__cols" aria-label="Footer links">
<div className="landing-footer__col">
<div className="landing-footer__heading">Quick Links</div>
<div className="landing-footer__links">
{quickLinks.map((l) => (
<a
key={l.label}
className="landing-footer__link"
href={l.href ?? "#"}
>
{l.label}
</a>
))}
</div>
</div>
</div>
<div className="landing-footer__col">
<div className="landing-footer__heading">Support</div>
<div className="landing-footer__links">
{supportLinks.map((l) => (
<a
key={l.label}
className="landing-footer__link"
href={l.href ?? "#"}
>
{l.label}
</a>
))}
<div className="landing-footer__col">
<div className="landing-footer__heading">Support</div>
<div className="landing-footer__links">
{supportLinks.map((l) => (
<a
key={l.label}
className="landing-footer__link"
href={l.href ?? "#"}
>
{l.label}
</a>
))}
</div>
</div>
</div>
<div className="landing-footer__col">
<div className="landing-footer__heading">Legal</div>
<div className="landing-footer__links">
{legalLinks.map((l) => (
<a
key={l.label}
className="landing-footer__link"
href={l.href ?? "#"}
>
<div className="landing-footer__col">
<div className="landing-footer__heading">Legal</div>
<div className="landing-footer__links">
{legalLinks.map((l) => (
<a
key={l.label}
className="landing-footer__link"
href={l.href ?? "#"}
>
{l.label}
</a>
))}
</div>
</div>
</nav>
) : null}
{panelBar ? (
<div className="landing-footer__panelRow">
<div className="landing-footer__panelLeft">
<span className="landing-footer__panelLogoWrap" aria-hidden="true">
<Image
className="landing-footer__logo"
src={logoSrc}
alt={`${brandLabel} logo`}
fill
sizes="130px"
/>
</span>
<span className="landing-footer__bottom">{copyright}</span>
</div>
<a className="landing-footer__panelSupport" href={supportCtaHref}>
{supportCtaLabel}
</a>
<div className="landing-footer__panelLegal">
{legalLinks.slice(0, 3).map((l) => (
<a key={l.label} className="landing-footer__panelLegalLink" href={l.href ?? "#"}>
{l.label}
</a>
))}
</div>
</div>
</nav>
<div className="landing-footer__bottom">{copyright}</div>
) : (
<div className="landing-footer__bottom">{copyright}</div>
)}
</div>
</footer>
);
......
......@@ -76,6 +76,86 @@
color: rgba(110, 90, 68, 0.7);
}
.landing-footer--panelBar {
padding-top: 8px;
padding-bottom: 8px;
border-top: 1px solid rgba(110, 90, 68, 0.2);
background: rgba(255, 255, 255, 0.97);
}
.landing-footer--panelBar .landing-footer__brandRow {
display: none;
}
.landing-footer__panelRow {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 14px;
min-height: 34px;
}
.landing-footer__panelLeft {
min-width: 0;
display: inline-flex;
align-items: center;
gap: 10px;
}
.landing-footer__panelLogoWrap {
position: relative;
width: 30px;
height: 30px;
flex: 0 0 auto;
}
.landing-footer--panelBar .landing-footer__bottom {
margin-top: 0;
font-size: 0.66rem;
white-space: nowrap;
color: rgba(110, 90, 68, 0.72);
}
.landing-footer__panelSupport {
text-decoration: none;
color: rgba(110, 90, 68, 0.96);
background: linear-gradient(145deg, rgba(255, 255, 255, 0.98) 0%, rgba(244, 238, 228, 0.98) 100%);
border: 1px solid rgba(110, 90, 68, 0.32);
border-radius: 999px;
padding: 0.28rem 0.76rem;
font-size: 0.7rem;
font-weight: 700;
box-shadow:
0 6px 14px rgba(110, 90, 68, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.75);
letter-spacing: 0.02em;
}
.landing-footer__panelSupport:hover {
color: rgba(110, 90, 68, 1);
background: rgba(255, 255, 255, 1);
}
.landing-footer__panelLegal {
display: inline-flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 12px;
}
.landing-footer__panelLegalLink {
text-decoration: none;
color: rgba(110, 90, 68, 0.76);
font-size: 0.68rem;
white-space: nowrap;
}
.landing-footer__panelLegalLink:hover {
color: rgba(110, 90, 68, 0.95);
text-decoration: underline;
}
@media (min-width: 860px) {
.landing-footer {
padding-top: 44px;
......
......@@ -22,6 +22,8 @@ export type HeaderProps = {
href?: string;
onClick?: () => void;
};
hideNavMenu?: boolean;
hideActions?: boolean;
};
function SupportGlyph({ className }: { className?: string }) {
......@@ -50,10 +52,28 @@ export function Header({
],
supportCta = { label: "Support", href: "#" },
cta = { label: "Login", href: "#" },
hideNavMenu = false,
hideActions = false,
}: HeaderProps) {
const [open, setOpen] = React.useState(false);
const openedAtRef = React.useRef(0);
const panelId = "header-drawer";
const toggleDrawer = () => {
setOpen((current) => {
const next = !current;
if (next) openedAtRef.current = Date.now();
return next;
});
};
const closeDrawerFromBackdrop = () => {
// On some touch devices, the same tap used to open the drawer can
// immediately land on the backdrop and close it again.
if (Date.now() - openedAtRef.current < 250) return;
setOpen(false);
};
return (
<header className="landing-header">
<div className="landing-header__bg" aria-hidden />
......@@ -76,24 +96,26 @@ export function Header({
</div>
<div className="landing-header__center">
<nav
className="landing-header__nav landing-header__nav--desktop"
aria-label="Primary"
>
{navLinks.map((l) => (
<a
key={l.label}
className="landing-header__nav-link"
href={l.href ?? "#"}
>
{l.label}
</a>
))}
</nav>
{!hideNavMenu ? (
<nav
className="landing-header__nav landing-header__nav--desktop"
aria-label="Primary"
>
{navLinks.map((l) => (
<a
key={l.label}
className="landing-header__nav-link"
href={l.href ?? "#"}
>
{l.label}
</a>
))}
</nav>
) : null}
</div>
<div className="landing-header__right">
{supportCta ? (
{!hideActions && supportCta ? (
supportCta.onClick ? (
<button
type="button"
......@@ -118,7 +140,7 @@ export function Header({
)
) : null}
{cta ? (
{!hideActions && cta ? (
cta.onClick ? (
<button
type="button"
......@@ -134,27 +156,30 @@ export function Header({
)
) : null}
<button
className="landing-header__hamburger"
type="button"
aria-label={open ? "Close menu" : "Open menu"}
aria-controls={panelId}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span />
<span />
<span />
</button>
{!hideNavMenu || !hideActions ? (
<button
className="landing-header__hamburger"
type="button"
aria-label={open ? "Close menu" : "Open menu"}
aria-controls={panelId}
aria-expanded={open}
onClick={toggleDrawer}
>
<span />
<span />
<span />
</button>
) : null}
</div>
</div>
{open ? (
{open && (!hideNavMenu || !hideActions) ? (
<div
className="landing-header__drawer"
role="dialog"
aria-modal="true"
id={panelId}
onClick={(event) => event.stopPropagation()}
>
<button
className="landing-header__drawer-close"
......@@ -237,7 +262,7 @@ export function Header({
className="landing-header__backdrop"
type="button"
aria-label="Close menu backdrop"
onClick={() => setOpen(false)}
onClick={closeDrawerFromBackdrop}
/>
) : null}
</header>
......
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { Sidebar } from "./Sidebar";
const meta = {
title: "Layout/Sidebar",
component: Sidebar,
parameters: {
layout: "fullscreen",
},
decorators: [
(Story) => (
<div
style={{
minHeight: "100vh",
height: "100vh",
background: "var(--ink)",
display: "flex",
alignItems: "stretch",
}}
>
<Story />
</div>
),
],
} satisfies Meta<typeof Sidebar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {},
};
"use client";
import "./sidebar.css";
export type SidebarItem = {
label: string;
href?: string;
active?: boolean;
};
export type SidebarProps = {
items?: SidebarItem[];
};
function SidebarIcon({ label }: { label: string }) {
const icon = label.toLowerCase();
if (icon === "home") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<path d="M4 10.4 12 4l8 6.4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path d="M6.4 9.8v9.2h11.2V9.8" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
if (icon === "modules") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<rect x="4" y="4" width="6.5" height="6.5" rx="1.1" fill="none" stroke="currentColor" strokeWidth="1.8" />
<rect x="13.5" y="4" width="6.5" height="6.5" rx="1.1" fill="none" stroke="currentColor" strokeWidth="1.8" />
<rect x="4" y="13.5" width="6.5" height="6.5" rx="1.1" fill="none" stroke="currentColor" strokeWidth="1.8" />
<rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.1" fill="none" stroke="currentColor" strokeWidth="1.8" />
</svg>
);
}
if (icon === "admin") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<circle cx="12" cy="8" r="3.2" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M6.2 19c.6-2.7 2.8-4.4 5.8-4.4s5.2 1.7 5.8 4.4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
if (icon === "incidents") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<rect x="5" y="3.8" width="14" height="16.4" rx="1.8" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M8.4 9h7.2M8.4 12.5h7.2M8.4 16h5.4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
if (icon === "permit") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<rect x="4.5" y="6.2" width="15" height="13.3" rx="2" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M8 4.5v3.4M16 4.5v3.4M4.5 10.8h15" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
if (icon === "audits") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<path d="M5 19V11M12 19V6M19 19V14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
if (icon === "inspection") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<circle cx="10.5" cy="10.5" r="5.8" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="m15 15 4.2 4.2" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
if (icon === "checklist") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<rect x="4.2" y="4.2" width="15.6" height="15.6" rx="2" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="m8.1 12.1 2.3 2.2 5.4-5.3" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
if (icon === "suggestion") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<path d="M5.2 6.5h13.6a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H9.7l-4.5 3.2V8.5a2 2 0 0 1 2-2Z" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
</svg>
);
}
if (icon === "tracking") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<circle cx="12" cy="12" r="7.5" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M12 7.8v4.5l2.8 2.2" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
if (icon === "settings") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<circle cx="12" cy="12" r="2.6" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M12 4.7v1.7M12 17.6v1.7M19.3 12h-1.7M6.4 12H4.7M17.1 6.9l-1.2 1.2M8.1 15.9l-1.2 1.2M17.1 17.1l-1.2-1.2M8.1 8.1 6.9 6.9" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<circle cx="12" cy="12" r="3.6" fill="none" stroke="currentColor" strokeWidth="1.8" />
</svg>
);
}
export function Sidebar({
items = [
{ label: "Home", href: "#", active: true },
{ label: "Modules", href: "#" },
{ label: "Admin", href: "#" },
{ label: "Incidents", href: "#" },
{ label: "Permit", href: "#" },
{ label: "Audits", href: "#" },
{ label: "Inspection", href: "#" },
{ label: "Checklist", href: "#" },
{ label: "Suggestion", href: "#" },
{ label: "Tracking", href: "#" },
],
}: SidebarProps) {
return (
<aside className="maf-sidebar" aria-label="Sidebar navigation">
<div className="maf-sidebar__list">
{items.map((item) => (
<a
key={item.label}
href={item.href ?? "#"}
className={`maf-sidebar__item ${item.active ? "is-active" : ""}`.trim()}
>
<span className="maf-sidebar__icon" aria-hidden>
<SidebarIcon label={item.label} />
</span>
<span>{item.label}</span>
</a>
))}
</div>
<div className="maf-sidebar__settings">
<a className="maf-sidebar__item" href="#">
<span className="maf-sidebar__icon" aria-hidden>
<SidebarIcon label="settings" />
</span>
<span>Settings</span>
</a>
</div>
</aside>
);
}
.maf-sidebar {
--maf-brown: #6e5a44;
--maf-brown-soft: #a78a68;
width: 280px;
max-width: 100%;
min-height: 100%;
height: 100%;
box-sizing: border-box;
border-right: 1px solid rgba(110, 90, 68, 0.24);
background:
radial-gradient(520px 240px at 0% 0%, rgba(167, 138, 104, 0.16), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(250, 246, 240, 0.98) 100%);
padding: 1.3rem 1.05rem;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 1rem;
overflow: hidden;
}
.maf-sidebar__list {
display: grid;
gap: 0.25rem;
min-height: 0;
overflow: auto;
padding-right: 0.15rem;
}
.maf-sidebar__item {
display: inline-flex;
align-items: center;
gap: 0.65rem;
border-radius: 10px;
padding: 0.68rem 0.72rem;
text-decoration: none;
color: rgba(110, 90, 68, 0.92);
transition: background-color 0.2s ease;
}
.maf-sidebar__item:hover {
background: rgba(110, 90, 68, 0.08);
}
.maf-sidebar__item.is-active {
color: rgba(110, 90, 68, 1);
background: rgba(167, 138, 104, 0.18);
border: 1px solid rgba(110, 90, 68, 0.28);
}
.maf-sidebar__icon {
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
color: rgba(110, 90, 68, 0.85);
}
.maf-sidebar__iconSvg {
width: 20px;
height: 20px;
}
.maf-sidebar__item.is-active .maf-sidebar__icon {
color: rgba(110, 90, 68, 1);
}
.maf-sidebar__settings {
margin-top: auto;
padding-top: 0.75rem;
border-top: 1px solid rgba(110, 90, 68, 0.2);
background: linear-gradient(180deg, rgba(250, 246, 240, 0), rgba(250, 246, 240, 0.98) 30%);
}
import "./listing-active-chips.css";
type ListingActiveChip = {
group: "Country" | "Status";
value: string;
};
type ListingActiveChipsProps = {
chips: ListingActiveChip[];
resultsCount: number;
onRemove: (chip: ListingActiveChip) => void;
};
export function ListingActiveChips({ chips, resultsCount, onRemove }: ListingActiveChipsProps) {
return (
<div className="maf-listing-chips">
{chips.map((chip) => (
<span key={`${chip.group}:${chip.value}`} className="maf-listing-chips__chip">
{chip.group}: {chip.value}
<button type="button" className="maf-listing-chips__chipX" onClick={() => onRemove(chip)}>
×
</button>
</span>
))}
<span className="maf-listing-chips__results">{resultsCount} results</span>
</div>
);
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { auditListingData } from "./audit-listing-data";
import { ListingCards } from "./ListingCards";
const meta = {
title: "Listing/Base/ListingCards",
component: ListingCards,
parameters: { layout: "fullscreen" },
decorators: [
(Story) => (
<div style={{ minHeight: "100vh", background: "#f7f8fa", padding: "16px" }}>
<Story />
</div>
),
],
} satisfies Meta<typeof ListingCards>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { rows: auditListingData },
};
import type { ListingRecord } from "./listing-types";
import "./listing-cards.css";
type ListingCardsProps = {
rows: ListingRecord[];
};
export function ListingCards({ rows }: ListingCardsProps) {
if (!rows.length) {
return <div className="maf-listing-cards__empty">No records found.</div>;
}
return (
<div className="maf-listing-cards">
{rows.map((row) => (
<article key={row.id} className="maf-listing-cards__card">
<header className="maf-listing-cards__head">
<h3 className="maf-listing-cards__title">{row.title}</h3>
<span className={`maf-listing-cards__badge ${row.status === "Ongoing" ? "is-ongoing" : "is-completed"}`.trim()}>
{row.status}
</span>
</header>
<div className="maf-listing-cards__meta">
<span>{row.countryFlag ? `${row.countryFlag} ${row.country}` : row.country}</span>
<span>{row.location}</span>
<span>{row.score === null ? "—" : `${row.score.toFixed(2)}%`}</span>
<span>{row.changed}</span>
</div>
<footer className="maf-listing-cards__foot">{row.reporter}</footer>
</article>
))}
</div>
);
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ListingPage } from "./ListingPage";
import { auditListingData } from "./audit-listing-data";
const meta = {
title: "Listing/Base/ListingPage",
component: ListingPage,
parameters: { layout: "fullscreen" },
decorators: [
(Story) => (
<div style={{ minHeight: "100vh", background: "#f7f8fa" }}>
<Story />
</div>
),
],
} satisfies Meta<typeof ListingPage>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { data: auditListingData },
};
"use client";
import { useEffect, useMemo, useState } from "react";
import { ListingActiveChips } from "./ListingActiveChips";
import { ListingCards } from "./ListingCards";
import { ListingShell } from "./ListingShell";
import { ListingTable } from "./ListingTable";
import { ListingToolbar } from "./ListingToolbar";
import type { ListingFilterOption, ListingRecord, ListingViewMode } from "./listing-types";
type ListingPageProps = {
data: ListingRecord[];
};
const countryOptions: ListingFilterOption[] = [
{ label: "UAE", value: "UAE" },
{ label: "BHR", value: "Bahrain" },
{ label: "KSA", value: "KSA" },
{ label: "OMN", value: "Oman" },
{ label: "EGY", value: "Egypt" },
];
const statusOptions: ListingFilterOption[] = [
{ label: "Ongoing", value: "Ongoing" },
{ label: "Completed", value: "Completed" },
];
export function ListingPage({ data }: ListingPageProps) {
const [query, setQuery] = useState("");
const [countries, setCountries] = useState<string[]>(["UAE", "KSA"]);
const [statuses, setStatuses] = useState<string[]>([]);
const [viewMode, setViewMode] = useState<ListingViewMode>(() => {
if (typeof window !== "undefined" && window.matchMedia("(max-width: 1024px)").matches) {
return "cards";
}
return "table";
});
useEffect(() => {
if (typeof window === "undefined") return;
if (window.matchMedia("(max-width: 1024px)").matches) {
setViewMode("cards");
}
}, []);
const filtered = useMemo(() => {
const normalized = query.trim().toLowerCase();
return data.filter((item) => {
const countryOk = !countries.length || countries.includes(item.country);
const statusOk = !statuses.length || statuses.includes(item.status);
const queryOk =
!normalized ||
item.title.toLowerCase().includes(normalized) ||
item.location.toLowerCase().includes(normalized) ||
item.reporter.toLowerCase().includes(normalized);
return countryOk && statusOk && queryOk;
});
}, [countries, data, query, statuses]);
const chips = [
...countries.map((value) => ({ group: "Country" as const, value })),
...statuses.map((value) => ({ group: "Status" as const, value })),
];
const toggleValue = (list: string[], value: string, set: (next: string[]) => void) => {
set(list.includes(value) ? list.filter((v) => v !== value) : [...list, value]);
};
return (
<ListingShell title="Audits" subtitle="Manage and review all audit submissions">
<ListingToolbar
query={query}
onQueryChange={setQuery}
countryOptions={countryOptions}
selectedCountries={countries}
onCountryToggle={(value) => toggleValue(countries, value, setCountries)}
statusOptions={statusOptions}
selectedStatuses={statuses}
onStatusToggle={(value) => toggleValue(statuses, value, setStatuses)}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
<ListingActiveChips
chips={chips}
resultsCount={filtered.length}
onRemove={(chip) => {
if (chip.group === "Country") {
setCountries((current) => current.filter((v) => v !== chip.value));
return;
}
setStatuses((current) => current.filter((v) => v !== chip.value));
}}
/>
{viewMode === "table" ? <ListingTable rows={filtered} /> : <ListingCards rows={filtered} />}
</ListingShell>
);
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ListingShell } from "./ListingShell";
const meta = {
title: "Listing/Base/ListingShell",
component: ListingShell,
parameters: { layout: "fullscreen" },
decorators: [
(Story) => (
<div style={{ minHeight: "100vh", background: "#f7f8fa", padding: "16px" }}>
<Story />
</div>
),
],
} satisfies Meta<typeof ListingShell>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: "Audits",
subtitle: "Manage and review all audit submissions",
children: <div style={{ color: "#667085" }}>Listing content slot</div>,
},
};
import type React from "react";
import "./listing-shell.css";
export type ListingShellProps = {
title: string;
subtitle?: string;
actions?: React.ReactNode;
children: React.ReactNode;
};
export function ListingShell({ title, subtitle, actions, children }: ListingShellProps) {
return (
<section className="maf-listing-shell">
<header className="maf-listing-shell__header">
<div>
<h1 className="maf-listing-shell__title">{title}</h1>
{subtitle ? <p className="maf-listing-shell__subtitle">{subtitle}</p> : null}
</div>
{actions ? <div className="maf-listing-shell__actions">{actions}</div> : null}
</header>
{children}
</section>
);
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { auditListingData } from "./audit-listing-data";
import { ListingTable } from "./ListingTable";
const meta = {
title: "Listing/Base/ListingTable",
component: ListingTable,
parameters: { layout: "fullscreen" },
decorators: [
(Story) => (
<div style={{ minHeight: "100vh", background: "#f7f8fa", padding: "16px" }}>
<Story />
</div>
),
],
} satisfies Meta<typeof ListingTable>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { rows: auditListingData },
};
import type { ListingRecord } from "./listing-types";
import "./listing-table.css";
type ListingTableProps = {
rows: ListingRecord[];
};
function scoreClass(score: number | null) {
if (score === null) return "";
if (score >= 75) return "is-high";
if (score >= 50) return "is-mid";
return "is-low";
}
function scoreLabel(score: number | null) {
if (score === null) return "No score";
if (score >= 75) return "High score";
if (score >= 50) return "Medium score";
return "Low score";
}
export function ListingTable({ rows }: ListingTableProps) {
if (!rows.length) {
return <div className="maf-listing-table__empty">No audits found for this filter set.</div>;
}
return (
<div className="maf-listing-tableWrap">
<table className="maf-listing-table">
<thead>
<tr>
<th>#</th>
<th>Audit</th>
<th className="maf-listing-table__colStatus">Status</th>
<th className="maf-listing-table__colCountry">Country</th>
<th className="maf-listing-table__colLocation">Location</th>
<th>Score</th>
<th className="maf-listing-table__colReporter">Reporter</th>
<th className="maf-listing-table__colChanged">Last changed</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={row.id}>
<td className="maf-listing-table__index">{String(index + 1).padStart(2, "0")}</td>
<td>
<strong className="maf-listing-table__title">{row.title}</strong>
<div className="maf-listing-table__sub">AUDIT-{String(row.id).padStart(4, "0")}</div>
</td>
<td className="maf-listing-table__colStatus">
<span className={`maf-listing-table__badge ${row.status === "Ongoing" ? "is-ongoing" : "is-completed"}`.trim()}>
{row.status}
</span>
</td>
<td className="maf-listing-table__colCountry">
{row.countryFlag ? (
<span className="maf-listing-table__country">
<span className="maf-listing-table__flag" aria-hidden>
{row.countryFlag}
</span>
<span>{row.country}</span>
</span>
) : (
row.country
)}
</td>
<td className={`maf-listing-table__mono maf-listing-table__colLocation`.trim()}>{row.location}</td>
<td className="maf-listing-table__scoreCell">
<div className="maf-listing-table__scoreTop">
<span
className={`maf-listing-table__score ${scoreClass(row.score)}`.trim()}
aria-label={scoreLabel(row.score)}
>
{row.score === null ? "—" : `${row.score.toFixed(2)}%`}
</span>
</div>
{row.score !== null ? (
<div className="maf-listing-table__scoreBar" aria-hidden>
<div
className={`maf-listing-table__scoreFill ${scoreClass(row.score)}`.trim()}
style={{ width: `${Math.min(100, Math.max(0, row.score))}%` }}
/>
</div>
) : null}
</td>
<td className={`maf-listing-table__mono maf-listing-table__colReporter`.trim()}>{row.reporter}</td>
<td className={`maf-listing-table__muted maf-listing-table__colChanged`.trim()}>{row.changed}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { useState } from "react";
import { ListingToolbar } from "./ListingToolbar";
const meta = {
title: "Listing/Base/ListingToolbar",
component: ListingToolbar,
parameters: { layout: "fullscreen" },
decorators: [
(Story) => (
<div style={{ minHeight: "100vh", background: "#f7f8fa", padding: "16px" }}>
<Story />
</div>
),
],
} satisfies Meta<typeof ListingToolbar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Interactive: Story = {
render: () => {
const [query, setQuery] = useState("");
const [countries, setCountries] = useState<string[]>(["UAE"]);
const [statuses, setStatuses] = useState<string[]>([]);
const [view, setView] = useState<"table" | "cards">("cards");
return (
<ListingToolbar
query={query}
onQueryChange={setQuery}
countryOptions={[
{ label: "UAE", value: "UAE" },
{ label: "KSA", value: "KSA" },
{ label: "BHR", value: "Bahrain" },
]}
selectedCountries={countries}
onCountryToggle={(value) =>
setCountries((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value]))
}
statusOptions={[
{ label: "Ongoing", value: "Ongoing" },
{ label: "Completed", value: "Completed" },
]}
selectedStatuses={statuses}
onStatusToggle={(value) =>
setStatuses((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value]))
}
viewMode={view}
onViewModeChange={setView}
/>
);
},
};
import type { ListingFilterOption, ListingViewMode } from "./listing-types";
import "./listing-toolbar.css";
type ListingToolbarProps = {
query: string;
onQueryChange: (value: string) => void;
countryOptions: ListingFilterOption[];
selectedCountries: string[];
onCountryToggle: (value: string) => void;
statusOptions: ListingFilterOption[];
selectedStatuses: string[];
onStatusToggle: (value: string) => void;
viewMode: ListingViewMode;
onViewModeChange: (mode: ListingViewMode) => void;
};
function ViewIcon({ mode }: { mode: ListingViewMode }) {
if (mode === "cards") {
return (
<svg
className="maf-listing-toolbar__viewIcon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<rect x="4" y="4" width="7" height="7" rx="2" />
<rect x="13" y="4" width="7" height="7" rx="2" />
<rect x="13" y="13" width="7" height="7" rx="2" />
<rect x="4" y="13" width="7" height="7" rx="2" />
</svg>
);
}
return (
<svg
className="maf-listing-toolbar__viewIcon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<rect x="3.5" y="4.5" width="17" height="15" rx="2.5" />
<path d="M3.5 9h17" />
<path d="M9 4.5v15" />
</svg>
);
}
export function ListingToolbar({
query,
onQueryChange,
countryOptions,
selectedCountries,
onCountryToggle,
statusOptions,
selectedStatuses,
onStatusToggle,
viewMode,
onViewModeChange,
}: ListingToolbarProps) {
return (
<div className="maf-listing-toolbar">
<div className="maf-listing-toolbar__searchWrap">
<input
className="maf-listing-toolbar__searchInput"
placeholder="Search by name, location, reporter..."
value={query}
onChange={(event) => onQueryChange(event.target.value)}
/>
</div>
<div className="maf-listing-toolbar__group">
<span className="maf-listing-toolbar__groupLabel">Country</span>
{countryOptions.map((option) => (
<button
key={option.value}
type="button"
className={`maf-listing-toolbar__pill ${selectedCountries.includes(option.value) ? "is-active" : ""}`.trim()}
onClick={() => onCountryToggle(option.value)}
>
{option.label}
</button>
))}
</div>
<div className="maf-listing-toolbar__group">
<span className="maf-listing-toolbar__groupLabel">Status</span>
{statusOptions.map((option) => (
<button
key={option.value}
type="button"
className={`maf-listing-toolbar__pill ${selectedStatuses.includes(option.value) ? "is-active" : ""}`.trim()}
onClick={() => onStatusToggle(option.value)}
>
{option.label}
</button>
))}
</div>
<div className="maf-listing-toolbar__view">
<button
type="button"
className={`maf-listing-toolbar__viewBtn ${viewMode === "table" ? "is-active" : ""}`.trim()}
onClick={() => onViewModeChange("table")}
aria-label="Switch to table view"
>
<ViewIcon mode="table" />
</button>
<button
type="button"
className={`maf-listing-toolbar__viewBtn ${viewMode === "cards" ? "is-active" : ""}`.trim()}
onClick={() => onViewModeChange("cards")}
aria-label="Switch to card view"
>
<ViewIcon mode="cards" />
</button>
</div>
</div>
);
}
import type { ListingRecord } from "./listing-types";
export const auditListingData: ListingRecord[] = [
{
id: 1,
title: "Area Manager Audit Compliance U",
country: "UAE",
countryFlag: "🇦🇪",
location: "VOX AHM",
score: 44.12,
changed: "Mar 5, 2026",
reporter: "Ritesh.Arora@maf.ae",
status: "Ongoing",
},
{
id: 2,
title: "Voucher Audit",
country: "Bahrain",
countryFlag: "🇧🇭",
location: "VOX BHAV",
score: null,
changed: "Mar 5, 2026",
reporter: "fatema.abdulla@maf.ae",
status: "Completed",
},
{
id: 3,
title: "LEC Cashier Spot Audit",
country: "UAE",
countryFlag: "🇦🇪",
location: "VOX AHM",
score: null,
changed: "Mar 5, 2026",
reporter: "Leona.Sena@maf.ae",
status: "Completed",
},
{
id: 4,
title: "Area Manager Audit Facilities",
country: "UAE",
countryFlag: "🇦🇪",
location: "VOX Al Jimi",
score: 87.68,
changed: "Mar 5, 2026",
reporter: "wahiba.abdelrahman@maf.ae",
status: "Ongoing",
},
{
id: 5,
title: "Safe and Key Box Code Change",
country: "KSA",
countryFlag: "🇸🇦",
location: "VOX KSA Town Square",
score: null,
changed: "Mar 5, 2026",
reporter: "Azhar.alamri2@maf.ae",
status: "Completed",
},
];
export * from "./listing-types";
export * from "./ListingShell";
export * from "./ListingToolbar";
export * from "./ListingActiveChips";
export * from "./ListingTable";
export * from "./ListingCards";
export * from "./ListingPage";
export * from "./audit-listing-data";
.maf-listing-chips {
margin: 0.6rem 0 0.7rem;
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
font-family: var(--font-sans), "Manrope", ui-sans-serif, system-ui, -apple-system, sans-serif;
}
.maf-listing-chips__chip {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.5rem;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.22);
background: rgba(167, 138, 104, 0.16);
color: rgba(110, 90, 68, 0.96);
font-size: 0.72rem;
font-weight: 650;
letter-spacing: 0.008em;
}
.maf-listing-chips__chipX {
border: 0;
width: 16px;
height: 16px;
border-radius: 999px;
background: rgba(110, 90, 68, 0.14);
color: rgba(110, 90, 68, 0.92);
cursor: pointer;
line-height: 1;
}
.maf-listing-chips__results {
margin-left: 0;
width: 100%;
order: 99;
color: #8b90a7;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
font-size: 0.72rem;
}
@media (min-width: 768px) {
.maf-listing-chips__results {
margin-left: auto;
width: auto;
order: initial;
}
}
.maf-listing-cards {
display: grid;
grid-template-columns: 1fr;
gap: 0.55rem;
font-family: var(--font-sans), "Manrope", ui-sans-serif, system-ui, -apple-system, sans-serif;
}
.maf-listing-cards__card {
border: 1px solid rgba(110, 90, 68, 0.22);
border-radius: 12px;
background:
radial-gradient(520px 220px at 10% 0%, rgba(167, 138, 104, 0.1), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(250, 246, 240, 0.98) 100%);
padding: 0.78rem;
box-shadow: 0 10px 22px rgba(110, 90, 68, 0.1);
}
.maf-listing-cards__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.5rem;
}
.maf-listing-cards__title {
margin: 0;
font-size: 0.85rem;
line-height: 1.32;
letter-spacing: -0.01em;
font-weight: 650;
color: rgba(58, 47, 37, 0.94);
}
.maf-listing-cards__badge {
padding: 0.16rem 0.45rem;
border-radius: 999px;
font-size: 0.65rem;
font-weight: 700;
}
.maf-listing-cards__badge.is-ongoing {
color: rgba(183, 124, 10, 0.98);
border: 1px solid rgba(217, 119, 6, 0.28);
background: rgba(255, 251, 235, 0.9);
}
.maf-listing-cards__badge.is-completed {
color: rgba(5, 150, 105, 0.98);
border: 1px solid rgba(5, 150, 105, 0.22);
background: rgba(236, 253, 245, 0.9);
}
.maf-listing-cards__meta {
margin-top: 0.68rem;
display: grid;
grid-template-columns: 1fr;
gap: 0.45rem;
color: rgba(82, 69, 54, 0.85);
font-size: 0.72rem;
font-weight: 540;
}
.maf-listing-cards__foot {
margin-top: 0.8rem;
color: rgba(110, 90, 68, 0.58);
font-size: 0.67rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
}
.maf-listing-cards__empty {
border: 1px solid #e4e6ec;
border-radius: 14px;
padding: 2rem 1rem;
text-align: center;
color: #8b90a7;
background: #ffffff;
}
@media (min-width: 640px) {
.maf-listing-cards {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
}
.maf-listing-cards__meta {
grid-template-columns: 1fr 1fr;
}
}
@media (min-width: 1024px) {
.maf-listing-cards {
grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
gap: 0.7rem;
}
}
.maf-listing-shell {
--maf-listing-font-sans: var(--font-sans), "Manrope", ui-sans-serif, system-ui, -apple-system, sans-serif;
--maf-listing-font-display: var(--font-display), "Syne", ui-sans-serif, system-ui, sans-serif;
width: min(1200px, 100%);
margin: 0 auto;
padding: 0.7rem;
color: #0f1117;
font-family: var(--maf-listing-font-sans);
}
.maf-listing-shell__header {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
gap: 0.55rem;
margin-bottom: 0.75rem;
}
.maf-listing-shell__title {
margin: 0;
color: rgba(110, 90, 68, 1);
font-size: 1.15rem;
line-height: 1.2;
font-family: var(--maf-listing-font-display);
font-weight: 700;
letter-spacing: -0.015em;
}
.maf-listing-shell__subtitle {
margin: 0.25rem 0 0;
font-size: 0.76rem;
color: rgba(110, 90, 68, 0.74);
font-weight: 500;
}
.maf-listing-shell__actions {
display: inline-flex;
gap: 0.45rem;
align-items: center;
width: 100%;
flex-wrap: wrap;
}
@media (min-width: 768px) {
.maf-listing-shell {
padding: 1rem;
}
.maf-listing-shell__header {
margin-bottom: 0.95rem;
}
.maf-listing-shell__title {
font-size: 1.32rem;
}
}
@media (min-width: 1024px) {
.maf-listing-shell {
padding: 1.25rem;
}
.maf-listing-shell__header {
flex-direction: row;
align-items: flex-end;
gap: 0.9rem;
margin-bottom: 1rem;
}
.maf-listing-shell__title {
font-size: 1.45rem;
}
.maf-listing-shell__subtitle {
font-size: 0.82rem;
}
.maf-listing-shell__actions {
width: auto;
}
}
.maf-listing-tableWrap {
--maf-brown: #6e5a44;
--maf-brown-soft: #a78a68;
--maf-paper: rgba(255, 255, 255, 0.98);
--maf-border: rgba(110, 90, 68, 0.22);
--maf-border-soft: rgba(110, 90, 68, 0.14);
border: 1px solid var(--maf-border);
border-radius: 12px;
overflow: auto;
background:
radial-gradient(820px 280px at 10% 0%, rgba(167, 138, 104, 0.12), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(250, 246, 240, 0.98) 100%);
box-shadow: 0 10px 22px rgba(110, 90, 68, 0.12);
-webkit-overflow-scrolling: touch;
}
.maf-listing-table {
width: 100%;
border-collapse: collapse;
min-width: 520px;
font-family: var(--font-sans), "Manrope", ui-sans-serif, system-ui, -apple-system, sans-serif;
}
.maf-listing-table th {
text-align: left;
padding: 0.62rem 0.55rem;
font-size: 0.61rem;
letter-spacing: 0.1em;
color: rgba(110, 90, 68, 0.7);
text-transform: uppercase;
border-bottom: 1px solid var(--maf-border);
position: sticky;
top: 0;
z-index: 2;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(250, 246, 240, 0.98) 100%);
backdrop-filter: blur(8px);
}
.maf-listing-table td {
padding: 0.72rem 0.55rem;
font-size: 0.76rem;
color: rgba(70, 59, 47, 0.9);
border-bottom: 1px solid var(--maf-border-soft);
vertical-align: middle;
}
.maf-listing-table tr:last-child td {
border-bottom: 0;
}
.maf-listing-table tbody tr {
transition: background-color 0.18s ease, transform 0.18s ease;
}
.maf-listing-table tbody tr:hover {
background: rgba(110, 90, 68, 0.06);
}
.maf-listing-table__index,
.maf-listing-table__mono,
.maf-listing-table__muted,
.maf-listing-table__sub {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
}
.maf-listing-table__index {
color: rgba(110, 90, 68, 0.68);
font-size: 0.64rem;
}
.maf-listing-table__title {
font-weight: 700;
letter-spacing: -0.01em;
color: rgba(58, 47, 37, 0.94);
font-size: 0.79rem;
}
.maf-listing-table__sub,
.maf-listing-table__muted {
font-size: 0.62rem;
color: rgba(110, 90, 68, 0.52);
}
.maf-listing-table__badge {
padding: 0.22rem 0.55rem;
border-radius: 999px;
font-size: 0.62rem;
font-weight: 800;
letter-spacing: 0.01em;
}
.maf-listing-table__badge.is-ongoing {
color: rgba(183, 124, 10, 0.98);
border: 1px solid rgba(217, 119, 6, 0.28);
background: rgba(255, 251, 235, 0.9);
}
.maf-listing-table__badge.is-completed {
color: rgba(5, 150, 105, 0.98);
border: 1px solid rgba(5, 150, 105, 0.22);
background: rgba(236, 253, 245, 0.9);
}
.maf-listing-table__country {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-weight: 620;
color: rgba(82, 69, 54, 0.9);
}
.maf-listing-table__flag {
font-size: 0.95rem;
line-height: 1;
}
.maf-listing-table__scoreCell {
white-space: nowrap;
}
.maf-listing-table__scoreTop {
display: flex;
align-items: center;
gap: 0.35rem;
}
.maf-listing-table__score {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
font-weight: 700;
font-size: 0.74rem;
}
.maf-listing-table__score.is-high {
color: rgba(5, 150, 105, 1);
}
.maf-listing-table__score.is-mid {
color: rgba(217, 119, 6, 1);
}
.maf-listing-table__score.is-low {
color: rgba(220, 38, 38, 1);
}
.maf-listing-table__scoreBar {
margin-top: 0.28rem;
width: 76px;
height: 4px;
border-radius: 999px;
background: rgba(110, 90, 68, 0.14);
overflow: hidden;
}
.maf-listing-table__scoreFill {
height: 100%;
border-radius: 999px;
background: rgba(110, 90, 68, 0.5);
}
.maf-listing-table__scoreFill.is-high {
background: rgba(5, 150, 105, 1);
}
.maf-listing-table__scoreFill.is-mid {
background: rgba(217, 119, 6, 1);
}
.maf-listing-table__scoreFill.is-low {
background: rgba(220, 38, 38, 1);
}
.maf-listing-table__empty {
border: 1px solid var(--maf-border);
border-radius: 12px;
padding: 2rem 1rem;
text-align: center;
color: rgba(110, 90, 68, 0.7);
background:
radial-gradient(820px 280px at 10% 0%, rgba(167, 138, 104, 0.12), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(250, 246, 240, 0.98) 100%);
}
/* Mobile-first visibility */
.maf-listing-table__colReporter,
.maf-listing-table__colLocation,
.maf-listing-table__colCountry,
.maf-listing-table__colChanged {
display: none;
}
@media (min-width: 640px) {
.maf-listing-table {
min-width: 680px;
}
.maf-listing-table__colCountry,
.maf-listing-table__colChanged {
display: none;
}
.maf-listing-table__colLocation {
display: none;
}
}
@media (min-width: 768px) {
.maf-listing-table {
min-width: 820px;
}
.maf-listing-table th {
padding: 0.7rem 0.78rem;
font-size: 0.62rem;
}
.maf-listing-table td {
padding: 0.78rem 0.78rem;
font-size: 0.8rem;
}
.maf-listing-table__title {
font-size: 0.82rem;
}
.maf-listing-table__scoreBar {
width: 86px;
}
.maf-listing-table__colCountry,
.maf-listing-table__colChanged,
.maf-listing-table__colLocation {
display: table-cell;
}
}
@media (min-width: 1024px) {
.maf-listing-table {
min-width: 980px;
}
.maf-listing-table th {
padding: 0.75rem 0.9rem;
font-size: 0.63rem;
}
.maf-listing-table td {
padding: 0.9rem 0.9rem;
font-size: 0.82rem;
}
.maf-listing-table__index {
font-size: 0.7rem;
}
.maf-listing-table__title {
font-size: 0.85rem;
}
.maf-listing-table__sub,
.maf-listing-table__muted {
font-size: 0.68rem;
}
.maf-listing-table__badge {
font-size: 0.68rem;
}
.maf-listing-table__score {
font-size: 0.82rem;
}
.maf-listing-table__scoreBar {
width: 92px;
}
.maf-listing-table__colReporter,
.maf-listing-table__colLocation,
.maf-listing-table__colCountry,
.maf-listing-table__colChanged {
display: table-cell;
}
}
.maf-listing-toolbar {
--maf-brown: #6e5a44;
--maf-brown-soft: #a78a68;
--maf-border: rgba(110, 90, 68, 0.22);
--maf-border-soft: rgba(110, 90, 68, 0.14);
border: 1px solid var(--maf-border);
border-radius: 12px;
background:
radial-gradient(560px 220px at 0% 0%, rgba(167, 138, 104, 0.12), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(250, 246, 240, 0.98) 100%);
padding: 0.55rem;
display: flex;
align-items: stretch;
gap: 0.45rem;
flex-direction: column;
flex-wrap: wrap;
font-family: var(--font-sans), "Manrope", ui-sans-serif, system-ui, -apple-system, sans-serif;
}
.maf-listing-toolbar__searchWrap {
flex: 1 1 100%;
min-width: 0;
width: 100%;
}
.maf-listing-toolbar__searchInput {
width: 100%;
border-radius: 8px;
border: 1px solid var(--maf-border-soft);
background: rgba(255, 255, 255, 0.78);
color: rgba(15, 17, 23, 0.95);
padding: 0.6rem 0.7rem;
font-size: 0.84rem;
font-weight: 560;
letter-spacing: 0.005em;
transition: box-shadow 0.16s ease, background-color 0.16s ease, border-color 0.16s ease;
}
.maf-listing-toolbar__searchInput:focus {
outline: none;
border-color: rgba(110, 90, 68, 0.32);
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 0 0 3px rgba(167, 138, 104, 0.18);
}
.maf-listing-toolbar__group {
display: flex;
align-items: center;
gap: 0.35rem;
flex-wrap: nowrap;
overflow-x: auto;
width: 100%;
padding-bottom: 0.05rem;
}
.maf-listing-toolbar__groupLabel {
flex-shrink: 0;
font-size: 0.64rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(110, 90, 68, 0.62);
font-weight: 800;
}
.maf-listing-toolbar__pill {
flex-shrink: 0;
border-radius: 999px;
border: 1px solid var(--maf-border-soft);
background: rgba(255, 255, 255, 0.78);
color: rgba(110, 90, 68, 0.92);
padding: 0.35rem 0.6rem;
font-size: 0.72rem;
font-weight: 650;
letter-spacing: 0.01em;
cursor: pointer;
transition: background-color 0.16s ease, border-color 0.16s ease;
}
.maf-listing-toolbar__pill:hover {
border-color: rgba(110, 90, 68, 0.22);
background: rgba(110, 90, 68, 0.06);
}
.maf-listing-toolbar__pill.is-active {
border-color: rgba(110, 90, 68, 0.28);
color: rgba(110, 90, 68, 1);
background: rgba(167, 138, 104, 0.18);
}
.maf-listing-toolbar__view {
margin-left: 0;
display: inline-flex;
border: 1px solid var(--maf-border-soft);
border-radius: 8px;
overflow: hidden;
align-self: flex-start;
}
.maf-listing-toolbar__viewBtn {
border: 0;
background: rgba(255, 255, 255, 0.6);
color: rgba(110, 90, 68, 0.86);
font-size: 0.73rem;
font-weight: 600;
padding: 0.42rem 0.64rem;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
transition: transform 0.16s ease, background-color 0.16s ease, color 0.16s ease;
}
.maf-listing-toolbar__viewBtn.is-active {
background: rgba(255, 255, 255, 0.96);
color: rgba(110, 90, 68, 1);
transform: translateY(-1px);
}
.maf-listing-toolbar__viewIcon {
width: 16px;
height: 16px;
transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.22s ease;
opacity: 0.9;
}
.maf-listing-toolbar__viewBtn:hover .maf-listing-toolbar__viewIcon {
transform: scale(1.06);
}
.maf-listing-toolbar__viewBtn.is-active .maf-listing-toolbar__viewIcon {
transform: scale(1.12);
opacity: 1;
}
@media (min-width: 768px) {
.maf-listing-toolbar {
flex-direction: row;
align-items: center;
gap: 0.5rem;
padding: 0.65rem;
}
.maf-listing-toolbar__searchWrap {
flex: 1 1 220px;
width: auto;
}
.maf-listing-toolbar__group {
width: auto;
overflow-x: visible;
flex-wrap: wrap;
}
.maf-listing-toolbar__view {
margin-left: auto;
align-self: auto;
}
}
export type ListingStatus = "Ongoing" | "Completed";
export type ListingViewMode = "table" | "cards";
export type ListingRecord = {
id: number;
title: string;
country: string;
countryFlag?: string;
location: string;
score: number | null;
changed: string;
reporter: string;
status: ListingStatus;
};
export type ListingFilterOption = {
label: string;
value: string;
};
......@@ -286,7 +286,7 @@ export function LoginModal({
</button>
</div>
<button type="submit" className="maf-login-modal__primaryButton">
<button type="submit" onClick={()=>setView("twoFactor")} className="maf-login-modal__primaryButton">
Log in
</button>
</form>
......
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { QuickAction } from "./QuickAction";
const meta = {
title: "Widgets/Quick Action",
component: QuickAction,
parameters: {
layout: "fullscreen",
},
decorators: [
(Story) => (
<div style={{ minHeight: "100vh", background: "var(--ink)", padding: "1rem" }}>
<Story />
</div>
),
],
} satisfies Meta<typeof QuickAction>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
floating: true,
},
};
export const InlinePanel: Story = {
args: {
floating: false,
},
decorators: [
(Story) => (
<div style={{ width: "360px", background: "var(--ink)", padding: "1rem" }}>
<Story />
</div>
),
],
};
"use client";
import { useState } from "react";
import "./quick-action.css";
export type QuickActionItem = {
label: string;
href?: string;
active?: boolean;
};
export type QuickActionProps = {
title?: string;
actions?: QuickActionItem[];
floating?: boolean;
defaultOpen?: boolean;
};
export function QuickAction({
title = "Quick actions",
actions = [
{ label: "Add incident", href: "#", active: true },
{ label: "Permit create", href: "#" },
{ label: "Conduct audit", href: "#" },
{ label: "Inspection", href: "#" },
{ label: "Checklist", href: "#" },
{ label: "Add suggestion", href: "#" },
{ label: "Action plan", href: "#" },
{ label: "Suggestion module", href: "#" },
{ label: "All modules", href: "#" },
],
floating = true,
defaultOpen = false,
}: QuickActionProps) {
const [open, setOpen] = useState(defaultOpen);
if (!floating) {
return (
<section className="maf-quick-action" aria-label={title}>
<h3 className="maf-quick-action__title">{title}</h3>
<div className="maf-quick-action__list">
{actions.map((action) => (
<a
key={action.label}
href={action.href ?? "#"}
className={`maf-quick-action__item ${action.active ? "is-active" : ""}`.trim()}
>
{action.label}
</a>
))}
</div>
</section>
);
}
return (
<div className="maf-quick-action__floatingWrap">
{open ? (
<section className="maf-quick-action maf-quick-action--floatingPanel" aria-label={title}>
<h3 className="maf-quick-action__title">{title}</h3>
<div className="maf-quick-action__list">
{actions.map((action) => (
<a
key={action.label}
href={action.href ?? "#"}
className={`maf-quick-action__item ${action.active ? "is-active" : ""}`.trim()}
>
{action.label}
</a>
))}
</div>
</section>
) : null}
<button
type="button"
className={`maf-quick-action__fab ${open ? "is-open" : ""}`.trim()}
onClick={() => setOpen((current) => !current)}
aria-expanded={open}
aria-label={open ? "Close quick actions" : "Open quick actions"}
>
{open ? "×" : "+"}
</button>
</div>
);
}
.maf-quick-action {
--maf-brown: #6e5a44;
--maf-brown-soft: #a78a68;
border-radius: 18px;
border: 1px solid rgba(110, 90, 68, 0.24);
background:
radial-gradient(420px 200px at 10% 0%, rgba(167, 138, 104, 0.14), transparent 58%),
rgba(255, 255, 255, 0.97);
overflow: hidden;
}
.maf-quick-action__floatingWrap {
position: fixed;
right: 1.1rem;
bottom: 1.1rem;
z-index: 45;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.65rem;
}
.maf-quick-action--floatingPanel {
width: min(280px, calc(100vw - 2rem));
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.5);
}
.maf-quick-action__title {
margin: 0;
padding: 0.9rem 1rem 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.95rem;
color: rgba(110, 90, 68, 0.9);
border-bottom: 1px solid rgba(110, 90, 68, 0.18);
}
.maf-quick-action__list {
display: grid;
gap: 0.1rem;
padding: 0.45rem;
}
.maf-quick-action__item {
border-radius: 8px;
text-decoration: none;
padding: 0.58rem 0.65rem;
color: rgba(110, 90, 68, 0.9);
}
.maf-quick-action__item:hover {
background: rgba(110, 90, 68, 0.08);
}
.maf-quick-action__item.is-active {
color: rgba(110, 90, 68, 1);
border-left: 2px solid rgba(110, 90, 68, 0.8);
padding-left: 0.53rem;
}
.maf-quick-action__fab {
width: 62px;
height: 62px;
border-radius: 999px;
border: 0;
font-size: 2rem;
line-height: 1;
color: rgba(110, 90, 68, 0.95);
background: linear-gradient(145deg, rgba(255, 255, 255, 0.98) 0%, rgba(244, 238, 228, 0.98) 100%);
box-shadow:
0 14px 32px rgba(110, 90, 68, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.65);
border: 1px solid rgba(110, 90, 68, 0.28);
cursor: pointer;
}
.maf-quick-action__fab.is-open {
font-size: 2.15rem;
transform: rotate(0deg);
}
@media (max-width: 1024px) {
.maf-quick-action__floatingWrap {
right: 0.9rem;
bottom: calc(4.85rem + env(safe-area-inset-bottom));
}
}
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
allowedDevOrigins: ["192.168.1.177"],
devIndicators: false,
images: {
remotePatterns: [
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment