WIP jordan
This commit is contained in:
836
app/pages/policies/book.vue
Normal file
836
app/pages/policies/book.vue
Normal file
@@ -0,0 +1,836 @@
|
||||
<script setup lang="ts">
|
||||
import { MOCK_CUSTOMERS, fmtMoney, type MockPolicy } from '~/data/mock-customers'
|
||||
|
||||
definePageMeta({ ssr: false })
|
||||
usePageTitle('Book of Business')
|
||||
|
||||
const { items } = useEmissionsQueue()
|
||||
const inForce = computed(() => items.value.filter((x) => x.status === 'in_force'))
|
||||
|
||||
/* ── Flatten all mock policies with customer info ── */
|
||||
type FlatPolicy = MockPolicy & { customerName: string; customerId: string }
|
||||
|
||||
const allPolicies = computed<FlatPolicy[]>(() => {
|
||||
const rows: FlatPolicy[] = []
|
||||
for (const cust of MOCK_CUSTOMERS) {
|
||||
for (const pol of cust.policies) {
|
||||
rows.push({ ...pol, customerName: cust.name, customerId: cust.id })
|
||||
}
|
||||
}
|
||||
return rows
|
||||
})
|
||||
|
||||
/* ── KPIs ── */
|
||||
const totalGWP = computed(() => allPolicies.value.reduce((s, p) => s + p.premium, 0))
|
||||
const activePolicies = computed(() => allPolicies.value.filter((p) => p.status === 'Active').length)
|
||||
const retentionRate = computed(() => {
|
||||
const total = allPolicies.value.length
|
||||
const active = allPolicies.value.filter((p) => p.status === 'Active').length
|
||||
return total > 0 ? Math.round((active / total) * 100) : 0
|
||||
})
|
||||
const avgPremium = computed(() => {
|
||||
const count = allPolicies.value.length
|
||||
return count > 0 ? Math.round(totalGWP.value / count) : 0
|
||||
})
|
||||
|
||||
/* ── Line of business breakdown ── */
|
||||
type LineRow = { line: string; count: number; totalPremium: number; pctOfBook: number; icon: string }
|
||||
|
||||
const lineBreakdown = computed<LineRow[]>(() => {
|
||||
const map = new Map<string, { count: number; total: number; icon: string }>()
|
||||
for (const pol of allPolicies.value) {
|
||||
const existing = map.get(pol.line)
|
||||
if (existing) {
|
||||
existing.count++
|
||||
existing.total += pol.premium
|
||||
} else {
|
||||
map.set(pol.line, { count: 1, total: pol.premium, icon: lineIcon(pol.line) })
|
||||
}
|
||||
}
|
||||
const gwp = totalGWP.value || 1
|
||||
const rows: LineRow[] = []
|
||||
for (const [line, data] of map.entries()) {
|
||||
rows.push({
|
||||
line,
|
||||
count: data.count,
|
||||
totalPremium: data.total,
|
||||
pctOfBook: Math.round((data.total / gwp) * 100),
|
||||
icon: data.icon
|
||||
})
|
||||
}
|
||||
rows.sort((a, b) => b.totalPremium - a.totalPremium)
|
||||
return rows
|
||||
})
|
||||
|
||||
/* ── Top carriers ── */
|
||||
type CarrierRow = { carrier: string; count: number; gwp: number; pctShare: number }
|
||||
|
||||
const topCarriers = computed<CarrierRow[]>(() => {
|
||||
const map = new Map<string, { count: number; gwp: number }>()
|
||||
for (const pol of allPolicies.value) {
|
||||
const existing = map.get(pol.carrier)
|
||||
if (existing) {
|
||||
existing.count++
|
||||
existing.gwp += pol.premium
|
||||
} else {
|
||||
map.set(pol.carrier, { count: 1, gwp: pol.premium })
|
||||
}
|
||||
}
|
||||
const gwp = totalGWP.value || 1
|
||||
const rows: CarrierRow[] = []
|
||||
for (const [carrier, data] of map.entries()) {
|
||||
rows.push({
|
||||
carrier,
|
||||
count: data.count,
|
||||
gwp: data.gwp,
|
||||
pctShare: Math.round((data.gwp / gwp) * 100)
|
||||
})
|
||||
}
|
||||
rows.sort((a, b) => b.gwp - a.gwp)
|
||||
return rows
|
||||
})
|
||||
|
||||
/* ── Recent activity ── */
|
||||
type ActivityItem = { date: string; text: string; type: string; customerName: string }
|
||||
|
||||
const recentActivity = computed<ActivityItem[]>(() => {
|
||||
const events: ActivityItem[] = []
|
||||
for (const cust of MOCK_CUSTOMERS) {
|
||||
for (const evt of cust.activity) {
|
||||
if (evt.type === 'policy' || evt.type === 'renewal' || evt.type === 'claim') {
|
||||
events.push({ ...evt, customerName: cust.name })
|
||||
}
|
||||
}
|
||||
}
|
||||
const order = ['Today', 'Yesterday']
|
||||
events.sort((a, b) => {
|
||||
const ai = order.indexOf(a.date)
|
||||
const bi = order.indexOf(b.date)
|
||||
if (ai >= 0 && bi >= 0) return ai - bi
|
||||
if (ai >= 0) return -1
|
||||
if (bi >= 0) return 1
|
||||
return b.date.localeCompare(a.date)
|
||||
})
|
||||
return events.slice(0, 5)
|
||||
})
|
||||
|
||||
/* ── Helpers ── */
|
||||
function lineIcon(line: string) {
|
||||
switch (line) {
|
||||
case 'Auto': return 'i-heroicons-truck'
|
||||
case 'Health': return 'i-heroicons-heart'
|
||||
case 'Life': return 'i-heroicons-shield-check'
|
||||
case 'Home': return 'i-heroicons-home-modern'
|
||||
case 'Renter': return 'i-heroicons-home-modern'
|
||||
case 'Umbrella': return 'i-heroicons-shield-exclamation'
|
||||
default: return 'i-heroicons-document'
|
||||
}
|
||||
}
|
||||
|
||||
function lineColorClass(line: string) {
|
||||
switch (line) {
|
||||
case 'Auto': return 'bk-line-auto'
|
||||
case 'Health': return 'bk-line-health'
|
||||
case 'Life': return 'bk-line-life'
|
||||
case 'Home': return 'bk-line-home'
|
||||
case 'Renter': return 'bk-line-home'
|
||||
case 'Umbrella': return 'bk-line-umbrella'
|
||||
default: return 'bk-line-default'
|
||||
}
|
||||
}
|
||||
|
||||
function activityIcon(type: string) {
|
||||
switch (type) {
|
||||
case 'policy': return 'i-heroicons-document-check'
|
||||
case 'renewal': return 'i-heroicons-arrow-path'
|
||||
case 'claim': return 'i-heroicons-shield-exclamation'
|
||||
default: return 'i-heroicons-document'
|
||||
}
|
||||
}
|
||||
|
||||
function activityColor(type: string) {
|
||||
switch (type) {
|
||||
case 'policy': return 'background: rgba(1,105,111,0.08); color: #01696f;'
|
||||
case 'renewal': return 'background: rgba(245,158,11,0.08); color: #d97706;'
|
||||
case 'claim': return 'background: rgba(244,63,94,0.08); color: #e11d48;'
|
||||
default: return 'background: rgba(0,0,0,0.04); color: #8a8a86;'
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Tab state ── */
|
||||
const activeTab = ref<'overview' | 'carriers'>('overview')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bk-root mx-auto max-w-6xl space-y-6 pb-12">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Book of Business</h1>
|
||||
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
|
||||
Consolidated view of all policies, lines, and carriers
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<NuxtLink to="/policies">
|
||||
<button class="bk-btn-secondary">
|
||||
<UIcon name="i-heroicons-arrow-left" class="w-3.5 h-3.5" />
|
||||
All Policies
|
||||
</button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process note -->
|
||||
<div style="display: flex; align-items: flex-start; gap: 10px; padding: 14px 16px; border-radius: 12px; background: rgba(147,51,234,0.05); border: 1px solid rgba(147,51,234,0.12);">
|
||||
<UIcon name="i-heroicons-beaker" style="width: 16px; height: 16px; color: #9333ea; flex-shrink: 0; margin-top: 1px;" />
|
||||
<div>
|
||||
<p style="font-size: 13px; font-weight: 600; color: #7c3aed;">Cartera Global in development</p>
|
||||
<p style="font-size: 12px; color: #8b5cf6; margin-top: 2px; line-height: 1.5;">Portfolio views, carrier breakdowns, retention analytics, and book-level reporting are actively being defined. Layout and feature scope may change.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI strip -->
|
||||
<div class="bk-kpi-strip">
|
||||
<div class="bk-kpi-item">
|
||||
<span class="bk-kpi-label">Total Book GWP</span>
|
||||
<span class="bk-kpi-value">{{ fmtMoney(totalGWP) }}<span class="bk-kpi-suffix">/yr</span></span>
|
||||
</div>
|
||||
<div class="bk-kpi-divider" />
|
||||
<div class="bk-kpi-item">
|
||||
<span class="bk-kpi-label">Active Policies</span>
|
||||
<span class="bk-kpi-value">{{ activePolicies }}</span>
|
||||
</div>
|
||||
<div class="bk-kpi-divider" />
|
||||
<div class="bk-kpi-item">
|
||||
<span class="bk-kpi-label">Retention Rate</span>
|
||||
<span class="bk-kpi-value">{{ retentionRate }}%</span>
|
||||
</div>
|
||||
<div class="bk-kpi-divider" />
|
||||
<div class="bk-kpi-item">
|
||||
<span class="bk-kpi-label">Avg Premium</span>
|
||||
<span class="bk-kpi-value">{{ fmtMoney(avgPremium) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In-force from emissions queue (real data) -->
|
||||
<div v-if="inForce.length > 0" class="bk-card bk-card-flush">
|
||||
<div class="bk-card-head">
|
||||
<span>In-Force from Emissions</span>
|
||||
<span class="bk-card-head-count">{{ inForce.length }}</span>
|
||||
</div>
|
||||
<div class="bk-table-wrap">
|
||||
<table class="bk-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="bk-th">Customer</th>
|
||||
<th class="bk-th">Insurer</th>
|
||||
<th class="bk-th">Product</th>
|
||||
<th class="bk-th">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in inForce" :key="row.id" class="bk-row">
|
||||
<td class="bk-td">
|
||||
<span class="bk-text-primary">{{ row.customerLabel }}</span>
|
||||
</td>
|
||||
<td class="bk-td">
|
||||
<span class="bk-text-muted">{{ row.insurerSlug }}</span>
|
||||
</td>
|
||||
<td class="bk-td">
|
||||
<span class="bk-text-muted">{{ row.subRamoKey }} · {{ row.productLine }}</span>
|
||||
</td>
|
||||
<td class="bk-td">
|
||||
<span class="bk-status-badge bk-status-active">In force</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab toggle -->
|
||||
<div class="bk-tab-container">
|
||||
<button
|
||||
class="bk-tab"
|
||||
:class="{ 'bk-tab-active': activeTab === 'overview' }"
|
||||
@click="activeTab = 'overview'"
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
class="bk-tab"
|
||||
:class="{ 'bk-tab-active': activeTab === 'carriers' }"
|
||||
@click="activeTab = 'carriers'"
|
||||
>
|
||||
Carriers
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Overview tab -->
|
||||
<template v-if="activeTab === 'overview'">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Line of Business breakdown -->
|
||||
<div class="bk-card bk-card-flush">
|
||||
<div class="bk-card-head">
|
||||
<span>By Line of Business</span>
|
||||
<span class="bk-card-head-count">{{ lineBreakdown.length }} lines</span>
|
||||
</div>
|
||||
<div class="bk-table-wrap">
|
||||
<table class="bk-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="bk-th">Line</th>
|
||||
<th class="bk-th" style="text-align: center;">Policies</th>
|
||||
<th class="bk-th" style="text-align: right;">Premium</th>
|
||||
<th class="bk-th" style="text-align: right; width: 80px;">% of Book</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in lineBreakdown" :key="row.line" class="bk-row">
|
||||
<td class="bk-td">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="bk-line-icon-wrap" :class="lineColorClass(row.line)">
|
||||
<UIcon :name="row.icon" class="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<span class="bk-text-primary" style="font-weight: 600;">{{ row.line }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="bk-td" style="text-align: center;">
|
||||
<span class="bk-text-muted">{{ row.count }}</span>
|
||||
</td>
|
||||
<td class="bk-td" style="text-align: right;">
|
||||
<span class="bk-text-primary" style="font-weight: 600;">{{ fmtMoney(row.totalPremium) }}</span>
|
||||
</td>
|
||||
<td class="bk-td" style="text-align: right;">
|
||||
<div class="bk-pct-cell">
|
||||
<div class="bk-pct-bar">
|
||||
<div class="bk-pct-bar-fill" :style="{ width: row.pctOfBook + '%' }" />
|
||||
</div>
|
||||
<span class="bk-pct-label">{{ row.pctOfBook }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="bk-card bk-card-flush">
|
||||
<div class="bk-card-head">
|
||||
<span>Recent Activity</span>
|
||||
<span class="bk-card-head-count">Last 5</span>
|
||||
</div>
|
||||
<div class="bk-activity-list">
|
||||
<div v-for="(evt, i) in recentActivity" :key="i" class="bk-activity-item">
|
||||
<div class="bk-activity-icon" :style="activityColor(evt.type)">
|
||||
<UIcon :name="activityIcon(evt.type)" class="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<div class="bk-activity-body">
|
||||
<p class="bk-activity-text">{{ evt.text }}</p>
|
||||
<p class="bk-activity-meta">{{ evt.customerName }} · {{ evt.date }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="recentActivity.length === 0" class="bk-empty-small">
|
||||
<p>No recent policy activity</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Premium distribution visual -->
|
||||
<div class="bk-card bk-card-flush">
|
||||
<div class="bk-card-head">
|
||||
<span>Premium Distribution</span>
|
||||
</div>
|
||||
<div style="padding: 20px;">
|
||||
<div class="bk-dist-bar">
|
||||
<div
|
||||
v-for="row in lineBreakdown"
|
||||
:key="row.line"
|
||||
class="bk-dist-segment"
|
||||
:class="lineColorClass(row.line)"
|
||||
:style="{ width: row.pctOfBook + '%' }"
|
||||
:title="`${row.line}: ${row.pctOfBook}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="bk-dist-legend">
|
||||
<div v-for="row in lineBreakdown" :key="row.line" class="bk-dist-legend-item">
|
||||
<span class="bk-dist-dot" :class="lineColorClass(row.line)" />
|
||||
<span class="bk-dist-legend-label">{{ row.line }}</span>
|
||||
<span class="bk-dist-legend-pct">{{ row.pctOfBook }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Carriers tab -->
|
||||
<template v-if="activeTab === 'carriers'">
|
||||
<div class="bk-card bk-card-flush">
|
||||
<div class="bk-card-head">
|
||||
<span>Top Carriers</span>
|
||||
<span class="bk-card-head-count">{{ topCarriers.length }} carriers</span>
|
||||
</div>
|
||||
<div class="bk-table-wrap">
|
||||
<table class="bk-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="bk-th" style="width: 36px;">#</th>
|
||||
<th class="bk-th">Carrier</th>
|
||||
<th class="bk-th" style="text-align: center;">Policies</th>
|
||||
<th class="bk-th" style="text-align: right;">GWP</th>
|
||||
<th class="bk-th" style="text-align: right; width: 100px;">Share</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in topCarriers" :key="row.carrier" class="bk-row">
|
||||
<td class="bk-td">
|
||||
<span class="bk-rank">{{ idx + 1 }}</span>
|
||||
</td>
|
||||
<td class="bk-td">
|
||||
<span class="bk-text-primary" style="font-weight: 600;">{{ row.carrier }}</span>
|
||||
</td>
|
||||
<td class="bk-td" style="text-align: center;">
|
||||
<span class="bk-text-muted">{{ row.count }}</span>
|
||||
</td>
|
||||
<td class="bk-td" style="text-align: right;">
|
||||
<span class="bk-text-primary" style="font-weight: 600;">{{ fmtMoney(row.gwp) }}</span>
|
||||
</td>
|
||||
<td class="bk-td" style="text-align: right;">
|
||||
<div class="bk-pct-cell">
|
||||
<div class="bk-pct-bar">
|
||||
<div class="bk-pct-bar-fill" :style="{ width: row.pctShare + '%' }" />
|
||||
</div>
|
||||
<span class="bk-pct-label">{{ row.pctShare }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Carrier premium breakdown -->
|
||||
<div class="bk-card bk-card-flush">
|
||||
<div class="bk-card-head">
|
||||
<span>Carrier Premium Share</span>
|
||||
</div>
|
||||
<div style="padding: 20px;">
|
||||
<div class="bk-carrier-bars">
|
||||
<div v-for="row in topCarriers" :key="row.carrier" class="bk-carrier-bar-row">
|
||||
<span class="bk-carrier-bar-label">{{ row.carrier }}</span>
|
||||
<div class="bk-carrier-bar-track">
|
||||
<div class="bk-carrier-bar-fill" :style="{ width: row.pctShare + '%' }" />
|
||||
</div>
|
||||
<span class="bk-carrier-bar-value">{{ fmtMoney(row.gwp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* =====================================================================
|
||||
BOOK OF BUSINESS — DESIGN SYSTEM (scoped, bk- prefix)
|
||||
===================================================================== */
|
||||
|
||||
.bk-root {
|
||||
--bk-brand: #01696f;
|
||||
--bk-brand-soft: rgba(1, 105, 111, 0.06);
|
||||
--bk-border: rgba(0, 0, 0, 0.06);
|
||||
--bk-muted: #8a8a86;
|
||||
}
|
||||
|
||||
|
||||
/* ---- Card system ---- */
|
||||
.bk-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.bk-card-flush {
|
||||
overflow: hidden;
|
||||
}
|
||||
.bk-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
.bk-card-head-count {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #8a8a86;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* ---- KPI strip ---- */
|
||||
.bk-kpi-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
padding: 16px 0;
|
||||
}
|
||||
.bk-kpi-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.bk-kpi-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #8a8a86;
|
||||
}
|
||||
.bk-kpi-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.bk-kpi-suffix {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #8a8a86;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.bk-kpi-divider {
|
||||
width: 1px;
|
||||
height: 36px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---- Buttons ---- */
|
||||
.bk-btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
.bk-btn-secondary:hover {
|
||||
border-color: rgba(1, 105, 111, 0.2);
|
||||
color: #01696f;
|
||||
}
|
||||
|
||||
/* ---- Tab toggle ---- */
|
||||
.bk-tab-container {
|
||||
display: inline-flex;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
}
|
||||
.bk-tab {
|
||||
padding: 6px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #8a8a86;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
.bk-tab:hover {
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
.bk-tab-active {
|
||||
background: #ffffff;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ---- Table ---- */
|
||||
.bk-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.bk-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
.bk-th {
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #8a8a86;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bk-td {
|
||||
padding: 12px 16px;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.bk-row {
|
||||
transition: background 120ms ease;
|
||||
}
|
||||
.bk-row:hover {
|
||||
background: rgba(0, 0, 0, 0.015);
|
||||
}
|
||||
.bk-row:last-child .bk-td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* ---- Text helpers ---- */
|
||||
.bk-text-primary {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
.bk-text-muted {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted, #5c5650);
|
||||
}
|
||||
|
||||
/* ---- Status badge ---- */
|
||||
.bk-status-badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bk-status-active { background: rgba(16, 185, 129, 0.1); color: #059669; }
|
||||
|
||||
/* ---- Line icon wrap ---- */
|
||||
.bk-line-icon-wrap {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---- Line colors (used for icons, dist segments, dots) ---- */
|
||||
.bk-line-auto { background: rgba(59, 130, 246, 0.08); color: #2563eb; }
|
||||
.bk-line-health { background: rgba(236, 72, 153, 0.08); color: #db2777; }
|
||||
.bk-line-life { background: rgba(16, 185, 129, 0.08); color: #059669; }
|
||||
.bk-line-home { background: rgba(245, 158, 11, 0.08); color: #d97706; }
|
||||
.bk-line-umbrella { background: rgba(139, 92, 246, 0.08); color: #7c3aed; }
|
||||
.bk-line-default { background: rgba(0, 0, 0, 0.04); color: #8a8a86; }
|
||||
|
||||
/* ---- Percentage cell ---- */
|
||||
.bk-pct-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.bk-pct-bar {
|
||||
width: 48px;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bk-pct-bar-fill {
|
||||
height: 100%;
|
||||
background: #01696f;
|
||||
border-radius: 2px;
|
||||
transition: width 300ms ease;
|
||||
}
|
||||
.bk-pct-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #8a8a86;
|
||||
min-width: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ---- Rank badge ---- */
|
||||
.bk-rank {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #8a8a86;
|
||||
}
|
||||
|
||||
/* ---- Distribution bar ---- */
|
||||
.bk-dist-bar {
|
||||
display: flex;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
gap: 2px;
|
||||
}
|
||||
.bk-dist-segment {
|
||||
height: 100%;
|
||||
min-width: 4px;
|
||||
border-radius: 3px;
|
||||
transition: width 300ms ease;
|
||||
}
|
||||
/* Reuse line color classes for segment backgrounds */
|
||||
.bk-dist-segment.bk-line-auto { background: #3b82f6; }
|
||||
.bk-dist-segment.bk-line-health { background: #ec4899; }
|
||||
.bk-dist-segment.bk-line-life { background: #10b981; }
|
||||
.bk-dist-segment.bk-line-home { background: #f59e0b; }
|
||||
.bk-dist-segment.bk-line-umbrella { background: #8b5cf6; }
|
||||
.bk-dist-segment.bk-line-default { background: #8a8a86; }
|
||||
|
||||
.bk-dist-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.bk-dist-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.bk-dist-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Dot colors match segment colors */
|
||||
.bk-dist-dot.bk-line-auto { background: #3b82f6; }
|
||||
.bk-dist-dot.bk-line-health { background: #ec4899; }
|
||||
.bk-dist-dot.bk-line-life { background: #10b981; }
|
||||
.bk-dist-dot.bk-line-home { background: #f59e0b; }
|
||||
.bk-dist-dot.bk-line-umbrella { background: #8b5cf6; }
|
||||
.bk-dist-dot.bk-line-default { background: #8a8a86; }
|
||||
|
||||
.bk-dist-legend-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
.bk-dist-legend-pct {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #8a8a86;
|
||||
}
|
||||
|
||||
/* ---- Carrier horizontal bars ---- */
|
||||
.bk-carrier-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.bk-carrier-bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.bk-carrier-bar-label {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
text-align: right;
|
||||
}
|
||||
.bk-carrier-bar-track {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bk-carrier-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #01696f, #018f97);
|
||||
border-radius: 4px;
|
||||
transition: width 300ms ease;
|
||||
}
|
||||
.bk-carrier-bar-value {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #5c5650);
|
||||
}
|
||||
|
||||
/* ---- Activity list ---- */
|
||||
.bk-activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.bk-activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
||||
transition: background 120ms ease;
|
||||
}
|
||||
.bk-activity-item:hover {
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
}
|
||||
.bk-activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.bk-activity-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bk-activity-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.bk-activity-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.bk-activity-meta {
|
||||
font-size: 11px;
|
||||
color: #8a8a86;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ---- Empty small ---- */
|
||||
.bk-empty-small {
|
||||
padding: 32px 20px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #8a8a86;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user