Redesign Team, Support, AI & Rules; solid-orange brand system
- Team Management: crew hero, member gallery + list, role rings, invites - Support Center: command-center Overview (live status, pulse ribbon, signal) - AI Assistant: animated orb rings + textured stage - Rules: rulebook cards (watermark, uniform orange), 3-up responsive grid - Brand: --grad-brand now solid rgba(253,169,19,.92), white text/icons on orange, no orange gradients; dark surfaces (--card-grad/--panel-2) #060608 - Segmented tabs get an on-brand orange active state Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 909 KiB After Width: | Height: | Size: 6.2 KiB |
+393
-39
@@ -31,7 +31,7 @@
|
|||||||
.dash-root[data-theme="dark"] {
|
.dash-root[data-theme="dark"] {
|
||||||
--bg: #060608;
|
--bg: #060608;
|
||||||
--panel: #0e0e13;
|
--panel: #0e0e13;
|
||||||
--panel-2: #15151b;
|
--panel-2: #060608;
|
||||||
--panel-3: #1b1b22;
|
--panel-3: #1b1b22;
|
||||||
--border: rgba(255, 255, 255, 0.07);
|
--border: rgba(255, 255, 255, 0.07);
|
||||||
--border-2: rgba(255, 255, 255, 0.12);
|
--border-2: rgba(255, 255, 255, 0.12);
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
--track: rgba(15, 23, 42, 0.07);
|
--track: rgba(15, 23, 42, 0.07);
|
||||||
--sidebar: #ffffff;
|
--sidebar: #ffffff;
|
||||||
--shadow: 0 18px 40px -26px rgba(15, 23, 42, 0.25);
|
--shadow: 0 18px 40px -26px rgba(15, 23, 42, 0.25);
|
||||||
|
--card-grad: var(--panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dash-root * { box-sizing: border-box; }
|
.dash-root * { box-sizing: border-box; }
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
.dash-brand { display: flex; align-items: center; gap: 9px; padding: 18px 18px 14px; }
|
.dash-brand { display: flex; align-items: center; gap: 9px; padding: 18px 18px 14px; }
|
||||||
.dash-brand img { height: 26px; width: auto; }
|
.dash-brand img { height: 44px; width: auto; }
|
||||||
.dash-brand .bk { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
|
.dash-brand .bk { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
|
||||||
.dash-nav { flex: 1; overflow-y: auto; padding: 6px 12px 12px; }
|
.dash-nav { flex: 1; overflow-y: auto; padding: 6px 12px 12px; }
|
||||||
.dash-nav::-webkit-scrollbar { width: 6px; }
|
.dash-nav::-webkit-scrollbar { width: 6px; }
|
||||||
@@ -91,8 +92,8 @@
|
|||||||
text-align: left; transition: background 0.14s, color 0.14s;
|
text-align: left; transition: background 0.14s, color 0.14s;
|
||||||
}
|
}
|
||||||
.nav-item:hover { background: var(--panel-2); color: var(--text-2); }
|
.nav-item:hover { background: var(--panel-2); color: var(--text-2); }
|
||||||
.nav-item.active { background: var(--orange); color: #1a1206; font-weight: 600; }
|
.nav-item.active { background: var(--orange); color: #fff; font-weight: 600; }
|
||||||
.nav-item.active svg { color: #1a1206; }
|
.nav-item.active svg { color: #fff; }
|
||||||
.nav-item svg { color: var(--faint); flex: 0 0 auto; }
|
.nav-item svg { color: var(--faint); flex: 0 0 auto; }
|
||||||
.nav-item:hover svg { color: var(--text-2); }
|
.nav-item:hover svg { color: var(--text-2); }
|
||||||
.fig-ic { display: inline-flex; flex: 0 0 auto; color: inherit; }
|
.fig-ic { display: inline-flex; flex: 0 0 auto; color: inherit; }
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
/* ---- main ---- */
|
/* ---- main ---- */
|
||||||
.dash-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
.dash-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||||
.dash-topbar {
|
.dash-topbar {
|
||||||
height: 64px; display: flex; align-items: center; justify-content: space-between;
|
height: 84px; display: flex; align-items: center; justify-content: space-between;
|
||||||
padding: 0 24px; border-bottom: 1px solid var(--border);
|
padding: 0 24px; border-bottom: 1px solid var(--border);
|
||||||
position: sticky; top: 0; z-index: 20;
|
position: sticky; top: 0; z-index: 20;
|
||||||
background: color-mix(in srgb, var(--bg) 80%, transparent);
|
background: color-mix(in srgb, var(--bg) 80%, transparent);
|
||||||
@@ -220,8 +221,8 @@
|
|||||||
|
|
||||||
.dash-root {
|
.dash-root {
|
||||||
--ring: color-mix(in srgb, var(--orange) 35%, transparent);
|
--ring: color-mix(in srgb, var(--orange) 35%, transparent);
|
||||||
--grad-brand: linear-gradient(135deg, #fda913 0%, #fd6d13 100%);
|
--grad-brand: rgba(253, 169, 19, 0.92);
|
||||||
--card-grad: linear-gradient(160deg, color-mix(in srgb, var(--panel-2) 55%, var(--panel)) 0%, var(--panel) 62%);
|
--card-grad: #060608;
|
||||||
--card-hi: inset 0 1px 0 color-mix(in srgb, #fff 6%, transparent);
|
--card-hi: inset 0 1px 0 color-mix(in srgb, #fff 6%, transparent);
|
||||||
--glow-orange: 0 10px 28px -12px color-mix(in srgb, var(--orange) 60%, transparent);
|
--glow-orange: 0 10px 28px -12px color-mix(in srgb, var(--orange) 60%, transparent);
|
||||||
}
|
}
|
||||||
@@ -236,20 +237,46 @@
|
|||||||
.dash-root .view { position: relative; z-index: 1; }
|
.dash-root .view { position: relative; z-index: 1; }
|
||||||
|
|
||||||
/* ---- sidebar (premium nav) ---- */
|
/* ---- sidebar (premium nav) ---- */
|
||||||
.dash-root .dash-sidebar { width: 236px; flex: 0 0 236px; background: linear-gradient(180deg, color-mix(in srgb, var(--orange) 4%, var(--sidebar)) 0%, var(--sidebar) 30%); }
|
.dash-root .dash-sidebar { width: 236px; flex: 0 0 236px; background: #040404 }
|
||||||
.dash-root .dash-brand { padding: 20px 18px 16px; }
|
.dash-root .dash-brand { padding: 20px 18px 16px; }
|
||||||
.dash-root .dash-brand .bk { font-size: 15.5px; font-weight: 800; letter-spacing: -0.01em; }
|
.dash-root .dash-brand .bk { font-size: 15.5px; font-weight: 800; letter-spacing: -0.01em; }
|
||||||
.dash-root .dash-nav { padding: 4px 12px 14px; }
|
.dash-root .dash-nav { padding: 4px 12px 14px; }
|
||||||
.dash-root .nav-section { margin-bottom: 4px; }
|
.dash-root .nav-section { margin-bottom: 4px; }
|
||||||
.dash-root .nav-section + .nav-section { border-top: 1px solid var(--border); padding-top: 4px; }
|
.dash-root .nav-section + .nav-section { border-top: 1px solid var(--border); padding-top: 4px; }
|
||||||
.dash-root .nav-group { font-size: 10px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); padding: 13px 10px 7px; }
|
.dash-root .nav-group {
|
||||||
.dash-root .nav-item { display: flex; align-items: center; gap: 11px; width: 100%; padding: 9px 11px; border-radius: 10px; border: 0; background: none; cursor: pointer; color: var(--muted); font-family: inherit; font-size: 13px; font-weight: 500; text-align: left; transition: background 0.14s, color 0.14s, transform 0.12s; }
|
letter-spacing: 1px;
|
||||||
.dash-root .nav-item svg { color: var(--faint); flex: 0 0 auto; transition: color 0.14s; }
|
text-transform: uppercase;
|
||||||
|
color: var(--faint);
|
||||||
|
padding: 13px 10px 11px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.dash-root .nav-item { cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--text);
|
||||||
|
text-align: left;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
padding: 15px 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background .14s, color .14s, transform .12s;
|
||||||
|
display: flex; }
|
||||||
|
.dash-root .nav-item svg { color: var(--text); flex: 0 0 auto; transition: color 0.14s; }
|
||||||
.dash-root .nav-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.dash-root .nav-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.dash-root .nav-item:hover { background: var(--panel-2); color: var(--text); }
|
.dash-root .nav-item:hover { background: var(--panel-2); color: var(--text); }
|
||||||
.dash-root .nav-item:hover svg { color: var(--text-2); }
|
.dash-root .nav-item:hover svg { color: var(--text-2); }
|
||||||
.dash-root .nav-item.active { background: var(--grad-brand); color: #1a1206; font-weight: 700; box-shadow: var(--glow-orange); }
|
.dash-root .nav-item.active {
|
||||||
.dash-root .nav-item.active svg { color: #1a1206; }
|
background: var(--grad-brand);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(90deg, rgba(17, 9, 9, 0) 0%, rgba(253, 169, 19, 0.21) 100%);
|
||||||
|
}
|
||||||
|
.dash-root .nav-item.active svg { color: #fff; }
|
||||||
|
|
||||||
/* ---- coming-soon placeholder ---- */
|
/* ---- coming-soon placeholder ---- */
|
||||||
.dash-root .coming-soon { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 56px 24px; gap: 8px; }
|
.dash-root .coming-soon { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 56px 24px; gap: 8px; }
|
||||||
@@ -288,7 +315,7 @@
|
|||||||
/* ---- PageHead ---- */
|
/* ---- PageHead ---- */
|
||||||
.dash-root .ds-pagehead { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
|
.dash-root .ds-pagehead { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
|
||||||
.dash-root .ds-pagehead-l { display: flex; gap: 14px; align-items: center; }
|
.dash-root .ds-pagehead-l { display: flex; gap: 14px; align-items: center; }
|
||||||
.dash-root .ds-pagehead-ic { width: 52px; height: 52px; border-radius: 16px; flex: 0 0 auto; display: grid; place-items: center; color: #1a1206; background: var(--grad-brand); box-shadow: var(--glow-orange); }
|
.dash-root .ds-pagehead-ic { width: 52px; height: 52px; border-radius: 16px; flex: 0 0 auto; display: grid; place-items: center; color: #fff; background: var(--grad-brand); box-shadow: var(--glow-orange); }
|
||||||
.dash-root .ds-pagehead h1 { font-size: 24px; }
|
.dash-root .ds-pagehead h1 { font-size: 24px; }
|
||||||
.dash-root .ds-eyebrow { font-size: 10.5px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--orange); margin-bottom: 3px; }
|
.dash-root .ds-eyebrow { font-size: 10.5px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--orange); margin-bottom: 3px; }
|
||||||
.dash-root .ds-pagehead h1 { font-size: 23px; font-weight: 800; letter-spacing: -0.02em; }
|
.dash-root .ds-pagehead h1 { font-size: 23px; font-weight: 800; letter-spacing: -0.02em; }
|
||||||
@@ -346,7 +373,7 @@
|
|||||||
.dash-root .ds-btn.s-sm { height: 32px; padding: 0 12px; font-size: 12px; border-radius: 9px; }
|
.dash-root .ds-btn.s-sm { height: 32px; padding: 0 12px; font-size: 12px; border-radius: 9px; }
|
||||||
.dash-root .ds-btn.full { width: 100%; }
|
.dash-root .ds-btn.full { width: 100%; }
|
||||||
.dash-root .ds-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
.dash-root .ds-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||||
.dash-root .ds-btn.v-primary { background: var(--grad-brand); color: #1a1206; box-shadow: 0 8px 20px -10px color-mix(in srgb, var(--orange) 75%, transparent); }
|
.dash-root .ds-btn.v-primary { background: var(--grad-brand); color: #fff; box-shadow: 0 8px 20px -10px color-mix(in srgb, var(--orange) 75%, transparent); }
|
||||||
.dash-root .ds-btn.v-primary:not(:disabled):hover { filter: brightness(1.05); transform: translateY(-1px); }
|
.dash-root .ds-btn.v-primary:not(:disabled):hover { filter: brightness(1.05); transform: translateY(-1px); }
|
||||||
.dash-root .ds-btn.v-ghost { background: transparent; color: var(--muted); }
|
.dash-root .ds-btn.v-ghost { background: transparent; color: var(--muted); }
|
||||||
.dash-root .ds-btn.v-ghost:hover { background: var(--panel-2); color: var(--text); }
|
.dash-root .ds-btn.v-ghost:hover { background: var(--panel-2); color: var(--text); }
|
||||||
@@ -363,17 +390,19 @@
|
|||||||
.dash-root .link-inline { background: none; border: 0; color: var(--orange); font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; }
|
.dash-root .link-inline { background: none; border: 0; color: var(--orange); font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; }
|
||||||
|
|
||||||
/* ---- Segmented / Tabs ---- */
|
/* ---- Segmented / Tabs ---- */
|
||||||
.dash-root .ds-segmented { display: inline-flex; gap: 3px; padding: 4px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 12px; flex-wrap: wrap; margin-bottom: 20px; }
|
.dash-root .ds-segmented { display: inline-flex; gap: 4px; padding: 5px; background: var(--panel-2); border: 1px solid var(--border-2); border-radius: 14px; flex-wrap: wrap; margin-bottom: 20px; }
|
||||||
.dash-root .ds-segmented .seg { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 14px; border: 0; border-radius: 9px; background: none; color: var(--muted); font-family: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; transition: 0.14s; }
|
.dash-root .ds-segmented .seg { display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 16px; border: 0; border-radius: 10px; background: none; color: var(--muted); font-family: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; transition: 0.14s; }
|
||||||
.dash-root .ds-segmented .seg:hover { color: var(--text); }
|
.dash-root .ds-segmented .seg:hover { color: var(--text); background: color-mix(in srgb, var(--orange) 9%, transparent); }
|
||||||
.dash-root .ds-segmented .seg.active { background: var(--panel); color: var(--text); box-shadow: var(--card-hi), 0 4px 12px -7px rgba(0,0,0,0.5); }
|
.dash-root .ds-segmented .seg.active { background: rgba(253, 169, 19, 0.92); color: #fff; box-shadow: 0 6px 16px -8px color-mix(in srgb, var(--orange) 75%, transparent); }
|
||||||
|
.dash-root .ds-segmented .seg.active:hover { background: rgba(253, 169, 19, 1); color: #fff; }
|
||||||
|
.dash-root .ds-segmented .seg.active svg { color: #fff; }
|
||||||
|
|
||||||
.dash-root .ds-tabs { display: flex; gap: 4px; padding: 5px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 14px; margin: 18px 0 22px; overflow-x: auto; }
|
.dash-root .ds-tabs { display: flex; gap: 4px; padding: 5px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 14px; margin: 18px 0 22px; overflow-x: auto; }
|
||||||
.dash-root .ds-tabs::-webkit-scrollbar { height: 0; }
|
.dash-root .ds-tabs::-webkit-scrollbar { height: 0; }
|
||||||
.dash-root .ds-tab { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border: 0; border-radius: 10px; background: none; color: var(--muted); font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: 0.16s; }
|
.dash-root .ds-tab { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border: 0; border-radius: 10px; background: none; color: var(--muted); font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: 0.16s; }
|
||||||
.dash-root .ds-tab:hover { color: var(--text-2); background: color-mix(in srgb, var(--panel) 55%, transparent); }
|
.dash-root .ds-tab:hover { color: var(--text-2); background: color-mix(in srgb, var(--panel) 55%, transparent); }
|
||||||
.dash-root .ds-tab.active { color: var(--orange); background: var(--panel); box-shadow: var(--card-hi), 0 4px 14px -8px rgba(0,0,0,0.5); }
|
.dash-root .ds-tab.active { color: var(--orange); background: var(--panel); box-shadow: var(--card-hi), 0 4px 14px -8px rgba(0,0,0,0.5); }
|
||||||
.dash-root .ds-tab-badge { background: var(--orange); color: #1a1206; font-size: 10px; font-weight: 700; min-width: 17px; height: 17px; border-radius: 99px; display: inline-grid; place-items: center; padding: 0 4px; }
|
.dash-root .ds-tab-badge { background: var(--orange); color: #fff; font-size: 10px; font-weight: 700; min-width: 17px; height: 17px; border-radius: 99px; display: inline-grid; place-items: center; padding: 0 4px; }
|
||||||
|
|
||||||
/* ---- OTP ---- */
|
/* ---- OTP ---- */
|
||||||
.dash-root .ds-otp { display: flex; gap: 9px; }
|
.dash-root .ds-otp { display: flex; gap: 9px; }
|
||||||
@@ -432,7 +461,7 @@
|
|||||||
.dash-root .profile-hero-bg { position: absolute; inset: 0; background: radial-gradient(90% 140% at 100% -10%, color-mix(in srgb, var(--orange) 22%, transparent), transparent 50%), radial-gradient(70% 120% at -5% 110%, color-mix(in srgb, var(--blue) 14%, transparent), transparent 55%); pointer-events: none; }
|
.dash-root .profile-hero-bg { position: absolute; inset: 0; background: radial-gradient(90% 140% at 100% -10%, color-mix(in srgb, var(--orange) 22%, transparent), transparent 50%), radial-gradient(70% 120% at -5% 110%, color-mix(in srgb, var(--blue) 14%, transparent), transparent 55%); pointer-events: none; }
|
||||||
.dash-root .profile-hero-main { display: flex; gap: 20px; align-items: center; position: relative; }
|
.dash-root .profile-hero-main { display: flex; gap: 20px; align-items: center; position: relative; }
|
||||||
.dash-root .profile-hero-avatar { position: relative; border-radius: 24px; box-shadow: 0 14px 34px -12px color-mix(in srgb, var(--orange) 60%, transparent); }
|
.dash-root .profile-hero-avatar { position: relative; border-radius: 24px; box-shadow: 0 14px 34px -12px color-mix(in srgb, var(--orange) 60%, transparent); }
|
||||||
.dash-root .profile-hero-cam { position: absolute; right: -4px; bottom: -4px; width: 28px; height: 28px; border-radius: 9px; border: 2px solid var(--panel); background: var(--orange); color: #1a1206; display: grid; place-items: center; cursor: pointer; }
|
.dash-root .profile-hero-cam { position: absolute; right: -4px; bottom: -4px; width: 28px; height: 28px; border-radius: 9px; border: 2px solid var(--panel); background: var(--orange); color: #fff; display: grid; place-items: center; cursor: pointer; }
|
||||||
.dash-root .profile-hero-name { display: flex; align-items: center; gap: 8px; font-size: 21px; font-weight: 800; letter-spacing: -0.02em; }
|
.dash-root .profile-hero-name { display: flex; align-items: center; gap: 8px; font-size: 21px; font-weight: 800; letter-spacing: -0.02em; }
|
||||||
.dash-root .verified-badge { color: var(--green); }
|
.dash-root .verified-badge { color: var(--green); }
|
||||||
.dash-root .profile-hero-role { font-size: 13px; color: var(--muted); margin-top: 2px; }
|
.dash-root .profile-hero-role { font-size: 13px; color: var(--muted); margin-top: 2px; }
|
||||||
@@ -456,7 +485,7 @@
|
|||||||
/* ---- KYC ---- */
|
/* ---- KYC ---- */
|
||||||
.dash-root .kyc-progress { margin-bottom: 18px; }
|
.dash-root .kyc-progress { margin-bottom: 18px; }
|
||||||
.dash-root .kyc-progress-bar { height: 9px; border-radius: 99px; background: var(--track); overflow: hidden; }
|
.dash-root .kyc-progress-bar { height: 9px; border-radius: 99px; background: var(--track); overflow: hidden; }
|
||||||
.dash-root .kyc-progress-bar > span { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--orange), var(--orange-2)); transition: width 0.5s cubic-bezier(0.2,0.7,0.2,1); animation: ds-bar 0.6s ease; }
|
.dash-root .kyc-progress-bar > span { display: block; height: 100%; border-radius: 99px; background: rgba(253, 169, 19, 0.92); transition: width 0.5s cubic-bezier(0.2,0.7,0.2,1); animation: ds-bar 0.6s ease; }
|
||||||
.dash-root .kyc-progress-legend { display: flex; gap: 16px; margin-top: 10px; flex-wrap: wrap; }
|
.dash-root .kyc-progress-legend { display: flex; gap: 16px; margin-top: 10px; flex-wrap: wrap; }
|
||||||
.dash-root .kyc-leg { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--faint); }
|
.dash-root .kyc-leg { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--faint); }
|
||||||
.dash-root .kyc-leg.verified { color: var(--green); } .dash-root .kyc-leg.uploaded { color: #6f9bff; } .dash-root .kyc-leg.missing { color: var(--muted); }
|
.dash-root .kyc-leg.verified { color: var(--green); } .dash-root .kyc-leg.uploaded { color: #6f9bff; } .dash-root .kyc-leg.missing { color: var(--muted); }
|
||||||
@@ -547,6 +576,34 @@
|
|||||||
.dash-root .tl-dot.ev-resolved, .dash-root .tl-dot.ev-closed { background: color-mix(in srgb, var(--green) 18%, transparent); color: var(--green); }
|
.dash-root .tl-dot.ev-resolved, .dash-root .tl-dot.ev-closed { background: color-mix(in srgb, var(--green) 18%, transparent); color: var(--green); }
|
||||||
.dash-root .tl-body { padding-top: 2px; } .dash-root .tl-title { font-size: 13px; font-weight: 600; } .dash-root .tl-meta { font-size: 11.5px; color: var(--muted); margin-top: 1px; } .dash-root .tl-time { font-size: 11px; color: var(--faint); margin-top: 2px; }
|
.dash-root .tl-body { padding-top: 2px; } .dash-root .tl-title { font-size: 13px; font-weight: 600; } .dash-root .tl-meta { font-size: 11.5px; color: var(--muted); margin-top: 1px; } .dash-root .tl-time { font-size: 11px; color: var(--faint); margin-top: 2px; }
|
||||||
|
|
||||||
|
/* ---- Support: command-center Overview ---- */
|
||||||
|
.dash-root .support-ov { display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
|
||||||
|
.dash-root .sup-status { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 16px; border-radius: 14px; border: 1px solid color-mix(in srgb, var(--green) 28%, var(--border)); background: color-mix(in srgb, var(--green) 9%, var(--panel)); flex-wrap: wrap; }
|
||||||
|
.dash-root .sup-status-l { display: inline-flex; align-items: center; gap: 9px; font-size: 12.5px; font-weight: 700; color: var(--green); }
|
||||||
|
.dash-root .sup-status-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 60%, transparent); animation: sup-pulse 1.8s infinite; }
|
||||||
|
.dash-root .sup-status-r { font-size: 11.5px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); }
|
||||||
|
@keyframes sup-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 55%, transparent); } 70% { box-shadow: 0 0 0 7px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
|
||||||
|
|
||||||
|
.dash-root .sup-pulse { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
|
||||||
|
.dash-root .sup-stat { position: relative; overflow: hidden; display: flex; align-items: center; gap: 13px; padding: 16px 18px; border-radius: 16px; border: 1px solid var(--border); background: var(--card-grad); box-shadow: var(--card-hi); transition: transform 0.16s, border-color 0.16s; }
|
||||||
|
.dash-root .sup-stat:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--orange) 40%, var(--border)); }
|
||||||
|
.dash-root .sup-stat-ic { width: 42px; height: 42px; border-radius: 13px; flex: 0 0 auto; display: grid; place-items: center; color: #fff; background: rgba(253, 169, 19, 0.92); box-shadow: var(--glow-orange); }
|
||||||
|
.dash-root .sup-stat-txt b { display: flex; align-items: center; gap: 4px; font-size: 22px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; }
|
||||||
|
.dash-root .sup-stat-txt b svg { color: var(--orange); }
|
||||||
|
.dash-root .sup-stat-txt span { display: block; font-size: 11.5px; color: var(--muted); margin-top: 5px; }
|
||||||
|
.dash-root .sup-stat-live { position: absolute; top: 13px; right: 13px; width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 60%, transparent); animation: sup-pulse 1.8s infinite; }
|
||||||
|
|
||||||
|
/* hero live-signal rings */
|
||||||
|
.dash-root .bento-chat-sig { position: absolute; right: 34px; top: 26px; width: 10px; height: 10px; pointer-events: none; z-index: 1; }
|
||||||
|
.dash-root .bento-chat-sig span { position: absolute; inset: 0; margin: auto; width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,0.55); animation: sup-ring 2.6s ease-out infinite; }
|
||||||
|
.dash-root .bento-chat-sig span:nth-child(2) { animation-delay: 0.85s; }
|
||||||
|
.dash-root .bento-chat-sig span:nth-child(3) { animation-delay: 1.7s; }
|
||||||
|
@keyframes sup-ring { 0% { transform: scale(1); opacity: 0.7; } 100% { transform: scale(6.5); opacity: 0; } }
|
||||||
|
|
||||||
|
@media (max-width: 1180px) { .dash-root .sup-pulse { grid-template-columns: 1fr 1fr; } }
|
||||||
|
@media (max-width: 560px) { .dash-root .sup-pulse { grid-template-columns: 1fr 1fr; } }
|
||||||
|
|
||||||
/* ---- Support: Bento hub ---- */
|
/* ---- Support: Bento hub ---- */
|
||||||
.dash-root .support-bento {
|
.dash-root .support-bento {
|
||||||
display: grid; gap: 16px; grid-template-columns: repeat(4, 1fr);
|
display: grid; gap: 16px; grid-template-columns: repeat(4, 1fr);
|
||||||
@@ -561,20 +618,20 @@
|
|||||||
.dash-root .bento-tile-head h3 { font-size: 15px; font-weight: 700; }
|
.dash-root .bento-tile-head h3 { font-size: 15px; font-weight: 700; }
|
||||||
|
|
||||||
/* hero live-chat tile */
|
/* hero live-chat tile */
|
||||||
.dash-root .bento-chat { grid-area: chat; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; gap: 20px; min-height: 268px; text-align: left; cursor: pointer; font-family: inherit; border: 0; color: #1a1206; background: linear-gradient(135deg, #ffb43c 0%, #fda913 45%, #fd6d13 100%); box-shadow: var(--glow-orange); transition: transform 0.18s, box-shadow 0.18s; }
|
.dash-root .bento-chat { grid-area: chat; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; gap: 20px; min-height: 268px; text-align: left; cursor: pointer; font-family: inherit; border: 0; color: #fff; background: rgba(253, 169, 19, 0.92); box-shadow: var(--glow-orange); transition: transform 0.18s, box-shadow 0.18s; }
|
||||||
.dash-root .bento-chat:hover { transform: translateY(-3px); box-shadow: 0 26px 54px -20px color-mix(in srgb, var(--orange) 85%, #000); }
|
.dash-root .bento-chat:hover { transform: translateY(-3px); box-shadow: 0 26px 54px -20px color-mix(in srgb, var(--orange) 85%, #000); }
|
||||||
.dash-root .bento-chat::after { content: ""; position: absolute; right: -70px; top: -70px; width: 260px; height: 260px; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.28), transparent 68%); pointer-events: none; }
|
.dash-root .bento-chat::after { content: ""; position: absolute; right: -70px; top: -70px; width: 260px; height: 260px; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.28), transparent 68%); pointer-events: none; }
|
||||||
.dash-root .bento-chat::before { content: ""; position: absolute; left: -40px; bottom: -80px; width: 220px; height: 220px; border-radius: 50%; background: radial-gradient(circle, rgba(26,18,6,0.12), transparent 70%); pointer-events: none; }
|
.dash-root .bento-chat::before { content: ""; position: absolute; left: -40px; bottom: -80px; width: 220px; height: 220px; border-radius: 50%; background: radial-gradient(circle, rgba(26,18,6,0.12), transparent 70%); pointer-events: none; }
|
||||||
.dash-root .bento-chat-top { display: flex; justify-content: space-between; align-items: center; gap: 10px; position: relative; z-index: 1; font-size: 12px; font-weight: 600; flex-wrap: wrap; }
|
.dash-root .bento-chat-top { display: flex; justify-content: space-between; align-items: center; gap: 10px; position: relative; z-index: 1; font-size: 12px; font-weight: 600; flex-wrap: wrap; }
|
||||||
.dash-root .bento-chat-badge { display: inline-flex; align-items: center; gap: 6px; background: rgba(26,18,6,0.14); padding: 6px 11px; border-radius: 99px; }
|
.dash-root .bento-chat-badge { display: inline-flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.2); padding: 6px 11px; border-radius: 99px; -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
|
||||||
.dash-root .bento-chat-badge .ds-statusdot { background: #15461f; }
|
.dash-root .bento-chat-badge .ds-statusdot { background: #0c3517; box-shadow: 0 0 0 3px rgba(12,53,23,0.3); }
|
||||||
.dash-root .bento-chat-wait { display: inline-flex; align-items: center; gap: 5px; color: rgba(26,18,6,0.72); }
|
.dash-root .bento-chat-wait { display: inline-flex; align-items: center; gap: 5px; color: rgba(255,255,255,0.85); }
|
||||||
.dash-root .bento-chat-mid { position: relative; z-index: 1; }
|
.dash-root .bento-chat-mid { position: relative; z-index: 1; }
|
||||||
.dash-root .bento-chat-mid h2 { font-size: 28px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.12; }
|
.dash-root .bento-chat-mid h2 { font-size: 28px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.12; }
|
||||||
.dash-root .bento-chat-mid p { font-size: 13px; margin-top: 9px; color: rgba(26,18,6,0.8); max-width: 360px; line-height: 1.5; }
|
.dash-root .bento-chat-mid p { font-size: 13px; margin-top: 9px; color: rgba(255,255,255,0.88); max-width: 360px; line-height: 1.5; }
|
||||||
.dash-root .bento-chat-foot { display: flex; align-items: center; justify-content: space-between; gap: 14px; position: relative; z-index: 1; flex-wrap: wrap; }
|
.dash-root .bento-chat-foot { display: flex; align-items: center; justify-content: space-between; gap: 14px; position: relative; z-index: 1; flex-wrap: wrap; }
|
||||||
.dash-root .bento-avstack { display: flex; align-items: center; }
|
.dash-root .bento-avstack { display: flex; align-items: center; }
|
||||||
.dash-root .bento-avstack .ds-avatar { margin-left: -9px; box-shadow: 0 0 0 2px #fd8a13; border-radius: 50%; }
|
.dash-root .bento-avstack .ds-avatar { margin-left: -9px; box-shadow: 0 0 0 2px rgba(255,255,255,0.92); border-radius: 50%; }
|
||||||
.dash-root .bento-avstack .ds-avatar:first-child { margin-left: 0; }
|
.dash-root .bento-avstack .ds-avatar:first-child { margin-left: 0; }
|
||||||
.dash-root .bento-av-more { margin-left: 11px; font-size: 12px; font-weight: 700; }
|
.dash-root .bento-av-more { margin-left: 11px; font-size: 12px; font-weight: 700; }
|
||||||
.dash-root .bento-chat-cta { display: inline-flex; align-items: center; gap: 8px; background: #1a1206; color: #fff; padding: 12px 20px; border-radius: 12px; font-size: 13.5px; font-weight: 700; transition: transform 0.14s; }
|
.dash-root .bento-chat-cta { display: inline-flex; align-items: center; gap: 8px; background: #1a1206; color: #fff; padding: 12px 20px; border-radius: 12px; font-size: 13.5px; font-weight: 700; transition: transform 0.14s; }
|
||||||
@@ -647,7 +704,7 @@
|
|||||||
.dash-root .msg-thread-sub { font-size: 12px; color: var(--text-2); margin-top: 1px; }
|
.dash-root .msg-thread-sub { font-size: 12px; color: var(--text-2); margin-top: 1px; }
|
||||||
.dash-root .msg-thread-prev { font-size: 11.5px; color: var(--muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.dash-root .msg-thread-prev { font-size: 11.5px; color: var(--muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.dash-root .typing-now { color: var(--orange); font-style: italic; }
|
.dash-root .typing-now { color: var(--orange); font-style: italic; }
|
||||||
.dash-root .msg-unread { position: absolute; right: 11px; bottom: 11px; background: var(--orange); color: #1a1206; font-size: 10px; font-weight: 700; min-width: 17px; height: 17px; border-radius: 99px; display: grid; place-items: center; padding: 0 4px; }
|
.dash-root .msg-unread { position: absolute; right: 11px; bottom: 11px; background: var(--orange); color: #fff; font-size: 10px; font-weight: 700; min-width: 17px; height: 17px; border-radius: 99px; display: grid; place-items: center; padding: 0 4px; }
|
||||||
.dash-root .msg-pin { position: absolute; right: 12px; top: 12px; color: var(--faint); }
|
.dash-root .msg-pin { position: absolute; right: 12px; top: 12px; color: var(--faint); }
|
||||||
.dash-root .msg-thread-pane { display: flex; flex-direction: column; min-height: 0; }
|
.dash-root .msg-thread-pane { display: flex; flex-direction: column; min-height: 0; }
|
||||||
.dash-root .msg-pane-head { display: flex; align-items: center; gap: 11px; padding: 14px 18px; border-bottom: 1px solid var(--border); }
|
.dash-root .msg-pane-head { display: flex; align-items: center; gap: 11px; padding: 14px 18px; border-bottom: 1px solid var(--border); }
|
||||||
@@ -659,7 +716,7 @@
|
|||||||
.dash-root .bubble { padding: 9px 13px; border-radius: 14px; background: var(--panel-2); border: 1px solid var(--border); }
|
.dash-root .bubble { padding: 9px 13px; border-radius: 14px; background: var(--panel-2); border: 1px solid var(--border); }
|
||||||
.dash-root .bubble p { margin: 0; font-size: 13px; line-height: 1.45; }
|
.dash-root .bubble p { margin: 0; font-size: 13px; line-height: 1.45; }
|
||||||
.dash-root .bubble-time { display: block; font-size: 10px; color: var(--faint); margin-top: 4px; }
|
.dash-root .bubble-time { display: block; font-size: 10px; color: var(--faint); margin-top: 4px; }
|
||||||
.dash-root .bubble-row.me .bubble { background: var(--orange); border-color: transparent; color: #1a1206; } .dash-root .bubble-row.me .bubble-time { color: color-mix(in srgb, #1a1206 60%, transparent); }
|
.dash-root .bubble-row.me .bubble { background: var(--orange); border-color: transparent; color: #fff; } .dash-root .bubble-row.me .bubble-time { color: rgba(255, 255, 255, 0.72); }
|
||||||
.dash-root .bubble.typing { display: flex; gap: 4px; padding: 13px; }
|
.dash-root .bubble.typing { display: flex; gap: 4px; padding: 13px; }
|
||||||
.dash-root .bubble.typing .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); animation: ds-bounce 1.2s infinite; }
|
.dash-root .bubble.typing .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); animation: ds-bounce 1.2s infinite; }
|
||||||
.dash-root .bubble.typing .dot:nth-child(2) { animation-delay: 0.18s; } .dash-root .bubble.typing .dot:nth-child(3) { animation-delay: 0.36s; }
|
.dash-root .bubble.typing .dot:nth-child(2) { animation-delay: 0.18s; } .dash-root .bubble.typing .dot:nth-child(3) { animation-delay: 0.36s; }
|
||||||
@@ -727,22 +784,27 @@
|
|||||||
.dash-root .cb-slot.active { border-color: var(--orange); background: color-mix(in srgb, var(--orange) 9%, transparent); }
|
.dash-root .cb-slot.active { border-color: var(--orange); background: color-mix(in srgb, var(--orange) 9%, transparent); }
|
||||||
.dash-root .cb-slot-l { font-size: 13px; font-weight: 600; } .dash-root .cb-slot-r { font-size: 12px; color: var(--muted); }
|
.dash-root .cb-slot-l { font-size: 13px; font-weight: 600; } .dash-root .cb-slot-r { font-size: 12px; color: var(--muted); }
|
||||||
|
|
||||||
/* ---- Rules grid ---- */
|
/* ---- Rules grid (uniform brand-orange cards) ---- */
|
||||||
.dash-root .rules-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
|
.dash-root .rules-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: start; }
|
||||||
.dash-root .rule-card { position: relative; overflow: hidden; --accent: var(--orange); transition: transform 0.18s, box-shadow 0.18s, border-color 0.18s; }
|
.dash-root .rule-card { position: relative; overflow: hidden; display: flex; flex-direction: column; --accent: var(--orange); transition: transform 0.18s, box-shadow 0.18s, border-color 0.18s; }
|
||||||
.dash-root .rule-card::before { content: ""; position: absolute; top: -55%; right: -15%; width: 170px; height: 170px; border-radius: 50%; background: radial-gradient(circle, color-mix(in srgb, var(--accent) 18%, transparent), transparent 70%); pointer-events: none; }
|
.dash-root .rule-watermark { position: absolute; right: 8px; bottom: -22px; font-size: 120px; font-weight: 800; letter-spacing: -0.05em; line-height: 1; color: color-mix(in srgb, var(--accent) 12%, transparent); pointer-events: none; user-select: none; }
|
||||||
|
.dash-root .rule-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; position: relative; z-index: 1; }
|
||||||
|
.dash-root .rule-ic { width: 44px; height: 44px; border-radius: 13px; display: grid; place-items: center; flex: 0 0 auto; color: var(--accent); background: color-mix(in srgb, var(--accent) 15%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent); }
|
||||||
.dash-root .rule-card:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--accent) 45%, transparent); box-shadow: 0 18px 42px -24px color-mix(in srgb, var(--accent) 70%, #000); }
|
.dash-root .rule-card:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--accent) 45%, transparent); box-shadow: 0 18px 42px -24px color-mix(in srgb, var(--accent) 70%, #000); }
|
||||||
.dash-root .rule-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; position: relative; }
|
.dash-root .rule-title { font-size: 15px; font-weight: 700; margin-bottom: 8px; position: relative; z-index: 1; letter-spacing: -0.01em; }
|
||||||
.dash-root .rule-n { font-size: 44px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 45%, var(--text))); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
|
.dash-root .rule-detail { font-size: 12.5px; color: var(--muted); line-height: 1.55; margin: 0; position: relative; z-index: 1; }
|
||||||
.dash-root .rule-title { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; margin-bottom: 8px; } .dash-root .rule-title svg { color: var(--accent); flex: 0 0 auto; }
|
.dash-root .rule-foot { display: flex; align-items: center; gap: 7px; margin-top: 16px; padding-top: 13px; border-top: 1px solid var(--border); font-size: 11px; font-weight: 600; color: var(--accent); position: relative; z-index: 1; }
|
||||||
.dash-root .rule-detail { font-size: 12.5px; color: var(--muted); line-height: 1.55; margin: 0; }
|
|
||||||
|
|
||||||
/* ---- responsive ---- */
|
/* ---- responsive ---- */
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.dash-root .grid-side, .dash-root .consent-cols, .dash-root .rules-grid, .dash-root .channel-grid, .dash-root .help-topics, .dash-root .grid-2 { grid-template-columns: 1fr; }
|
.dash-root .grid-side, .dash-root .consent-cols, .dash-root .channel-grid, .dash-root .help-topics, .dash-root .grid-2 { grid-template-columns: 1fr; }
|
||||||
|
.dash-root .rules-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
.dash-root .msgctr { grid-template-columns: 1fr; height: auto; }
|
.dash-root .msgctr { grid-template-columns: 1fr; height: auto; }
|
||||||
.dash-root .msg-inbox { border-right: 0; border-bottom: 1px solid var(--border); max-height: 280px; }
|
.dash-root .msg-inbox { border-right: 0; border-bottom: 1px solid var(--border); max-height: 280px; }
|
||||||
}
|
}
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.dash-root .rules-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.dash-root .notif-row { grid-template-columns: 1fr; gap: 10px; }
|
.dash-root .notif-row { grid-template-columns: 1fr; gap: 10px; }
|
||||||
.dash-root .notif-head { display: none; }
|
.dash-root .notif-head { display: none; }
|
||||||
@@ -751,3 +813,295 @@
|
|||||||
.dash-root .pw-checks, .dash-root .time-windows { grid-template-columns: 1fr; }
|
.dash-root .pw-checks, .dash-root .time-windows { grid-template-columns: 1fr; }
|
||||||
.dash-root .profile-hero-stats { padding-left: 0; border-top: 1px solid var(--border); padding-top: 14px; width: 100%; }
|
.dash-root .profile-hero-stats { padding-left: 0; border-top: 1px solid var(--border); padding-top: 14px; width: 100%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================================== */
|
||||||
|
/* Glass box treatment — every card wrapped in a brand-brown */
|
||||||
|
/* border over a translucent black, blurred (dark theme). */
|
||||||
|
/* =========================================================== */
|
||||||
|
.dash-root[data-theme="dark"] .card {
|
||||||
|
background: rgba(0, 0, 0, 0.54);
|
||||||
|
border: 0.5px solid #452B1A;
|
||||||
|
-webkit-backdrop-filter: blur(2px);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
.dash-root[data-theme="dark"] .ds-modal,
|
||||||
|
.dash-root[data-theme="dark"] .tm-menu-pop {
|
||||||
|
border: 0.5px solid #452B1A;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================== */
|
||||||
|
/* Team Management */
|
||||||
|
/* =========================================================== */
|
||||||
|
.dash-root .tm { display: flex; flex-direction: column; gap: 18px; }
|
||||||
|
|
||||||
|
/* ---- Crew hero ---- */
|
||||||
|
.dash-root .tm-hero { position: relative; overflow: hidden; border-radius: 24px; padding: 28px 30px; display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; border: 0.5px solid #452B1A; background: var(--panel); }
|
||||||
|
.dash-root[data-theme="light"] .tm-hero { border: 1px solid var(--border); }
|
||||||
|
.dash-root .tm-hero-orbs { position: absolute; inset: 0; pointer-events: none; background:
|
||||||
|
radial-gradient(42% 120% at 92% -20%, color-mix(in srgb, var(--blue) 16%, transparent), transparent 60%),
|
||||||
|
radial-gradient(40% 120% at 8% 130%, rgba(255,255,255,0.05), transparent 62%); }
|
||||||
|
.dash-root .tm-hero-grid { position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
|
||||||
|
background-image: linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px);
|
||||||
|
background-size: 30px 30px; -webkit-mask-image: radial-gradient(70% 120% at 80% 0%, #000, transparent 70%); mask-image: radial-gradient(70% 120% at 80% 0%, #000, transparent 70%); }
|
||||||
|
.dash-root .tm-hero-main { position: relative; display: flex; align-items: center; gap: 22px; min-width: 0; }
|
||||||
|
.dash-root .tm-stack { display: flex; align-items: center; flex: 0 0 auto; }
|
||||||
|
.dash-root .tm-stack-av { margin-left: -14px; border-radius: 50%; box-shadow: 0 0 0 3px var(--panel); transition: 0.18s; }
|
||||||
|
.dash-root .tm-stack-av:first-child { margin-left: 0; }
|
||||||
|
.dash-root .tm-stack-av:hover { transform: translateY(-5px); z-index: 20 !important; }
|
||||||
|
.dash-root .tm-stack-more { margin-left: -14px; width: 46px; height: 46px; border-radius: 50%; display: grid; place-items: center; font-size: 13px; font-weight: 800; color: #fff; background: var(--grad-brand); box-shadow: 0 0 0 3px var(--panel); }
|
||||||
|
.dash-root .tm-hero-copy { min-width: 0; }
|
||||||
|
.dash-root .tm-hero-eyebrow { display: inline-flex; align-items: center; gap: 6px; font-size: 10.5px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--orange); }
|
||||||
|
.dash-root .tm-hero-copy h1 { font-size: 26px; font-weight: 800; letter-spacing: -0.03em; margin: 6px 0 4px; }
|
||||||
|
.dash-root .tm-hero-copy p { font-size: 13px; color: var(--muted); line-height: 1.5; max-width: 460px; }
|
||||||
|
.dash-root .tm-hero-live { display: inline-flex; align-items: center; gap: 8px; margin-top: 14px; font-size: 12.5px; color: var(--text-2); font-weight: 600; }
|
||||||
|
.dash-root .tm-hero-live b { color: var(--green); }
|
||||||
|
.dash-root .tm-pulse { width: 9px; height: 9px; border-radius: 50%; background: var(--green); animation: tm-pulse 1.8s infinite; }
|
||||||
|
.dash-root .tm-hero-dot { width: 3px; height: 3px; border-radius: 50%; background: var(--faint); margin: 0 4px; }
|
||||||
|
.dash-root .tm-hero-actions { position: relative; flex: 0 0 auto; }
|
||||||
|
@keyframes tm-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 55%, transparent); } 70% { box-shadow: 0 0 0 8px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
|
||||||
|
|
||||||
|
/* ---- Metric strip ---- */
|
||||||
|
.dash-root .tm-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
|
||||||
|
.dash-root .card.tm-stat { display: flex; flex-direction: column; gap: 16px; padding: 20px; position: relative; overflow: hidden; transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s; }
|
||||||
|
.dash-root .card.tm-stat:hover { transform: translateY(-4px); border-color: color-mix(in srgb, var(--rc) 42%, var(--border)) !important; box-shadow: 0 22px 44px -28px color-mix(in srgb, var(--rc) 80%, #000); }
|
||||||
|
.dash-root .card.tm-stat::after { content: ""; position: absolute; top: -40%; right: -20%; width: 150px; height: 150px; border-radius: 50%; background: radial-gradient(circle, color-mix(in srgb, var(--rc) 20%, transparent), transparent 70%); opacity: 0; transition: opacity 0.25s; pointer-events: none; }
|
||||||
|
.dash-root .card.tm-stat:hover::after { opacity: 1; }
|
||||||
|
.dash-root .tm-stat-head { display: flex; align-items: center; gap: 14px; position: relative; z-index: 1; }
|
||||||
|
.dash-root .tm-stat-ic { width: 46px; height: 46px; border-radius: 14px; display: grid; place-items: center; flex: 0 0 auto; color: var(--rc); background: color-mix(in srgb, var(--rc) 16%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rc) 30%, transparent); }
|
||||||
|
.dash-root .tm-stat-body { min-width: 0; }
|
||||||
|
.dash-root .tm-stat-val { font-size: 30px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; }
|
||||||
|
.dash-root .tm-stat-lbl { font-size: 12.5px; color: var(--text-2); font-weight: 600; margin-top: 6px; }
|
||||||
|
.dash-root .tm-stat-bar { display: block; height: 6px; border-radius: 99px; background: color-mix(in srgb, var(--rc) 16%, transparent); overflow: hidden; position: relative; z-index: 1; }
|
||||||
|
.dash-root .tm-stat-bar > span { display: block; height: 100%; border-radius: 99px; background: var(--rc); }
|
||||||
|
.dash-root .tm-stat-foot { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--faint); padding-top: 14px; border-top: 1px solid var(--border); margin-top: auto; position: relative; z-index: 1; }
|
||||||
|
.dash-root .tm-stat-dot { width: 6px; height: 6px; border-radius: 50%; flex: 0 0 auto; background: var(--rc); }
|
||||||
|
|
||||||
|
/* ---- Tabs ---- */
|
||||||
|
.dash-root .tm-tabs { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.dash-root .tm-tab { display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 15px; border-radius: 12px; border: 1px solid var(--border-2); background: var(--panel-2); color: var(--muted); font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; transition: 0.14s; }
|
||||||
|
.dash-root .tm-tab:hover { color: var(--text); border-color: color-mix(in srgb, var(--orange) 40%, var(--border-2)); }
|
||||||
|
.dash-root .tm-tab.active { color: #fff; background: rgba(253, 169, 19, 0.92); border-color: transparent; box-shadow: var(--glow-orange); }
|
||||||
|
.dash-root .tm-tab.active svg { color: #fff; }
|
||||||
|
.dash-root .tm-tab-badge { min-width: 19px; height: 19px; padding: 0 5px; border-radius: 99px; display: inline-grid; place-items: center; font-size: 10.5px; font-weight: 800; background: color-mix(in srgb, var(--text) 12%, transparent); color: var(--text-2); }
|
||||||
|
.dash-root .tm-tab.active .tm-tab-badge { background: rgba(255,255,255,0.26); color: #fff; }
|
||||||
|
|
||||||
|
/* Brand action — solid orange (no gradient) + white text/icon */
|
||||||
|
.dash-root .tm .ds-btn.v-primary { background: rgba(253, 169, 19, 0.92); color: #fff; border-radius: 9.132px; box-shadow: 0 8px 20px -10px rgba(253, 169, 19, 0.75); }
|
||||||
|
.dash-root .tm .ds-btn.v-primary svg { color: #fff; }
|
||||||
|
.dash-root .tm .ds-btn.v-primary:not(:disabled):hover { background: rgba(253, 169, 19, 1); }
|
||||||
|
|
||||||
|
.dash-root .view-body { display: flex; flex-direction: column; gap: 14px; }
|
||||||
|
|
||||||
|
.dash-root .tm-toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.dash-root .tm-search { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 220px; height: 44px; padding: 0 13px; border-radius: 13px; border: 1px solid var(--border-2); background: var(--panel-2); color: var(--muted); transition: 0.14s; }
|
||||||
|
.dash-root .tm-search:focus-within { border-color: var(--orange); box-shadow: 0 0 0 3px var(--ring); color: var(--orange); }
|
||||||
|
.dash-root .tm-search .ds-input { flex: 1; }
|
||||||
|
.dash-root .tm-search-x { border: 0; background: none; color: var(--faint); cursor: pointer; display: grid; place-items: center; padding: 2px; border-radius: 6px; }
|
||||||
|
.dash-root .tm-search-x:hover { color: var(--text); background: var(--panel-3); }
|
||||||
|
.dash-root .tm-viewtoggle { display: inline-flex; padding: 4px; gap: 4px; border-radius: 12px; border: 1px solid var(--border-2); background: var(--panel-2); }
|
||||||
|
.dash-root .tm-viewtoggle button { width: 34px; height: 34px; border: 0; border-radius: 9px; background: none; color: var(--muted); display: grid; place-items: center; cursor: pointer; transition: 0.14s; }
|
||||||
|
.dash-root .tm-viewtoggle button:hover { color: var(--text); }
|
||||||
|
.dash-root .tm-viewtoggle button.on { background: var(--grad-brand); color: #fff; }
|
||||||
|
|
||||||
|
/* ---- Role filter chips ---- */
|
||||||
|
.dash-root .tm-chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.dash-root .tm-chip { --rc: var(--text-2); display: inline-flex; align-items: center; gap: 7px; height: 32px; padding: 0 12px; border-radius: 99px; border: 1px solid var(--border-2); background: var(--panel-2); color: var(--text-2); font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer; transition: 0.14s; }
|
||||||
|
.dash-root .tm-chip:hover { border-color: color-mix(in srgb, var(--rc) 55%, var(--border-2)); }
|
||||||
|
.dash-root .tm-chip.on { color: var(--rc); border-color: color-mix(in srgb, var(--rc) 55%, transparent); background: color-mix(in srgb, var(--rc) 13%, transparent); }
|
||||||
|
.dash-root .tm-chip-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
|
||||||
|
.dash-root .tm-chip i { font-style: normal; font-weight: 800; opacity: 0.7; }
|
||||||
|
|
||||||
|
/* ---- People gallery ---- */
|
||||||
|
.dash-root .tm-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(252px, 1fr)); gap: 14px; }
|
||||||
|
.dash-root .card.tm-pcard { position: relative; overflow: hidden; padding: 18px; display: flex; flex-direction: column; transition: 0.18s; --rc: var(--orange); }
|
||||||
|
.dash-root .card.tm-pcard:hover { transform: translateY(-4px); border-color: color-mix(in srgb, var(--rc) 50%, var(--border)) !important; box-shadow: 0 22px 44px -26px color-mix(in srgb, var(--rc) 85%, #000); }
|
||||||
|
.dash-root .tm-pcard-bg { position: absolute; inset: 0; pointer-events: none; background: radial-gradient(80% 60% at 100% 0%, color-mix(in srgb, var(--rc) 16%, transparent), transparent 60%); opacity: 0; transition: 0.18s; }
|
||||||
|
.dash-root .card.tm-pcard:hover .tm-pcard-bg { opacity: 1; }
|
||||||
|
.dash-root .tm-pcard-top { display: flex; align-items: flex-start; justify-content: space-between; position: relative; }
|
||||||
|
.dash-root .tm-pcard-av { position: relative; border-radius: 50%; padding: 3px; background: color-mix(in srgb, var(--rc) 65%, transparent); }
|
||||||
|
.dash-root .tm-pcard-name { display: flex; align-items: center; gap: 7px; font-size: 15px; font-weight: 800; letter-spacing: -0.01em; margin-top: 13px; }
|
||||||
|
.dash-root .tm-crown { color: var(--orange); display: inline-grid; place-items: center; }
|
||||||
|
.dash-root .tm-pcard-title { font-size: 12.5px; color: var(--text-2); font-weight: 600; margin-top: 3px; }
|
||||||
|
.dash-root .tm-pcard-email { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--muted); margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.dash-root .tm-pcard-email svg { color: var(--faint); flex: 0 0 auto; }
|
||||||
|
.dash-root .tm-pcard-role { margin-top: 14px; }
|
||||||
|
.dash-root .tm-meter { margin-top: 14px; }
|
||||||
|
.dash-root .tm-meter-h { display: flex; align-items: center; justify-content: space-between; font-size: 11px; color: var(--faint); margin-bottom: 6px; }
|
||||||
|
.dash-root .tm-meter-h b { color: var(--text); font-size: 12px; font-weight: 800; }
|
||||||
|
.dash-root .tm-meter-track { display: block; height: 6px; border-radius: 99px; background: color-mix(in srgb, var(--rc) 14%, transparent); overflow: hidden; }
|
||||||
|
.dash-root .tm-meter-fill { display: block; height: 100%; border-radius: 99px; background: var(--rc); transition: width 0.5s cubic-bezier(0.2,0.7,0.2,1); }
|
||||||
|
.dash-root .tm-pcard-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 15px; padding-top: 13px; border-top: 1px solid var(--border); }
|
||||||
|
|
||||||
|
.dash-root .tm-table { display: flex; flex-direction: column; }
|
||||||
|
.dash-root .tm-row { display: grid; grid-template-columns: minmax(220px, 2.4fr) 210px 190px 110px 52px; align-items: center; gap: 14px; padding: 13px 18px; border-bottom: 1px solid var(--border); }
|
||||||
|
.dash-root .tm-row:last-child { border-bottom: 0; }
|
||||||
|
.dash-root .tm-head { padding-top: 12px; padding-bottom: 12px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--faint); background: var(--panel-2); }
|
||||||
|
.dash-root .tm-row:not(.tm-head):hover { background: color-mix(in srgb, var(--panel-2) 60%, transparent); }
|
||||||
|
.dash-root .tm-member { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||||
|
.dash-root .tm-member-meta { min-width: 0; }
|
||||||
|
.dash-root .tm-name { font-size: 13.5px; font-weight: 700; display: flex; align-items: center; flex-wrap: wrap; }
|
||||||
|
.dash-root .tm-sub { font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.dash-root .tm-c-num { text-align: right; font-size: 14px; }
|
||||||
|
.dash-root .tm-c-num b { font-weight: 800; }
|
||||||
|
.dash-root .tm-dash { color: var(--faint); }
|
||||||
|
.dash-root .tm-c-act { display: flex; justify-content: center; position: relative; }
|
||||||
|
|
||||||
|
.dash-root .tm-rolebadge { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 11px; border-radius: 99px; font-size: 12px; font-weight: 700; color: var(--rc); background: color-mix(in srgb, var(--rc) 14%, transparent); border: 1px solid color-mix(in srgb, var(--rc) 32%, transparent); white-space: nowrap; }
|
||||||
|
.dash-root .tm-roleselect { position: relative; display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 10px 0 11px; border-radius: 10px; border: 1px solid var(--border-2); background: var(--panel-2); transition: 0.14s; }
|
||||||
|
.dash-root .tm-roleselect:hover { border-color: color-mix(in srgb, var(--rc) 55%, transparent); }
|
||||||
|
.dash-root .tm-roledot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; background: var(--rc); box-shadow: 0 0 0 3px color-mix(in srgb, var(--rc) 22%, transparent); }
|
||||||
|
.dash-root .tm-roleselect .ds-select { border: 0; background-color: transparent; height: 34px; padding: 0 26px 0 0; font-weight: 600; font-size: 12.5px; box-shadow: none; background-position: calc(100% - 6px) 15px, calc(100% - 1px) 15px; }
|
||||||
|
.dash-root .tm-roleselect .ds-select:focus { box-shadow: none; }
|
||||||
|
|
||||||
|
.dash-root .tm-statuscell { display: flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 600; }
|
||||||
|
.dash-root .tm-lastactive { color: var(--faint); font-weight: 500; font-size: 11.5px; margin-left: 2px; }
|
||||||
|
|
||||||
|
.dash-root .tm-menu-lock { color: var(--faint); display: grid; place-items: center; }
|
||||||
|
.dash-root .tm-menu-pop { position: absolute; top: 38px; right: 0; z-index: 30; min-width: 178px; padding: 6px; border-radius: 13px; border: 1px solid var(--border-2); background: var(--panel); box-shadow: var(--shadow), 0 8px 30px -12px rgba(0,0,0,0.6); animation: ds-rise 0.14s ease; }
|
||||||
|
.dash-root .tm-menu-scrim { position: fixed; inset: 0; z-index: 20; }
|
||||||
|
.dash-root .tm-menu-pop button { display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: none; color: var(--text-2); font-family: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; text-align: left; }
|
||||||
|
.dash-root .tm-menu-pop button:hover { background: var(--panel-2); color: var(--text); }
|
||||||
|
.dash-root .tm-menu-pop button.danger { color: var(--red); }
|
||||||
|
.dash-root .tm-menu-pop button.danger:hover { background: color-mix(in srgb, var(--red) 14%, transparent); }
|
||||||
|
.dash-root .tm-menu-sep { height: 1px; background: var(--border); margin: 5px 4px; }
|
||||||
|
|
||||||
|
.dash-root .tm-empty { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 54px 20px; color: var(--muted); text-align: center; }
|
||||||
|
.dash-root .tm-empty-ic { width: 56px; height: 56px; border-radius: 17px; display: grid; place-items: center; color: var(--orange); background: color-mix(in srgb, var(--orange) 13%, transparent); }
|
||||||
|
.dash-root .tm-empty p { font-size: 13px; }
|
||||||
|
|
||||||
|
.dash-root .tm-roles { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 16px; }
|
||||||
|
.dash-root .tm-rolecard { position: relative; overflow: hidden; --rc: var(--orange); }
|
||||||
|
.dash-root .tm-rolecard::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--rc); }
|
||||||
|
.dash-root .tm-rolecard-head { display: flex; align-items: center; gap: 13px; margin-bottom: 12px; }
|
||||||
|
.dash-root .tm-ring { position: relative; width: 46px; height: 46px; border-radius: 50%; flex: 0 0 auto; display: grid; place-items: center; background: conic-gradient(var(--rc) calc(var(--pct) * 1%), color-mix(in srgb, var(--rc) 16%, transparent) 0); }
|
||||||
|
.dash-root .tm-ring-in { width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; color: var(--rc); background: var(--panel); }
|
||||||
|
.dash-root[data-theme="dark"] .tm-ring-in { background: #14110d; }
|
||||||
|
.dash-root .tm-rolecard-name { font-size: 15px; font-weight: 800; display: flex; align-items: center; }
|
||||||
|
.dash-root .tm-rolecard-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||||
|
.dash-root .tm-rolecard-desc { font-size: 12.5px; color: var(--muted); line-height: 1.5; margin: 0 0 16px; }
|
||||||
|
.dash-root .tm-permgroup { margin-bottom: 14px; }
|
||||||
|
.dash-root .tm-permgroup:last-child { margin-bottom: 0; }
|
||||||
|
.dash-root .tm-permgroup-h { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--faint); margin-bottom: 8px; }
|
||||||
|
.dash-root .tm-perm { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 11px; border-radius: 11px; border: 1px solid transparent; transition: 0.14s; }
|
||||||
|
.dash-root .tm-perm:hover { background: var(--panel-2); }
|
||||||
|
.dash-root .tm-perm.on { background: color-mix(in srgb, var(--rc) 8%, transparent); border-color: color-mix(in srgb, var(--rc) 22%, transparent); }
|
||||||
|
.dash-root .tm-perm-meta { min-width: 0; }
|
||||||
|
.dash-root .tm-perm-label { display: block; font-size: 12.5px; font-weight: 600; }
|
||||||
|
.dash-root .tm-perm-desc { display: block; font-size: 11px; color: var(--faint); margin-top: 2px; }
|
||||||
|
|
||||||
|
.dash-root .tm-invite-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 18px 20px; border-bottom: 1px solid var(--border); }
|
||||||
|
.dash-root .tm-invite-head h3 { font-size: 14.5px; font-weight: 700; }
|
||||||
|
.dash-root .tm-invite-head p { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||||
|
.dash-root .tm-invites { display: flex; flex-direction: column; }
|
||||||
|
.dash-root .tm-inviterow { display: flex; align-items: center; gap: 13px; padding: 14px 20px; border-bottom: 1px solid var(--border); }
|
||||||
|
.dash-root .tm-inviterow:last-child { border-bottom: 0; }
|
||||||
|
.dash-root .tm-invite-ic { width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center; flex: 0 0 auto; color: var(--blue); background: color-mix(in srgb, var(--blue) 14%, transparent); }
|
||||||
|
.dash-root .tm-invite-meta { flex: 1; min-width: 0; }
|
||||||
|
.dash-root .tm-invite-actions { display: flex; gap: 8px; }
|
||||||
|
|
||||||
|
.dash-root .tm-form { display: flex; flex-direction: column; }
|
||||||
|
.dash-root .tm-roleprev { border-radius: 13px; border: 1px solid color-mix(in srgb, var(--rc) 30%, transparent); background: color-mix(in srgb, var(--rc) 8%, transparent); padding: 13px 15px; --rc: var(--blue); }
|
||||||
|
.dash-root .tm-roleprev-h { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 700; }
|
||||||
|
.dash-root .tm-roleprev-count { margin-left: auto; font-size: 11px; font-weight: 600; color: var(--muted); }
|
||||||
|
.dash-root .tm-roleprev p { font-size: 12px; color: var(--muted); margin: 7px 0 0; line-height: 1.5; }
|
||||||
|
.dash-root .tm-edit-hero { display: flex; align-items: center; gap: 14px; padding: 4px 0 16px; }
|
||||||
|
.dash-root .tm-confirm-text { font-size: 13px; color: var(--muted); line-height: 1.6; margin: 0; }
|
||||||
|
|
||||||
|
/* =========================================================== */
|
||||||
|
/* AI Assistant — Lynk AI */
|
||||||
|
/* =========================================================== */
|
||||||
|
.dash-root .ai-view { display: flex; flex-direction: column; gap: 16px; height: calc(100vh - 168px); min-height: 520px; }
|
||||||
|
.dash-root .ai-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
|
||||||
|
.dash-root .ai-head-l { display: flex; align-items: center; gap: 13px; }
|
||||||
|
.dash-root .ai-mark { width: 44px; height: 44px; border-radius: 14px; display: grid; place-items: center; flex: 0 0 auto; color: #fff; background: var(--grad-brand); box-shadow: 0 10px 26px -10px color-mix(in srgb, var(--orange) 80%, transparent); }
|
||||||
|
.dash-root .ai-eyebrow { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--faint); }
|
||||||
|
.dash-root .ai-head h1 { display: flex; align-items: center; gap: 9px; font-size: 21px; font-weight: 800; letter-spacing: -0.02em; }
|
||||||
|
.dash-root .ai-head-actions { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.dash-root .ai-model { display: inline-flex; align-items: center; gap: 7px; height: 32px; padding: 0 12px; border-radius: 99px; border: 1px solid var(--border-2); background: var(--panel-2); font-size: 12px; font-weight: 600; color: var(--muted); }
|
||||||
|
.dash-root .ai-model-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 25%, transparent); }
|
||||||
|
|
||||||
|
.dash-root .ai-stage { position: relative; display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }
|
||||||
|
.dash-root .ai-stage::before { content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
|
||||||
|
background-image: radial-gradient(rgba(255,255,255,0.045) 1px, transparent 1px); background-size: 22px 22px;
|
||||||
|
-webkit-mask-image: radial-gradient(60% 55% at 50% 0%, #000, transparent 75%); mask-image: radial-gradient(60% 55% at 50% 0%, #000, transparent 75%); }
|
||||||
|
.dash-root .ai-scroll { position: relative; z-index: 1; flex: 1; min-height: 0; overflow-y: auto; padding: 26px; }
|
||||||
|
.dash-root .ai-scroll::-webkit-scrollbar { width: 9px; }
|
||||||
|
.dash-root .ai-scroll::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 99px; }
|
||||||
|
|
||||||
|
.dash-root .ai-hero { max-width: 720px; margin: 0 auto; padding: 18px 0 6px; text-align: center; display: flex; flex-direction: column; align-items: center; }
|
||||||
|
.dash-root .ai-orb-wrap { position: relative; width: 84px; height: 84px; display: grid; place-items: center; }
|
||||||
|
.dash-root .ai-orb-ring { position: absolute; inset: 0; margin: auto; width: 84px; height: 84px; border-radius: 28px; border: 1.5px solid color-mix(in srgb, var(--orange) 50%, transparent); animation: ai-orbring 3s ease-out infinite; }
|
||||||
|
.dash-root .ai-orb-ring.d2 { animation-delay: 1s; }
|
||||||
|
.dash-root .ai-orb-ring.d3 { animation-delay: 2s; }
|
||||||
|
@keyframes ai-orbring { 0% { transform: scale(1); opacity: 0.65; } 100% { transform: scale(2.1); opacity: 0; } }
|
||||||
|
.dash-root .ai-hero-orb { position: relative; z-index: 1; width: 84px; height: 84px; border-radius: 26px; display: grid; place-items: center; color: #fff; background: rgba(253, 169, 19, 0.92); box-shadow: 0 18px 50px -14px color-mix(in srgb, var(--orange) 78%, transparent), 0 0 0 1px color-mix(in srgb, var(--orange) 40%, transparent); animation: ai-float 4s ease-in-out infinite; }
|
||||||
|
.dash-root .ai-hero h2 { font-size: 26px; font-weight: 800; letter-spacing: -0.03em; margin-top: 20px; }
|
||||||
|
.dash-root .ai-hero > p { font-size: 13.5px; color: var(--muted); line-height: 1.6; max-width: 480px; margin: 10px auto 0; }
|
||||||
|
.dash-root .ai-caps { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; width: 100%; margin-top: 28px; }
|
||||||
|
.dash-root .ai-cap { --rc: var(--orange); text-align: left; display: flex; flex-direction: column; gap: 5px; padding: 16px; border-radius: 16px; border: 1px solid var(--border); background: var(--panel-2); cursor: pointer; transition: 0.16s; }
|
||||||
|
.dash-root .ai-cap:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--rc) 50%, transparent); box-shadow: 0 18px 38px -22px color-mix(in srgb, var(--rc) 80%, #000); }
|
||||||
|
.dash-root .ai-cap-ic { width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center; color: var(--rc); background: color-mix(in srgb, var(--rc) 15%, transparent); margin-bottom: 4px; }
|
||||||
|
.dash-root .ai-cap-title { font-size: 13.5px; font-weight: 700; }
|
||||||
|
.dash-root .ai-cap-desc { font-size: 12px; color: var(--muted); line-height: 1.45; }
|
||||||
|
.dash-root .ai-suggest { width: 100%; margin-top: 26px; }
|
||||||
|
.dash-root .ai-suggest-lbl { display: block; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--faint); margin-bottom: 11px; }
|
||||||
|
.dash-root .ai-chips { display: flex; flex-wrap: wrap; gap: 9px; justify-content: center; }
|
||||||
|
.dash-root .ai-chip, .dash-root .ai-quick { display: inline-flex; align-items: center; gap: 7px; padding: 8px 13px; border-radius: 99px; border: 1px solid var(--border-2); background: var(--panel-2); color: var(--text-2); font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer; transition: 0.14s; }
|
||||||
|
.dash-root .ai-chip:hover, .dash-root .ai-quick:hover { border-color: var(--orange); color: var(--text); background: color-mix(in srgb, var(--orange) 10%, var(--panel-2)); }
|
||||||
|
.dash-root .ai-chip svg, .dash-root .ai-quick svg { color: var(--orange); flex: 0 0 auto; }
|
||||||
|
|
||||||
|
.dash-root .ai-thread { max-width: 780px; margin: 0 auto; display: flex; flex-direction: column; gap: 18px; }
|
||||||
|
.dash-root .ai-msg { display: flex; gap: 11px; align-items: flex-start; }
|
||||||
|
.dash-root .ai-msg.me { flex-direction: row-reverse; }
|
||||||
|
.dash-root .ai-msg-av { width: 32px; height: 32px; border-radius: 10px; flex: 0 0 auto; }
|
||||||
|
.dash-root .ai-bubble { max-width: 78%; padding: 12px 15px; border-radius: 16px; border: 1px solid var(--border); background: var(--panel-2); font-size: 13.5px; line-height: 1.6; }
|
||||||
|
.dash-root .ai-bubble p { margin: 0; }
|
||||||
|
.dash-root .ai-bubble p + p { margin-top: 5px; }
|
||||||
|
.dash-root .ai-msg.me .ai-bubble { background: var(--grad-brand); color: #fff; border-color: transparent; border-bottom-right-radius: 5px; font-weight: 500; }
|
||||||
|
.dash-root .ai-msg.ai .ai-bubble { border-bottom-left-radius: 5px; }
|
||||||
|
.dash-root .ai-msg-actions { display: flex; gap: 4px; margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--border); }
|
||||||
|
.dash-root .ai-msg-actions button { width: 26px; height: 26px; border-radius: 8px; border: 0; background: none; color: var(--faint); display: grid; place-items: center; cursor: pointer; transition: 0.14s; }
|
||||||
|
.dash-root .ai-msg-actions button:hover { background: var(--panel-3); color: var(--text); }
|
||||||
|
.dash-root .ai-typing { display: inline-flex; gap: 5px; align-items: center; }
|
||||||
|
.dash-root .ai-typing span { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); animation: ai-blink 1.2s infinite ease-in-out; }
|
||||||
|
.dash-root .ai-typing span:nth-child(2) { animation-delay: 0.18s; }
|
||||||
|
.dash-root .ai-typing span:nth-child(3) { animation-delay: 0.36s; }
|
||||||
|
|
||||||
|
.dash-root .ai-composer { border-top: 1px solid var(--border); padding: 14px 18px 12px; background: color-mix(in srgb, var(--panel-2) 40%, transparent); }
|
||||||
|
.dash-root .ai-quickrow { display: flex; gap: 8px; margin-bottom: 11px; overflow-x: auto; padding-bottom: 2px; }
|
||||||
|
.dash-root .ai-quickrow::-webkit-scrollbar { height: 0; }
|
||||||
|
.dash-root .ai-quick { white-space: nowrap; }
|
||||||
|
.dash-root .ai-inputbar { display: flex; align-items: flex-end; gap: 9px; padding: 7px 7px 7px 11px; border-radius: 16px; border: 1px solid var(--border-2); background: var(--panel); transition: 0.14s; }
|
||||||
|
.dash-root .ai-inputbar:focus-within { border-color: var(--orange); box-shadow: 0 0 0 3px var(--ring); }
|
||||||
|
.dash-root .ai-attach { flex: 0 0 auto; width: 36px; height: 36px; border-radius: 10px; border: 0; background: none; color: var(--muted); display: grid; place-items: center; cursor: pointer; transition: 0.14s; }
|
||||||
|
.dash-root .ai-attach:hover { background: var(--panel-3); color: var(--text); }
|
||||||
|
.dash-root .ai-textarea { flex: 1; resize: none; border: 0; background: none; outline: none; color: var(--text); font-family: inherit; font-size: 13.5px; line-height: 1.5; max-height: 132px; padding: 8px 0; }
|
||||||
|
.dash-root .ai-textarea::placeholder { color: var(--faint); }
|
||||||
|
.dash-root .ai-send { flex: 0 0 auto; width: 40px; height: 40px; border-radius: 12px; border: 0; cursor: pointer; display: grid; place-items: center; color: #fff; background: var(--grad-brand); box-shadow: 0 8px 20px -9px color-mix(in srgb, var(--orange) 80%, transparent); transition: 0.14s; }
|
||||||
|
.dash-root .ai-send:not(:disabled):hover { filter: brightness(1.08); transform: translateY(-1px); }
|
||||||
|
.dash-root .ai-send:disabled { opacity: 0.4; cursor: not-allowed; box-shadow: none; }
|
||||||
|
.dash-root .ai-foot { text-align: center; font-size: 11px; color: var(--faint); margin-top: 9px; }
|
||||||
|
|
||||||
|
@keyframes ai-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
|
||||||
|
@keyframes ai-blink { 0%, 80%, 100% { opacity: 0.25; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1); } }
|
||||||
|
|
||||||
|
@media (max-width: 920px) {
|
||||||
|
.dash-root .tm-stats { grid-template-columns: 1fr 1fr; }
|
||||||
|
.dash-root .tm-hero { padding: 22px; }
|
||||||
|
.dash-root .tm-hero-actions { width: 100%; }
|
||||||
|
.dash-root .tm-hero-actions .ds-btn { width: 100%; }
|
||||||
|
.dash-root .tm-head { display: none; }
|
||||||
|
.dash-root .tm-row { grid-template-columns: 1fr auto; row-gap: 10px; padding: 14px 16px; }
|
||||||
|
.dash-root .tm-member { grid-column: 1 / -1; }
|
||||||
|
.dash-root .tm-rolecell { grid-column: 1; }
|
||||||
|
.dash-root .tm-c-act { grid-column: 2; grid-row: 2; }
|
||||||
|
.dash-root .tm-statuscell { grid-column: 1 / -1; }
|
||||||
|
.dash-root .tm-c-num { grid-column: 1 / -1; text-align: left; }
|
||||||
|
.dash-root .tm-c-num::before { content: "Open deals: "; color: var(--faint); font-size: 12px; }
|
||||||
|
.dash-root .ai-caps { grid-template-columns: 1fr; }
|
||||||
|
.dash-root .ai-bubble { max-width: 86%; }
|
||||||
|
.dash-root .ai-view { height: calc(100vh - 150px); }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// AI Assistant — "Lynk AI", a creative conversational copilot.
|
||||||
|
// · Empty state : warm greeting, capability cards, prompt chips
|
||||||
|
// · Active chat : message thread with a typing indicator and a
|
||||||
|
// simulated, keyword-aware assistant reply
|
||||||
|
// · Composer : auto-suggesting prompt row + send / new chat
|
||||||
|
// All responses are canned + client-side so it demos with no API.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { user } from "./account-data";
|
||||||
|
import { Avatar, Btn, Icon, Pill, useToast } from "./ui";
|
||||||
|
|
||||||
|
type Msg = { id: number; from: "me" | "ai"; text: string };
|
||||||
|
|
||||||
|
const CAPABILITIES = [
|
||||||
|
{ icon: "leads", tone: "var(--orange)", title: "Qualify leads", desc: "Score and summarise new leads, draft the first outreach." },
|
||||||
|
{ icon: "estimates", tone: "var(--blue)", title: "Build estimates", desc: "Turn scope notes into a clean, line-itemed proposal." },
|
||||||
|
{ icon: "schedule", tone: "var(--green)", title: "Plan the week", desc: "Suggest a crew schedule around weather and priorities." },
|
||||||
|
{ icon: "leaderboard", tone: "var(--purple)", title: "Analyse pipeline", desc: "Spot stalled deals and where revenue is leaking." },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PROMPTS = [
|
||||||
|
{ icon: "leads", text: "Summarise my new leads from this week" },
|
||||||
|
{ icon: "estimates", text: "Draft an estimate for a 28-square asphalt reroof" },
|
||||||
|
{ icon: "storm", text: "Which territories were hit by the last storm?" },
|
||||||
|
{ icon: "pipeline", text: "Show deals stuck in the pipeline over 14 days" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let msgSeq = 1;
|
||||||
|
|
||||||
|
export function AiAssistant() {
|
||||||
|
const toast = useToast();
|
||||||
|
const [messages, setMessages] = useState<Msg[]>([]);
|
||||||
|
const [draft, setDraft] = useState("");
|
||||||
|
const [typing, setTyping] = useState(false);
|
||||||
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const started = messages.length > 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
|
||||||
|
}, [messages, typing]);
|
||||||
|
|
||||||
|
useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
|
||||||
|
|
||||||
|
function send(text: string) {
|
||||||
|
const body = text.trim();
|
||||||
|
if (!body || typing) return;
|
||||||
|
setMessages((m) => [...m, { id: msgSeq++, from: "me", text: body }]);
|
||||||
|
setDraft("");
|
||||||
|
setTyping(true);
|
||||||
|
timer.current = setTimeout(() => {
|
||||||
|
setMessages((m) => [...m, { id: msgSeq++, from: "ai", text: replyFor(body) }]);
|
||||||
|
setTyping(false);
|
||||||
|
}, 1100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
if (timer.current) clearTimeout(timer.current);
|
||||||
|
setMessages([]); setDraft(""); setTyping(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="view ai-view">
|
||||||
|
<div className="ai-head">
|
||||||
|
<div className="ai-head-l">
|
||||||
|
<span className="ai-mark"><Icon name="ai" size={20} /></span>
|
||||||
|
<div>
|
||||||
|
<div className="ai-eyebrow">LynkedUp Pro</div>
|
||||||
|
<h1>Lynk AI <Pill tone="orange">Beta</Pill></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ai-head-actions">
|
||||||
|
<span className="ai-model"><span className="ai-model-dot" /> Opus 4.8</span>
|
||||||
|
{started && <Btn variant="outline" size="sm" icon="plus" onClick={reset}>New chat</Btn>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ai-stage card card-pad-0">
|
||||||
|
<div className="ai-scroll" ref={scrollRef}>
|
||||||
|
{!started ? (
|
||||||
|
<div className="ai-hero">
|
||||||
|
<div className="ai-orb-wrap">
|
||||||
|
<span className="ai-orb-ring" aria-hidden />
|
||||||
|
<span className="ai-orb-ring d2" aria-hidden />
|
||||||
|
<span className="ai-orb-ring d3" aria-hidden />
|
||||||
|
<span className="ai-hero-orb"><Icon name="ai" size={34} /></span>
|
||||||
|
</div>
|
||||||
|
<h2>{greeting()}, {user.firstName}.</h2>
|
||||||
|
<p>I'm your roofing copilot. Ask me to summarise leads, draft estimates, plan crews or dig into your pipeline.</p>
|
||||||
|
|
||||||
|
<div className="ai-caps">
|
||||||
|
{CAPABILITIES.map((c) => (
|
||||||
|
<button className="ai-cap" key={c.title} style={{ ["--rc" as string]: c.tone }} onClick={() => send(`Help me ${c.title.toLowerCase()}`)}>
|
||||||
|
<span className="ai-cap-ic"><Icon name={c.icon} size={18} /></span>
|
||||||
|
<span className="ai-cap-title">{c.title}</span>
|
||||||
|
<span className="ai-cap-desc">{c.desc}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ai-suggest">
|
||||||
|
<span className="ai-suggest-lbl">Try asking</span>
|
||||||
|
<div className="ai-chips">
|
||||||
|
{PROMPTS.map((p) => (
|
||||||
|
<button className="ai-chip" key={p.text} onClick={() => send(p.text)}>
|
||||||
|
<Icon name={p.icon} size={14} /> {p.text}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ai-thread">
|
||||||
|
{messages.map((m) => (
|
||||||
|
<div className={`ai-msg ${m.from}`} key={m.id}>
|
||||||
|
{m.from === "ai"
|
||||||
|
? <span className="ai-msg-av ai-mark"><Icon name="ai" size={16} /></span>
|
||||||
|
: <Avatar initials={user.initials} gradient={user.avatarGradient} size={32} />}
|
||||||
|
<div className="ai-bubble">
|
||||||
|
{m.text.split("\n").map((line, i) => <p key={i}>{line}</p>)}
|
||||||
|
{m.from === "ai" && (
|
||||||
|
<div className="ai-msg-actions">
|
||||||
|
<button onClick={() => { navigator.clipboard?.writeText(m.text); toast.push({ tone: "success", title: "Copied to clipboard" }); }} aria-label="Copy"><Icon name="copy" size={13} /></button>
|
||||||
|
<button onClick={() => toast.push({ tone: "success", title: "Thanks for the feedback" })} aria-label="Good response"><Icon name="check" size={13} /></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{typing && (
|
||||||
|
<div className="ai-msg ai">
|
||||||
|
<span className="ai-msg-av ai-mark"><Icon name="ai" size={16} /></span>
|
||||||
|
<div className="ai-bubble ai-typing"><span /><span /><span /></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ai-composer">
|
||||||
|
{started && (
|
||||||
|
<div className="ai-quickrow">
|
||||||
|
{PROMPTS.slice(0, 3).map((p) => (
|
||||||
|
<button key={p.text} className="ai-quick" onClick={() => send(p.text)}><Icon name={p.icon} size={13} /> {p.text}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="ai-inputbar">
|
||||||
|
<button className="ai-attach" aria-label="Attach" onClick={() => toast.push({ tone: "info", title: "Attachments", desc: "File uploads land here soon." })}><Icon name="paperclip" size={18} /></button>
|
||||||
|
<textarea
|
||||||
|
className="ai-textarea"
|
||||||
|
rows={1}
|
||||||
|
placeholder="Ask Lynk AI anything about your roofing business…"
|
||||||
|
value={draft}
|
||||||
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
|
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(draft); } }}
|
||||||
|
/>
|
||||||
|
<button className="ai-send" aria-label="Send" disabled={!draft.trim() || typing} onClick={() => send(draft)}><Icon name="send" size={17} /></button>
|
||||||
|
</div>
|
||||||
|
<div className="ai-foot">Lynk AI can make mistakes. Verify estimates and figures before sending to owners.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
/* helpers */
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
function greeting(): string {
|
||||||
|
// Deterministic across renders to avoid hydration drift — anchored to a
|
||||||
|
// fixed demo time-of-day rather than the live clock.
|
||||||
|
return "Good evening";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lightweight keyword router so the demo feels responsive without an API.
|
||||||
|
function replyFor(q: string): string {
|
||||||
|
const t = q.toLowerCase();
|
||||||
|
if (t.includes("lead")) {
|
||||||
|
return "You have 6 new leads this week — 4 inbound from the storm landing page and 2 referrals.\nTop priority: Mark Reynolds (Sector 18) — full roof replacement, insurance claim already filed. I've drafted a first-touch SMS; want me to queue it?";
|
||||||
|
}
|
||||||
|
if (t.includes("estimate") || t.includes("proposal") || t.includes("reroep") || t.includes("reroof")) {
|
||||||
|
return "Here's a starting estimate for a 28-square asphalt reroof:\n• Tear-off & disposal — $3,920\n• Architectural shingles (28 sq) — $7,560\n• Underlayment, flashing & ridge vent — $2,180\n• Labour (3-day crew) — $5,400\nSubtotal $19,060 · 10% margin → $20,966. Want it itemised into a sendable proposal?";
|
||||||
|
}
|
||||||
|
if (t.includes("schedule") || t.includes("crew") || t.includes("week") || t.includes("plan")) {
|
||||||
|
return "Clear weather Tue–Thu, rain Friday. Suggested plan:\n• Tue: Crew A → 181 Sector 18 (reroof, day 1)\n• Wed: Crew A continues; Crew B → inspections in Pocket B\n• Thu: finish 181, start estimates follow-ups\nShould I block these in Team Schedule?";
|
||||||
|
}
|
||||||
|
if (t.includes("storm") || t.includes("territory") || t.includes("weather")) {
|
||||||
|
return "Last cell tracked through Sectors 14, 18 and Pocket B on 26 Jun with 1.5\" hail. 23 owners in those zones are good storm-canvass targets — 9 already have open claims. Want a canvass route?";
|
||||||
|
}
|
||||||
|
if (t.includes("pipeline") || t.includes("stuck") || t.includes("stall") || t.includes("deal")) {
|
||||||
|
return "3 deals have been idle over 14 days:\n• Bennett reroof — $21k, no contact in 18 days\n• Foster gutters — $4.2k, awaiting estimate\n• Turner inspection — booked, never closed\nThe Bennett deal is your biggest risk. Draft a re-engagement email?";
|
||||||
|
}
|
||||||
|
return "Got it. I can pull leads, build estimates, plan crew schedules, read storm/territory data and analyse your pipeline. Tell me which one to start with, or ask a specific question and I'll dig in.";
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import { ToastProvider, PageHead, Btn, Icon } from "./ui";
|
|||||||
import { Profile } from "./profile";
|
import { Profile } from "./profile";
|
||||||
import { Support } from "./support";
|
import { Support } from "./support";
|
||||||
import { Rules } from "./rules";
|
import { Rules } from "./rules";
|
||||||
|
import { AiAssistant } from "./ai-assistant";
|
||||||
|
import { TeamManagement } from "./team-management";
|
||||||
import "../../app/dashboard/dashboard.css";
|
import "../../app/dashboard/dashboard.css";
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
@@ -42,6 +44,8 @@ export function Dashboard() {
|
|||||||
{active === "profile" ? <Profile />
|
{active === "profile" ? <Profile />
|
||||||
: active === "support" ? <Support />
|
: active === "support" ? <Support />
|
||||||
: active === "rules" ? <Rules />
|
: active === "rules" ? <Rules />
|
||||||
|
: active === "ai" ? <AiAssistant />
|
||||||
|
: active === "team" ? <TeamManagement />
|
||||||
: <ComingSoon title={title} icon={item?.icon ?? "dashboard"} onGo={setActive} />}
|
: <ComingSoon title={title} icon={item?.icon ?? "dashboard"} onGo={setActive} />}
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Rules Checklist — the 1–13 business rules in one place.
|
// Rules Checklist — the 1–13 business rules in one place.
|
||||||
|
// A single filter bar + a uniform (brand-orange) card grid:
|
||||||
|
// each rule card has a ghost watermark number, an icon and an
|
||||||
|
// "enforced" footer. No per-category colours.
|
||||||
// (house 181 · OTP to registered mobile · KYC two-docs ·
|
// (house 181 · OTP to registered mobile · KYC two-docs ·
|
||||||
// password policy · ticket lifecycle · …)
|
// password policy · ticket lifecycle · …)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -10,6 +13,12 @@ import { useMemo, useState } from "react";
|
|||||||
import { rules } from "./account-data";
|
import { rules } from "./account-data";
|
||||||
import { Icon, PageHead, Pill, Segmented } from "./ui";
|
import { Icon, PageHead, Pill, Segmented } from "./ui";
|
||||||
|
|
||||||
|
// Each rule category maps to a glyph so cards read at a glance.
|
||||||
|
const TAG_ICON: Record<string, string> = {
|
||||||
|
Profile: "user", Security: "shield", KYC: "verify", Support: "chat",
|
||||||
|
Notifications: "bell", Privacy: "privacy",
|
||||||
|
};
|
||||||
|
|
||||||
export function Rules() {
|
export function Rules() {
|
||||||
const tags = useMemo(() => ["All", ...Array.from(new Set(rules.map((r) => r.tag)))], []);
|
const tags = useMemo(() => ["All", ...Array.from(new Set(rules.map((r) => r.tag)))], []);
|
||||||
const [tag, setTag] = useState("All");
|
const [tag, setTag] = useState("All");
|
||||||
@@ -29,14 +38,16 @@ export function Rules() {
|
|||||||
|
|
||||||
<div className="rules-grid view-body">
|
<div className="rules-grid view-body">
|
||||||
{list.map((r) => (
|
{list.map((r) => (
|
||||||
<div className="card rule-card" key={r.n} style={{ ["--accent" as string]: r.tagColor }}>
|
<article className="card rule-card" key={r.n}>
|
||||||
|
<span className="rule-watermark" aria-hidden>{String(r.n).padStart(2, "0")}</span>
|
||||||
<div className="rule-top">
|
<div className="rule-top">
|
||||||
<span className="rule-n">{String(r.n).padStart(2, "0")}</span>
|
<span className="rule-ic"><Icon name={TAG_ICON[r.tag] ?? "check-circle"} size={18} /></span>
|
||||||
<Pill tone="custom" style={{ color: r.tagColor, background: `color-mix(in srgb, ${r.tagColor} 16%, transparent)` }}>{r.tag}</Pill>
|
<Pill tone="muted">{r.tag}</Pill>
|
||||||
</div>
|
</div>
|
||||||
<div className="rule-title"><Icon name="check-circle" size={16} /> {r.title}</div>
|
<div className="rule-title">{r.title}</div>
|
||||||
<p className="rule-detail">{r.detail}</p>
|
<p className="rule-detail">{r.detail}</p>
|
||||||
</div>
|
<div className="rule-foot"><Icon name="shield-check" size={13} /> Enforced · Rule {r.n}</div>
|
||||||
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const NAV_GROUPS: NavGroup[] = [
|
|||||||
{
|
{
|
||||||
title: "Team",
|
title: "Team",
|
||||||
items: [
|
items: [
|
||||||
|
{ key: "team", label: "Team Management", icon: "team", subtitle: "Your crew, roles and permissions" },
|
||||||
{ key: "schedule", label: "Team Schedule", icon: "schedule" },
|
{ key: "schedule", label: "Team Schedule", icon: "schedule" },
|
||||||
{ key: "leaderboard", label: "Leaderboard", icon: "leaderboard" },
|
{ key: "leaderboard", label: "Leaderboard", icon: "leaderboard" },
|
||||||
{ key: "subtasks", label: "Subcontractor Tasks", icon: "subtasks" },
|
{ key: "subtasks", label: "Subcontractor Tasks", icon: "subtasks" },
|
||||||
@@ -41,10 +42,10 @@ export const NAV_GROUPS: NavGroup[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Team",
|
title: "Workspace AI",
|
||||||
items: [
|
items: [
|
||||||
{ key: "settings", label: "Org Settings", icon: "settings" },
|
{ key: "settings", label: "Org Settings", icon: "settings" },
|
||||||
{ key: "ai", label: "AI Assistant", icon: "ai" },
|
{ key: "ai", label: "AI Assistant", icon: "ai", subtitle: "Lynk AI — your roofing copilot" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -65,7 +66,6 @@ export function Sidebar({ active, onSelect }: { active: string; onSelect: (k: st
|
|||||||
<div className="dash-brand">
|
<div className="dash-brand">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img src="/image/logo.png" alt="LynkedUp Pro" />
|
<img src="/image/logo.png" alt="LynkedUp Pro" />
|
||||||
<span className="bk">LynkedUp Pro</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="dash-nav">
|
<nav className="dash-nav">
|
||||||
|
|||||||
@@ -86,10 +86,40 @@ function Overview({ onChannel, onSection }: { onChannel: (id: string) => void; o
|
|||||||
const mini = supportChannels.filter((c) => ["ticket", "callback", "email", "help"].includes(c.id));
|
const mini = supportChannels.filter((c) => ["ticket", "callback", "email", "help"].includes(c.id));
|
||||||
const recent = ticketSeed.slice(0, 4);
|
const recent = ticketSeed.slice(0, 4);
|
||||||
|
|
||||||
|
// Derived "live" metrics so the command-center ribbon stays in sync with data.
|
||||||
|
const openCount = ticketSeed.filter((t) => !["resolved", "closed"].includes(t.status)).length;
|
||||||
|
const avgRating = (supportTeam.reduce((s, a) => s + a.rating, 0) / supportTeam.length).toFixed(1);
|
||||||
|
const pulse = [
|
||||||
|
{ icon: "people", value: String(online.length), label: "Agents online", live: true },
|
||||||
|
{ icon: "clock", value: "< 2m", label: "Avg wait time" },
|
||||||
|
{ icon: "ticket", value: String(openCount), label: "Open tickets" },
|
||||||
|
{ icon: "star", value: avgRating, label: "Avg CSAT rating" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="support-ov">
|
||||||
|
{/* Live command-center ribbon */}
|
||||||
|
<div className="sup-status">
|
||||||
|
<span className="sup-status-l"><span className="sup-status-dot" /> All systems operational</span>
|
||||||
|
<span className="sup-status-r">Support · Live status</span>
|
||||||
|
</div>
|
||||||
|
<div className="sup-pulse">
|
||||||
|
{pulse.map((p) => (
|
||||||
|
<div className="sup-stat" key={p.label}>
|
||||||
|
<span className="sup-stat-ic"><Icon name={p.icon} size={17} /></span>
|
||||||
|
<div className="sup-stat-txt">
|
||||||
|
<b>{p.value}{p.icon === "star" && <Icon name="star" size={12} />}</b>
|
||||||
|
<span>{p.label}</span>
|
||||||
|
</div>
|
||||||
|
{p.live && <span className="sup-stat-live" />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="support-bento">
|
<div className="support-bento">
|
||||||
{/* Hero — live chat */}
|
{/* Hero — live chat */}
|
||||||
<button className="bento-tile bento-chat" onClick={() => onChannel("chat")}>
|
<button className="bento-tile bento-chat" onClick={() => onChannel("chat")}>
|
||||||
|
<span className="bento-chat-sig" aria-hidden><span /><span /><span /></span>
|
||||||
<div className="bento-chat-top">
|
<div className="bento-chat-top">
|
||||||
<span className="bento-chat-badge"><StatusDot status="online" /> Agents online now</span>
|
<span className="bento-chat-badge"><StatusDot status="online" /> Agents online now</span>
|
||||||
<span className="bento-chat-wait"><Icon name="clock" size={13} /> Avg wait < 2 min</span>
|
<span className="bento-chat-wait"><Icon name="clock" size={13} /> Avg wait < 2 min</span>
|
||||||
@@ -149,6 +179,7 @@ function Overview({ onChannel, onSection }: { onChannel: (id: string) => void; o
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
// ============================================================
|
||||||
|
// LynkedUp Pro — Team Management mock data.
|
||||||
|
// Members, roles and a permission matrix, modelled the way a
|
||||||
|
// CRM team module works: every member carries a role, and a
|
||||||
|
// role is a named bundle of permissions. All client-side so the
|
||||||
|
// screen is fully interactive without a backend.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
/* PERMISSIONS — grouped, the building blocks of a role */
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
export type Permission = { id: string; label: string; desc: string };
|
||||||
|
export type PermissionGroup = { id: string; title: string; icon: string; perms: Permission[] };
|
||||||
|
|
||||||
|
export const permissionGroups: PermissionGroup[] = [
|
||||||
|
{
|
||||||
|
id: "sales",
|
||||||
|
title: "Sales",
|
||||||
|
icon: "pipeline",
|
||||||
|
perms: [
|
||||||
|
{ id: "leads.manage", label: "Manage leads & contacts", desc: "Create, edit, assign and delete leads" },
|
||||||
|
{ id: "pipeline.manage", label: "Manage pipeline & deals", desc: "Move deals across stages and edit values" },
|
||||||
|
{ id: "estimates.create", label: "Create estimates & proposals", desc: "Build and send estimates to owners" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ops",
|
||||||
|
title: "Operations",
|
||||||
|
icon: "dispatch",
|
||||||
|
perms: [
|
||||||
|
{ id: "dispatch.manage", label: "Manage dispatch & schedule", desc: "Assign crews and schedule site visits" },
|
||||||
|
{ id: "billing.view", label: "View billing & invoices", desc: "See invoices, receipts and payment status" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "insights",
|
||||||
|
title: "Insights",
|
||||||
|
icon: "leaderboard",
|
||||||
|
perms: [
|
||||||
|
{ id: "reports.view", label: "View reports & analytics", desc: "Access dashboards and export reports" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "admin",
|
||||||
|
title: "Administration",
|
||||||
|
icon: "settings",
|
||||||
|
perms: [
|
||||||
|
{ id: "team.manage", label: "Manage team members", desc: "Invite, edit and deactivate members" },
|
||||||
|
{ id: "roles.manage", label: "Manage roles & permissions", desc: "Create roles and edit permission sets" },
|
||||||
|
{ id: "settings.manage", label: "Manage org settings", desc: "Billing plan, branding and integrations" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const allPermissionIds: string[] = permissionGroups.flatMap((g) => g.perms.map((p) => p.id));
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
/* ROLES — named permission bundles */
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
export type Role = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string; // token, e.g. var(--orange)
|
||||||
|
description: string;
|
||||||
|
system: boolean; // system roles can't be deleted
|
||||||
|
perms: string[]; // granted permission ids
|
||||||
|
};
|
||||||
|
|
||||||
|
export const roles: Role[] = [
|
||||||
|
{
|
||||||
|
id: "owner",
|
||||||
|
name: "Owner",
|
||||||
|
color: "var(--orange)",
|
||||||
|
description: "Full, unrestricted access. There is exactly one account owner.",
|
||||||
|
system: true,
|
||||||
|
perms: [...allPermissionIds],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "admin",
|
||||||
|
name: "Admin",
|
||||||
|
color: "var(--purple)",
|
||||||
|
description: "Runs the workspace — manages people, roles and settings.",
|
||||||
|
system: true,
|
||||||
|
perms: [...allPermissionIds],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "manager",
|
||||||
|
name: "Sales Manager",
|
||||||
|
color: "var(--blue)",
|
||||||
|
description: "Leads the sales floor and oversees the crew's pipeline.",
|
||||||
|
system: false,
|
||||||
|
perms: ["leads.manage", "pipeline.manage", "estimates.create", "dispatch.manage", "billing.view", "reports.view", "team.manage"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rep",
|
||||||
|
name: "Sales Rep",
|
||||||
|
color: "var(--green)",
|
||||||
|
description: "Works leads and moves deals through the pipeline.",
|
||||||
|
system: false,
|
||||||
|
perms: ["leads.manage", "pipeline.manage", "estimates.create"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dispatcher",
|
||||||
|
name: "Dispatcher",
|
||||||
|
color: "var(--cyan)",
|
||||||
|
description: "Schedules crews and coordinates site visits.",
|
||||||
|
system: false,
|
||||||
|
perms: ["dispatch.manage", "reports.view"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "estimator",
|
||||||
|
name: "Estimator",
|
||||||
|
color: "var(--yellow)",
|
||||||
|
description: "Prepares estimates and proposals for approved leads.",
|
||||||
|
system: false,
|
||||||
|
perms: ["leads.manage", "estimates.create"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "viewer",
|
||||||
|
name: "Viewer",
|
||||||
|
color: "var(--muted)",
|
||||||
|
description: "Read-only access to reports and billing — no edits.",
|
||||||
|
system: false,
|
||||||
|
perms: ["reports.view", "billing.view"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
/* MEMBERS */
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
export type MemberStatus = "active" | "away" | "offline";
|
||||||
|
export type Member = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
initials: string;
|
||||||
|
email: string;
|
||||||
|
title: string; // job title, free text
|
||||||
|
roleId: string;
|
||||||
|
gradient: string;
|
||||||
|
status: MemberStatus;
|
||||||
|
lastActive: string;
|
||||||
|
deals: number; // open deals owned
|
||||||
|
joined: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const members: Member[] = [
|
||||||
|
{ id: "u1", name: "James Carter", initials: "JC", email: "james.carter@lynkeduppro.com", title: "Founder & CEO", roleId: "owner", gradient: "linear-gradient(135deg,#fda913,#fd6d13)", status: "active", lastActive: "Active now", deals: 12, joined: "Mar 2021" },
|
||||||
|
{ id: "u2", name: "Emma Wilson", initials: "EW", email: "emma.wilson@lynkeduppro.com", title: "Head of Sales", roleId: "manager", gradient: "linear-gradient(135deg,#285ef0,#09b9c6)", status: "active", lastActive: "Active now", deals: 21, joined: "Aug 2021" },
|
||||||
|
{ id: "u3", name: "Liam Foster", initials: "LF", email: "liam.foster@lynkeduppro.com", title: "Senior Sales Rep", roleId: "rep", gradient: "linear-gradient(135deg,#9036e9,#285ef0)", status: "active", lastActive: "5 min ago", deals: 17, joined: "Jan 2022" },
|
||||||
|
{ id: "u4", name: "Sophie Turner", initials: "ST", email: "sophie.turner@lynkeduppro.com", title: "Account Executive", roleId: "rep", gradient: "linear-gradient(135deg,#14bc83,#09b9c6)", status: "away", lastActive: "32 min ago", deals: 9, joined: "May 2022" },
|
||||||
|
{ id: "u5", name: "Noah Mitchell", initials: "NM", email: "noah.mitchell@lynkeduppro.com", title: "Dispatch Coordinator", roleId: "dispatcher", gradient: "linear-gradient(135deg,#f0563f,#fda913)", status: "active", lastActive: "Active now", deals: 0, joined: "Sep 2022" },
|
||||||
|
{ id: "u6", name: "Olivia Bennett", initials: "OB", email: "olivia.bennett@lynkeduppro.com", title: "Operations Lead", roleId: "admin", gradient: "linear-gradient(135deg,#09b9c6,#285ef0)", status: "away", lastActive: "1 hour ago", deals: 3, joined: "Nov 2021" },
|
||||||
|
{ id: "u7", name: "Ethan Brooks", initials: "EB", email: "ethan.brooks@lynkeduppro.com", title: "Lead Estimator", roleId: "estimator", gradient: "linear-gradient(135deg,#ffd60a,#fda913)", status: "offline", lastActive: "Yesterday", deals: 6, joined: "Feb 2023" },
|
||||||
|
{ id: "u8", name: "Ava Reynolds", initials: "AR", email: "ava.reynolds@lynkeduppro.com", title: "Finance Analyst", roleId: "viewer", gradient: "linear-gradient(135deg,#9036e9,#f0563f)", status: "offline", lastActive: "2 days ago", deals: 0, joined: "Apr 2023" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
/* PENDING INVITES */
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
export type Invite = { id: string; email: string; roleId: string; invitedBy: string; sentAt: string };
|
||||||
|
|
||||||
|
export const pendingInvites: Invite[] = [
|
||||||
|
{ id: "inv1", email: "daniel.hughes@gmail.com", roleId: "rep", invitedBy: "Emma Wilson", sentAt: "2 days ago" },
|
||||||
|
{ id: "inv2", email: "grace.murphy@outlook.com", roleId: "estimator", invitedBy: "James Carter", sentAt: "5 days ago" },
|
||||||
|
];
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Team Management — a CRM people + roles module, redesigned as
|
||||||
|
// a creative "crew" experience.
|
||||||
|
// · Crew hero : avatar stack, live presence, headline stats
|
||||||
|
// · Members : people gallery (cards) ⇄ compact list, with
|
||||||
|
// role filter chips, search, inline role swap,
|
||||||
|
// deal-load meters and row actions
|
||||||
|
// · Roles : permission-coverage rings + live toggle matrix
|
||||||
|
// · Invites : envelope timeline with resend / revoke
|
||||||
|
// Everything is local state so the screen is fully clickable.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
Avatar, Btn, Field, Icon, Modal, Pill, StatusDot, Toggle, useToast,
|
||||||
|
} from "./ui";
|
||||||
|
import {
|
||||||
|
members as seedMembers, roles as seedRoles, pendingInvites as seedInvites,
|
||||||
|
permissionGroups, allPermissionIds, type Member, type Role, type Invite,
|
||||||
|
} from "./team-data";
|
||||||
|
|
||||||
|
const STATUS_LABEL: Record<Member["status"], string> = { active: "Active", away: "Away", offline: "Offline" };
|
||||||
|
const toDot = (s: Member["status"]) => (s === "offline" ? "offline" : s === "away" ? "away" : "online");
|
||||||
|
|
||||||
|
export function TeamManagement() {
|
||||||
|
const toast = useToast();
|
||||||
|
const [tab, setTab] = useState("members");
|
||||||
|
const [view, setView] = useState<"grid" | "list">("grid");
|
||||||
|
const [members, setMembers] = useState<Member[]>(seedMembers);
|
||||||
|
const [roles, setRoles] = useState<Role[]>(seedRoles);
|
||||||
|
const [invites, setInvites] = useState<Invite[]>(seedInvites);
|
||||||
|
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [roleFilter, setRoleFilter] = useState("all");
|
||||||
|
const [inviteOpen, setInviteOpen] = useState(false);
|
||||||
|
const [editing, setEditing] = useState<Member | null>(null);
|
||||||
|
const [confirmRemove, setConfirmRemove] = useState<Member | null>(null);
|
||||||
|
|
||||||
|
const roleById = useMemo(() => Object.fromEntries(roles.map((r) => [r.id, r])), [roles]);
|
||||||
|
const countByRole = useMemo(() => {
|
||||||
|
const m: Record<string, number> = {};
|
||||||
|
for (const mem of members) m[mem.roleId] = (m[mem.roleId] ?? 0) + 1;
|
||||||
|
return m;
|
||||||
|
}, [members]);
|
||||||
|
const maxDeals = useMemo(() => Math.max(1, ...members.map((m) => m.deals)), [members]);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = query.trim().toLowerCase();
|
||||||
|
return members.filter((m) => {
|
||||||
|
const matchRole = roleFilter === "all" || m.roleId === roleFilter;
|
||||||
|
const matchQ = !q || m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q) || m.title.toLowerCase().includes(q);
|
||||||
|
return matchRole && matchQ;
|
||||||
|
});
|
||||||
|
}, [members, query, roleFilter]);
|
||||||
|
|
||||||
|
const activeCount = members.filter((m) => m.status === "active").length;
|
||||||
|
const totalDeals = members.reduce((sum, m) => sum + m.deals, 0);
|
||||||
|
|
||||||
|
function changeRole(id: string, roleId: string) {
|
||||||
|
setMembers((list) => list.map((m) => (m.id === id ? { ...m, roleId } : m)));
|
||||||
|
const m = members.find((x) => x.id === id);
|
||||||
|
toast.push({ tone: "success", title: "Role updated", desc: `${m?.name ?? "Member"} is now ${roleById[roleId]?.name ?? roleId}.` });
|
||||||
|
}
|
||||||
|
function removeMember(m: Member) {
|
||||||
|
setMembers((list) => list.filter((x) => x.id !== m.id));
|
||||||
|
setConfirmRemove(null);
|
||||||
|
toast.push({ tone: "info", title: "Member removed", desc: `${m.name} no longer has access.` });
|
||||||
|
}
|
||||||
|
function togglePerm(roleId: string, permId: string) {
|
||||||
|
setRoles((list) => list.map((r) => {
|
||||||
|
if (r.id !== roleId) return r;
|
||||||
|
const has = r.perms.includes(permId);
|
||||||
|
return { ...r, perms: has ? r.perms.filter((p) => p !== permId) : [...r.perms, permId] };
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crew stack — show online folks first, capped to 6 with a +N bubble.
|
||||||
|
const stack = [...members].sort((a, b) => Number(b.status === "active") - Number(a.status === "active"));
|
||||||
|
const stackShown = stack.slice(0, 6);
|
||||||
|
const stackRest = members.length - stackShown.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="view tm">
|
||||||
|
{/* ---- Crew hero ---------------------------------------- */}
|
||||||
|
<header className="tm-hero">
|
||||||
|
<div className="tm-hero-orbs" aria-hidden />
|
||||||
|
<div className="tm-hero-grid" aria-hidden />
|
||||||
|
<div className="tm-hero-main">
|
||||||
|
<div className="tm-stack" aria-hidden>
|
||||||
|
{stackShown.map((m, i) => (
|
||||||
|
<span key={m.id} className="tm-stack-av" style={{ zIndex: stackShown.length - i }}>
|
||||||
|
<Avatar initials={m.initials} gradient={m.gradient} size={46} status={toDot(m.status)} />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{stackRest > 0 && <span className="tm-stack-more">+{stackRest}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="tm-hero-copy">
|
||||||
|
<div className="tm-hero-eyebrow"><Icon name="team" size={13} /> Your crew · LynkedUp Pro</div>
|
||||||
|
<h1>Team Management</h1>
|
||||||
|
<p>Manage your crew, assign roles and control exactly what each person can do.</p>
|
||||||
|
<div className="tm-hero-live">
|
||||||
|
<span className="tm-pulse" /><b>{activeCount}</b> online now
|
||||||
|
<span className="tm-hero-dot" /> {members.length} members
|
||||||
|
<span className="tm-hero-dot" /> {invites.length} invites pending
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="tm-hero-actions">
|
||||||
|
<Btn icon="plus" onClick={() => setInviteOpen(true)}>Invite member</Btn>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* ---- Metric strip ------------------------------------- */}
|
||||||
|
{/* <div className="tm-stats">
|
||||||
|
<StatCard icon="people" tone="var(--orange)" value={members.length} label="Total members" foot="across the workspace" />
|
||||||
|
<StatCard icon="check-circle" tone="var(--green)" value={activeCount} label="Active now" foot={`${Math.round((activeCount / members.length) * 100)}% of the crew online`} bar={activeCount / members.length} />
|
||||||
|
<StatCard icon="shield" tone="var(--purple)" value={roles.length} label="Roles defined" foot={`${roles.filter((r) => !r.system).length} custom`} />
|
||||||
|
<StatCard icon="pipeline" tone="var(--blue)" value={totalDeals} label="Open deals owned" foot="by the whole team" />
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* ---- Tabs --------------------------------------------- */}
|
||||||
|
<div className="tm-tabs" role="tablist">
|
||||||
|
{[
|
||||||
|
{ value: "members", label: "Members", icon: "people", badge: members.length },
|
||||||
|
{ value: "roles", label: "Roles & Permissions", icon: "shield", badge: roles.length },
|
||||||
|
{ value: "invites", label: "Invites", icon: "mail", badge: invites.length },
|
||||||
|
].map((t) => (
|
||||||
|
<button key={t.value} role="tab" aria-selected={tab === t.value} className={`tm-tab ${tab === t.value ? "active" : ""}`} onClick={() => setTab(t.value)}>
|
||||||
|
<Icon name={t.icon} size={16} /> <span>{t.label}</span>
|
||||||
|
<span className="tm-tab-badge">{t.badge}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ---- MEMBERS ------------------------------------------ */}
|
||||||
|
{tab === "members" && (
|
||||||
|
<div className="view-body">
|
||||||
|
<div className="tm-toolbar">
|
||||||
|
<div className="tm-search">
|
||||||
|
<Icon name="search" size={16} />
|
||||||
|
<input className="ds-input flush" placeholder="Search by name, email or title…" value={query} onChange={(e) => setQuery(e.target.value)} />
|
||||||
|
{query && <button className="tm-search-x" aria-label="Clear" onClick={() => setQuery("")}><Icon name="x" size={14} /></button>}
|
||||||
|
</div>
|
||||||
|
<div className="tm-viewtoggle" role="group" aria-label="Layout">
|
||||||
|
<button className={view === "grid" ? "on" : ""} aria-label="Gallery view" onClick={() => setView("grid")}><Icon name="subtasks" size={15} /></button>
|
||||||
|
<button className={view === "list" ? "on" : ""} aria-label="List view" onClick={() => setView("list")}><Icon name="leaderboard" size={15} /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Role filter chips */}
|
||||||
|
<div className="tm-chips">
|
||||||
|
<button className={`tm-chip ${roleFilter === "all" ? "on" : ""}`} onClick={() => setRoleFilter("all")}>
|
||||||
|
<span className="tm-chip-dot" style={{ background: "var(--text-2)" }} /> All <i>{members.length}</i>
|
||||||
|
</button>
|
||||||
|
{roles.filter((r) => (countByRole[r.id] ?? 0) > 0).map((r) => (
|
||||||
|
<button key={r.id} className={`tm-chip ${roleFilter === r.id ? "on" : ""}`} style={{ ["--rc" as string]: r.color }} onClick={() => setRoleFilter(r.id)}>
|
||||||
|
<span className="tm-chip-dot" style={{ background: r.color }} /> {r.name} <i>{countByRole[r.id] ?? 0}</i>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<div className="card tm-empty"><span className="tm-empty-ic"><Icon name="search" size={24} /></span><p>No members match your search.</p><Btn variant="soft" size="sm" onClick={() => { setQuery(""); setRoleFilter("all"); }}>Clear filters</Btn></div>
|
||||||
|
) : view === "grid" ? (
|
||||||
|
<div className="tm-gallery">
|
||||||
|
{filtered.map((m) => {
|
||||||
|
const role = roleById[m.roleId];
|
||||||
|
const isOwner = m.roleId === "owner";
|
||||||
|
return (
|
||||||
|
<article className="card tm-pcard" key={m.id} style={{ ["--rc" as string]: role?.color }}>
|
||||||
|
<span className="tm-pcard-bg" aria-hidden />
|
||||||
|
<div className="tm-pcard-top">
|
||||||
|
<span className="tm-pcard-av"><Avatar initials={m.initials} gradient={m.gradient} size={58} status={toDot(m.status)} /></span>
|
||||||
|
<RowMenu disabled={isOwner} onEdit={() => setEditing(m)} onRemove={() => setConfirmRemove(m)} />
|
||||||
|
</div>
|
||||||
|
<div className="tm-pcard-name">{m.name}{isOwner && <span className="tm-crown" title="Account owner"><Icon name="star" size={13} /></span>}</div>
|
||||||
|
<div className="tm-pcard-title">{m.title}</div>
|
||||||
|
<div className="tm-pcard-email"><Icon name="mail" size={12} /> {m.email}</div>
|
||||||
|
|
||||||
|
<div className="tm-pcard-role">
|
||||||
|
{isOwner ? (
|
||||||
|
<span className="tm-rolebadge" style={{ ["--rc" as string]: role?.color }}><Icon name="shield" size={13} /> {role?.name}</span>
|
||||||
|
) : (
|
||||||
|
<div className="tm-roleselect" style={{ ["--rc" as string]: role?.color }}>
|
||||||
|
<span className="tm-roledot" />
|
||||||
|
<select className="ds-select" value={m.roleId} onChange={(e) => changeRole(m.id, e.target.value)} aria-label={`Role for ${m.name}`}>
|
||||||
|
{roles.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tm-meter" title={`${m.deals} open deals`}>
|
||||||
|
<div className="tm-meter-h"><span>Deal load</span><b>{m.deals}</b></div>
|
||||||
|
<span className="tm-meter-track"><span className="tm-meter-fill" style={{ width: `${Math.round((m.deals / maxDeals) * 100)}%` }} /></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tm-pcard-foot">
|
||||||
|
<span className="tm-statuscell"><StatusDot status={toDot(m.status)} /> {STATUS_LABEL[m.status]}</span>
|
||||||
|
<span className="tm-lastactive">{m.lastActive}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="card card-pad-0 tm-table">
|
||||||
|
<div className="tm-row tm-head">
|
||||||
|
<span>Member</span><span>Role</span><span>Status</span><span className="tm-c-num">Open deals</span><span className="tm-c-act" />
|
||||||
|
</div>
|
||||||
|
{filtered.map((m) => {
|
||||||
|
const role = roleById[m.roleId];
|
||||||
|
const isOwner = m.roleId === "owner";
|
||||||
|
return (
|
||||||
|
<div className="tm-row" key={m.id}>
|
||||||
|
<div className="tm-member">
|
||||||
|
<Avatar initials={m.initials} gradient={m.gradient} size={40} status={toDot(m.status)} />
|
||||||
|
<div className="tm-member-meta">
|
||||||
|
<div className="tm-name">{m.name}{isOwner && <Pill tone="orange" style={{ marginLeft: 8 }}>You · Owner</Pill>}</div>
|
||||||
|
<div className="tm-sub">{m.title} · {m.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="tm-rolecell">
|
||||||
|
{isOwner ? (
|
||||||
|
<span className="tm-rolebadge" style={{ ["--rc" as string]: role?.color }}><Icon name="shield" size={13} /> {role?.name}</span>
|
||||||
|
) : (
|
||||||
|
<div className="tm-roleselect" style={{ ["--rc" as string]: role?.color }}>
|
||||||
|
<span className="tm-roledot" />
|
||||||
|
<select className="ds-select" value={m.roleId} onChange={(e) => changeRole(m.id, e.target.value)} aria-label={`Role for ${m.name}`}>
|
||||||
|
{roles.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="tm-statuscell">
|
||||||
|
<StatusDot status={toDot(m.status)} /><span>{STATUS_LABEL[m.status]}</span>
|
||||||
|
<span className="tm-lastactive">{m.lastActive}</span>
|
||||||
|
</div>
|
||||||
|
<div className="tm-c-num">{m.deals > 0 ? <b>{m.deals}</b> : <span className="tm-dash">—</span>}</div>
|
||||||
|
<div className="tm-c-act"><RowMenu disabled={isOwner} onEdit={() => setEditing(m)} onRemove={() => setConfirmRemove(m)} /></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ---- ROLES -------------------------------------------- */}
|
||||||
|
{tab === "roles" && (
|
||||||
|
<div className="view-body tm-roles">
|
||||||
|
{roles.map((r) => {
|
||||||
|
const pct = Math.round((r.perms.length / allPermissionIds.length) * 100);
|
||||||
|
return (
|
||||||
|
<div className="card tm-rolecard" key={r.id} style={{ ["--rc" as string]: r.color }}>
|
||||||
|
<div className="tm-rolecard-head">
|
||||||
|
<span className="tm-ring" style={{ ["--pct" as string]: pct }}>
|
||||||
|
<span className="tm-ring-in"><Icon name={r.id === "owner" ? "star" : r.system ? "shield-check" : "shield"} size={17} /></span>
|
||||||
|
</span>
|
||||||
|
<div className="tm-rolecard-title">
|
||||||
|
<div className="tm-rolecard-name">{r.name}{r.system && <Pill tone="muted" style={{ marginLeft: 8 }}>System</Pill>}</div>
|
||||||
|
<div className="tm-rolecard-sub">{countByRole[r.id] ?? 0} {(countByRole[r.id] ?? 0) === 1 ? "member" : "members"} · {r.perms.length}/{allPermissionIds.length} permissions</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="tm-rolecard-desc">{r.description}</p>
|
||||||
|
|
||||||
|
<div className="tm-permlist">
|
||||||
|
{permissionGroups.map((g) => (
|
||||||
|
<div className="tm-permgroup" key={g.id}>
|
||||||
|
<div className="tm-permgroup-h"><Icon name={g.icon} size={13} /> {g.title}</div>
|
||||||
|
{g.perms.map((p) => {
|
||||||
|
const granted = r.perms.includes(p.id);
|
||||||
|
const locked = r.id === "owner";
|
||||||
|
return (
|
||||||
|
<div className={`tm-perm ${granted ? "on" : ""}`} key={p.id}>
|
||||||
|
<div className="tm-perm-meta">
|
||||||
|
<span className="tm-perm-label">{p.label}</span>
|
||||||
|
<span className="tm-perm-desc">{p.desc}</span>
|
||||||
|
</div>
|
||||||
|
<Toggle checked={granted} disabled={locked} onChange={() => togglePerm(r.id, p.id)} label={p.label} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ---- INVITES ------------------------------------------ */}
|
||||||
|
{tab === "invites" && (
|
||||||
|
<div className="view-body card card-pad-0">
|
||||||
|
<div className="tm-invite-head">
|
||||||
|
<div>
|
||||||
|
<h3>Pending invitations</h3>
|
||||||
|
<p>People you've invited who haven't joined yet.</p>
|
||||||
|
</div>
|
||||||
|
<Btn icon="plus" size="sm" onClick={() => setInviteOpen(true)}>New invite</Btn>
|
||||||
|
</div>
|
||||||
|
{invites.length === 0 ? (
|
||||||
|
<div className="tm-empty"><span className="tm-empty-ic"><Icon name="mail" size={24} /></span><p>No pending invites. Everyone's on board.</p></div>
|
||||||
|
) : (
|
||||||
|
<div className="tm-invites">
|
||||||
|
{invites.map((inv) => {
|
||||||
|
const role = roleById[inv.roleId];
|
||||||
|
return (
|
||||||
|
<div className="tm-inviterow" key={inv.id}>
|
||||||
|
<span className="tm-invite-ic"><Icon name="mail" size={16} /></span>
|
||||||
|
<div className="tm-invite-meta">
|
||||||
|
<div className="tm-name">{inv.email}</div>
|
||||||
|
<div className="tm-sub">Invited by {inv.invitedBy} · {inv.sentAt}</div>
|
||||||
|
</div>
|
||||||
|
<span className="tm-rolebadge" style={{ ["--rc" as string]: role?.color }}><Icon name="shield" size={13} /> {role?.name}</span>
|
||||||
|
<div className="tm-invite-actions">
|
||||||
|
<Btn variant="soft" size="sm" icon="refresh" onClick={() => toast.push({ tone: "success", title: "Invite resent", desc: `A fresh link was sent to ${inv.email}.` })}>Resend</Btn>
|
||||||
|
<Btn variant="ghost" size="sm" icon="x" onClick={() => { setInvites((l) => l.filter((x) => x.id !== inv.id)); toast.push({ tone: "info", title: "Invite revoked" }); }}>Revoke</Btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<InviteModal
|
||||||
|
open={inviteOpen}
|
||||||
|
roles={roles}
|
||||||
|
onClose={() => setInviteOpen(false)}
|
||||||
|
onInvite={(email, roleId) => {
|
||||||
|
setInvites((l) => [{ id: `inv_${l.length + Date.now()}`, email, roleId, invitedBy: "James Carter", sentAt: "Just now" }, ...l]);
|
||||||
|
setInviteOpen(false);
|
||||||
|
setTab("invites");
|
||||||
|
toast.push({ tone: "success", title: "Invitation sent", desc: `${email} was invited as ${roleById[roleId]?.name}.` });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditMemberModal
|
||||||
|
member={editing}
|
||||||
|
roles={roles}
|
||||||
|
onClose={() => setEditing(null)}
|
||||||
|
onSave={(id, patch) => {
|
||||||
|
setMembers((list) => list.map((m) => (m.id === id ? { ...m, ...patch } : m)));
|
||||||
|
setEditing(null);
|
||||||
|
toast.push({ tone: "success", title: "Member updated" });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={!!confirmRemove}
|
||||||
|
onClose={() => setConfirmRemove(null)}
|
||||||
|
title="Remove member"
|
||||||
|
subtitle={confirmRemove ? `${confirmRemove.name} will lose access immediately.` : ""}
|
||||||
|
icon="alert"
|
||||||
|
size="sm"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Btn variant="ghost" onClick={() => setConfirmRemove(null)}>Cancel</Btn>
|
||||||
|
<Btn variant="danger" icon="trash" onClick={() => confirmRemove && removeMember(confirmRemove)}>Remove member</Btn>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p className="tm-confirm-text">Their open deals and assignments stay in the workspace and can be reassigned afterwards. This action can be undone by re-inviting them.</p>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
/* Stat card */
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
function StatCard({ icon, tone, value, label, foot, bar }: { icon: string; tone: string; value: number; label: string; foot?: string; bar?: number }) {
|
||||||
|
return (
|
||||||
|
<div className="card tm-stat" style={{ ["--rc" as string]: tone }}>
|
||||||
|
<div className="tm-stat-head">
|
||||||
|
<span className="tm-stat-ic"><Icon name={icon} size={20} /></span>
|
||||||
|
<div className="tm-stat-body">
|
||||||
|
<div className="tm-stat-val">{value}</div>
|
||||||
|
<div className="tm-stat-lbl">{label}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{bar != null && <span className="tm-stat-bar"><span style={{ width: `${Math.round(bar * 100)}%` }} /></span>}
|
||||||
|
{foot && <div className="tm-stat-foot"><span className="tm-stat-dot" />{foot}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
/* Row actions menu */
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
function RowMenu({ onEdit, onRemove, disabled }: { onEdit: () => void; onRemove: () => void; disabled?: boolean }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
if (disabled) return <span className="tm-menu-lock" title="The owner can't be edited"><Icon name="lock" size={15} /></span>;
|
||||||
|
return (
|
||||||
|
<div className="tm-menu">
|
||||||
|
<button className="ds-iconbtn" aria-label="Member actions" onClick={() => setOpen((o) => !o)}><Icon name="dots" size={18} /></button>
|
||||||
|
{open && (
|
||||||
|
<>
|
||||||
|
<div className="tm-menu-scrim" onClick={() => setOpen(false)} />
|
||||||
|
<div className="tm-menu-pop" role="menu">
|
||||||
|
<button onClick={() => { setOpen(false); onEdit(); }}><Icon name="edit" size={15} /> Edit member</button>
|
||||||
|
<button onClick={() => { setOpen(false); onEdit(); }}><Icon name="shield" size={15} /> Change role</button>
|
||||||
|
<div className="tm-menu-sep" />
|
||||||
|
<button className="danger" onClick={() => { setOpen(false); onRemove(); }}><Icon name="trash" size={15} /> Remove</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
/* Invite modal */
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
function InviteModal({ open, roles, onClose, onInvite }: { open: boolean; roles: Role[]; onClose: () => void; onInvite: (email: string, roleId: string) => void }) {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [roleId, setRoleId] = useState("rep");
|
||||||
|
const valid = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim());
|
||||||
|
const role = roles.find((r) => r.id === roleId);
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
if (!valid) return;
|
||||||
|
onInvite(email.trim(), roleId);
|
||||||
|
setEmail(""); setRoleId("rep");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
title="Invite a team member"
|
||||||
|
subtitle="They'll get an email with a secure link to set up their account."
|
||||||
|
icon="people"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Btn variant="ghost" onClick={onClose}>Cancel</Btn>
|
||||||
|
<Btn icon="send" disabled={!valid} onClick={submit}>Send invitation</Btn>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="tm-form">
|
||||||
|
<Field label="Work email" required hint={email && !valid ? undefined : "We'll send the invite here."} error={email && !valid ? "Enter a valid email address." : undefined}>
|
||||||
|
<input className="ds-input" type="email" placeholder="name@company.com" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Assign a role" required hint="Roles decide what this person can see and do.">
|
||||||
|
<select className="ds-select" value={roleId} onChange={(e) => setRoleId(e.target.value)}>
|
||||||
|
{roles.filter((r) => r.id !== "owner").map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
{role && (
|
||||||
|
<div className="tm-roleprev" style={{ ["--rc" as string]: role.color }}>
|
||||||
|
<div className="tm-roleprev-h"><span className="tm-roledot" /> {role.name}<span className="tm-roleprev-count">{role.perms.length} permissions</span></div>
|
||||||
|
<p>{role.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
/* Edit member modal */
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
function EditMemberModal({ member, roles, onClose, onSave }: { member: Member | null; roles: Role[]; onClose: () => void; onSave: (id: string, patch: Partial<Member>) => void }) {
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [roleId, setRoleId] = useState("rep");
|
||||||
|
|
||||||
|
// Sync local form when a different member is opened.
|
||||||
|
const key = member?.id ?? "";
|
||||||
|
useEffect(() => { if (member) { setTitle(member.title); setRoleId(member.roleId); } }, [key]); // eslint-disable-line react-hooks/exhaustive-deps, react-hooks/set-state-in-effect
|
||||||
|
|
||||||
|
if (!member) return null;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={!!member}
|
||||||
|
onClose={onClose}
|
||||||
|
title={`Edit ${member.name}`}
|
||||||
|
subtitle={member.email}
|
||||||
|
icon="edit"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Btn variant="ghost" onClick={onClose}>Cancel</Btn>
|
||||||
|
<Btn icon="check" onClick={() => onSave(member.id, { title, roleId })}>Save changes</Btn>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="tm-form">
|
||||||
|
<div className="tm-edit-hero">
|
||||||
|
<Avatar initials={member.initials} gradient={member.gradient} size={52} />
|
||||||
|
<div>
|
||||||
|
<div className="tm-name">{member.name}</div>
|
||||||
|
<div className="tm-sub">Joined {member.joined} · {member.deals} open deals</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Field label="Job title">
|
||||||
|
<input className="ds-input" value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Role" hint="Changing the role updates this member's permissions instantly.">
|
||||||
|
<select className="ds-select" value={roleId} onChange={(e) => setRoleId(e.target.value)}>
|
||||||
|
{roles.filter((r) => r.id !== "owner").map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
CheckCircle2, KeyRound, Pencil, Copy, RefreshCw, AlertTriangle,
|
CheckCircle2, KeyRound, Pencil, Copy, RefreshCw, AlertTriangle,
|
||||||
LayoutDashboard, Building2, FolderKanban, UserPlus, BadgeCheck, Filter,
|
LayoutDashboard, Building2, FolderKanban, UserPlus, BadgeCheck, Filter,
|
||||||
Truck, CloudLightning, Map as MapIcon, PenTool, Calculator, CalendarDays,
|
Truck, CloudLightning, Map as MapIcon, PenTool, Calculator, CalendarDays,
|
||||||
Trophy, ListChecks, Users, Settings, Sparkles, type LucideIcon,
|
Trophy, ListChecks, Users, Settings, Sparkles, MoreHorizontal,
|
||||||
|
UsersRound, type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
@@ -47,6 +48,7 @@ const ICONS: Record<string, LucideIcon> = {
|
|||||||
storm: CloudLightning, territory: MapIcon, procanvas: PenTool,
|
storm: CloudLightning, territory: MapIcon, procanvas: PenTool,
|
||||||
estimates: Calculator, schedule: CalendarDays, leaderboard: Trophy,
|
estimates: Calculator, schedule: CalendarDays, leaderboard: Trophy,
|
||||||
subtasks: ListChecks, people: Users, settings: Settings, ai: Sparkles,
|
subtasks: ListChecks, people: Users, settings: Settings, ai: Sparkles,
|
||||||
|
team: UsersRound, dots: MoreHorizontal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Icon({ name, size = 18, className, strokeWidth = 2 }: { name: string; size?: number; className?: string; strokeWidth?: number }) {
|
export function Icon({ name, size = 18, className, strokeWidth = 2 }: { name: string; size?: number; className?: string; strokeWidth?: number }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user