ui improvement

This commit is contained in:
2026-09-17 17:57:21 +05:30
parent 2cd048e6f0
commit 115a06a02c
79 changed files with 3574 additions and 2186 deletions

View File

@@ -63,40 +63,84 @@
.kpi-strip { grid-auto-columns: minmax(0, 1fr); overflow-x: visible; }
}
/* Icon beside the figure, nothing beneath it. The card used to carry a trend
row and a sparkline; with those gone it is a label and a number, and the
layout should not still be shaped around what it no longer holds. */
.kpi {
border: 1px solid var(--color-line); border-radius: 16px;
background: var(--color-surface); padding: 16px;
display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 12px; align-items: center;
}
.kpi-icon {
width: 40px; height: 40px; border-radius: 12px; display: grid; place-items: center;
background: var(--color-brand-tint); color: var(--color-brand);
}
.kpi-head { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kpi-note { font: 400 11.5px/1.35 var(--font-sans); color: var(--color-ink-4); }
/* Two lines rather than an ellipsis. With Nearle Buddy open each card is about
195px wide, and "Cancelled Orders" truncated to "Cance..." is a label that
has stopped doing its job. The min-height keeps all five value rows level
whether the label took one line or two. */
.kpi-label { font: 500 12.5px/1.25 var(--font-sans); color: var(--color-ink-3); }
.kpi-value { font: 600 22px/1.15 var(--font-sans); color: var(--color-ink-1); font-variant-numeric: tabular-nums; }
/* The Console's own KPI tile used to live here — `.kpi`, `.kpi-icon`,
`.kpi-label`, `.kpi-value`, `.kpi-note` — a second implementation of the
same object that `components/KpiCard.tsx` renders, with a different icon
radius, a different value size and a different gap. Both appeared on the
same screen. The component won, this strip now renders it, and the classes
are gone rather than left as a near-copy for someone to edit by mistake. */
/* ── Panels ─────────────────────────────────────────────────────────────── */
/* ── Panels ─────────────────────────────────────────────────────────────────
The Console's panels are the SAME object as `components/Panel.tsx` — a
framed card with a header band, a body and a footer band. They stay as CSS
classes rather than becoming that component because these six sections each
lay their own body out (a chart, a donut, a map, a list of alerts) and have
no table or pager to hand it; the component's job is the table case.
What matters is that they are the same DESIGN. They were not: this file had
a white header with a 15.5px title and a sub-line, the component has a
tinted band with an 11px eyebrow, and the two sat on the same screen. The
band and the type below are now the component's, restated here.
The panel no longer pads itself — the bands and the body pad separately, or
a header band cannot reach the panel's edges. */
.panel {
border: 1px solid var(--color-line); border-radius: 16px;
background: var(--color-surface); padding: 18px;
display: flex; flex-direction: column; gap: 14px; min-width: 0;
border: var(--card-border); border-radius: var(--card-radius);
background: var(--card-bg); box-shadow: var(--card-shadow);
display: flex; flex-direction: column; min-width: 0;
/* The clip belongs on the element that owns the radius, or the header
band's tint squares off the top two corners. */
overflow: hidden;
}
/* The body. Every direct child of `.panel` that is not a band is content, so
the padding and the stack live on a wrapper the sections already render. */
.panel > :not(.panel-head):not(.panel-foot) {
padding: 16px 18px;
}
/* Consecutive content blocks should not double their gutter. */
.panel > :not(.panel-head):not(.panel-foot) ~ :not(.panel-head):not(.panel-foot) {
padding-top: 0;
}
.panel-head {
display: flex; justify-content: space-between; align-items: center;
gap: 14px; flex-wrap: wrap;
min-height: 48px; padding: 8px 18px;
background: var(--color-surface-subtle);
border-bottom: var(--card-border);
}
/* An eyebrow, matching the table panels: 11px caps. A 15.5px heading inside a
card competes with the page's own title above it. */
.panel-title {
margin: 0;
font: 600 11px/1.2 var(--font-sans);
letter-spacing: 0.08em; text-transform: uppercase;
color: var(--color-ink-3);
}
/* Title and sub-line sit on ONE row, the way the table panel's title and count
do. Every section wraps the pair in a plain `div`, which stacks them by
default — and a stacked pair is the two-line header this band replaced. */
.panel-head > div:first-child {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
min-width: 0;
}
.panel-sub {
margin: 0;
font: 400 12px/1.4 var(--font-sans);
color: var(--color-ink-4);
}
.panel-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
.panel-title { margin: 0; font: 600 15.5px/1.25 var(--font-sans); color: var(--color-ink-1); }
.panel-sub { margin: 3px 0 0; font: 400 12.5px/1.4 var(--font-sans); color: var(--color-ink-3); }
.panel-foot {
display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;
font: 400 12.5px/1 var(--font-sans); color: var(--color-ink-3);
padding: 8px 18px;
background: var(--color-surface-subtle);
border-top: var(--card-border);
font: 400 12px/1.5 var(--font-sans); color: var(--color-ink-3);
}
.panel-empty {
display: flex; flex-direction: column; gap: 5px; padding: 28px 20px; text-align: center;
@@ -198,8 +242,8 @@
.share {
display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
padding: 10px 14px;
border: 1px solid var(--color-line); border-radius: 12px;
background: var(--color-surface-subtle);
border: var(--card-border); border-radius: var(--card-radius-sm);
background: var(--card-bg-subtle);
}
.share .donut-key { align-self: center; }
.share-label { font: 400 12.5px/1 var(--font-sans); color: var(--color-ink-3); }
@@ -236,8 +280,8 @@
.quick-list { display: flex; flex-direction: column; gap: 8px; }
.quick {
display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 12px; align-items: center;
padding: 11px 12px; border: 1px solid var(--color-line); border-radius: 12px;
text-decoration: none; background: var(--color-surface);
padding: 11px 12px; border: var(--card-border); border-radius: var(--card-radius-sm);
text-decoration: none; background: var(--card-bg);
}
.quick-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.quick-note { font: 400 11.5px/1.3 var(--font-sans); color: var(--color-ink-3); }
@@ -277,13 +321,8 @@
.chip[data-tone="healthy"][data-active="true"] { background: #1c6b47; border-color: #1c6b47; }
.chip:focus-visible { outline: 2px solid var(--color-brand); outline-offset: 2px; }
.search {
display: inline-flex; align-items: center; gap: 8px; padding: 0 11px; height: 32px;
min-width: 220px; border: 1px solid var(--color-line); border-radius: 10px;
background: var(--color-surface); color: var(--color-ink-4);
}
.search input { border: 0; outline: 0; background: none; flex: 1; min-width: 0; font: 400 13px/1 var(--font-sans); color: var(--color-ink-1); }
.search:focus-within { border-color: var(--color-brand); }
/* `.search` was here — the Console and Counters each drew a label-wrapping-an-
input version of the search box. Both use `components/SearchInput` now. */
/* ── Segmented control ──────────────────────────────────────────────────── */
.seg { display: inline-flex; padding: 3px; gap: 2px; border: 1px solid var(--color-line); border-radius: 10px; background: var(--color-surface-subtle); }
@@ -303,7 +342,7 @@
.attention-card {
display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; gap: 12px;
align-items: center; padding: 13px 14px; border: 1px solid var(--color-line); border-radius: 13px;
align-items: center; padding: 13px 14px; border: var(--card-border); border-radius: var(--card-radius);
}
.attention-card[data-tone="critical"] { background: #fdefee; border-color: #f2cbc7; }
.attention-card[data-tone="attention"] { background: #fdf6e8; border-color: #eeddb4; }
@@ -324,8 +363,8 @@
/* ── Loading ────────────────────────────────────────────────────────────── */
@keyframes sk-pulse { 0%, 100% { opacity: .55; } 50% { opacity: .9; } }
.sk-card, .sk-chart {
border: 1px solid var(--color-line); border-radius: 16px;
background: var(--color-surface-subtle); animation: sk-pulse 1.3s ease-in-out infinite;
border: var(--card-border); border-radius: var(--card-radius);
background: var(--card-bg-subtle); animation: sk-pulse 1.3s ease-in-out infinite;
}
.sk-card { height: 104px; }
.sk-chart { height: 300px; }

View File

@@ -29,7 +29,7 @@ export interface BranchOverviewProps {
export function BranchOverview({ rows, onSelect }: BranchOverviewProps) {
if (rows.length === 0) {
return (
<Card padding={3} elevation="low">
<Card padding={3}>
<Text type="body" size="sm" color="secondary">
No branches are set up yet.
</Text>
@@ -42,7 +42,7 @@ export function BranchOverview({ rows, onSelect }: BranchOverviewProps) {
);
return (
<Card padding={0} elevation="low">
<Card padding={0}>
<div className="table-scroll">
<table className="branch-table">
<thead>

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import { Ban, CloudUpload, ListChecks, Receipt, ShoppingCart } from 'lucide-react';
import { KpiCard } from '@/components/KpiCard';
import { count, money } from '@/features/store-admin/format';
import type { ConsoleTotals } from '../consoleModel';
@@ -39,15 +39,15 @@ export function KpiStrip({ totals, isLoading }: KpiStripProps) {
return (
<div className="kpi-strip">
<Kpi icon={<ShoppingCart size={17} />} label="Online Sales" value={money(totals.onlineRevenue)} />
<Kpi icon={<Receipt size={17} />} label="Counter Sales" value={money(totals.counterRevenue)} />
<Kpi
<KpiCard icon={<ShoppingCart size={17} />} label="Online Sales" value={money(totals.onlineRevenue)} />
<KpiCard icon={<Receipt size={17} />} label="Counter Sales" value={money(totals.counterRevenue)} />
<KpiCard
icon={<ListChecks size={17} />}
label="Total Orders"
value={count(totals.totalOrders)}
note={`${count(totals.onlineOrders)} app · ${count(totals.counterBills)} counter`}
/>
<Kpi
<KpiCard
icon={<Ban size={17} />}
label="Cancelled Orders"
value={count(totals.cancelled)}
@@ -55,7 +55,7 @@ export function KpiStrip({ totals, isLoading }: KpiStripProps) {
? { note: `${Math.round((totals.cancelled / totals.onlineOrders) * 100)}% of app orders` }
: {})}
/>
<Kpi
<KpiCard
icon={<CloudUpload size={17} />}
label="Unsynced Bills"
value={count(totals.pendingBills)}
@@ -65,22 +65,3 @@ export function KpiStrip({ totals, isLoading }: KpiStripProps) {
);
}
function Kpi({
icon, label, value, note,
}: {
icon: ReactNode;
label: string;
value: string;
note?: string;
}) {
return (
<article className="kpi">
<span className="kpi-icon" aria-hidden>{icon}</span>
<div className="kpi-head">
<span className="kpi-label">{label}</span>
<span className="kpi-value">{value}</span>
{note ? <span className="kpi-note">{note}</span> : null}
</div>
</article>
);
}

View File

@@ -29,7 +29,7 @@ export interface ShopSummaryProps {
export function ShopSummary({ row, shop, base }: ShopSummaryProps) {
if (!row) {
return (
<Card padding={3} elevation="low">
<Card padding={3}>
<Text type="body" size="sm" color="secondary">
No branch is selected.
</Text>
@@ -46,7 +46,7 @@ export function ShopSummary({ row, shop, base }: ShopSummaryProps) {
const total = row.onlineRevenue + row.counterRevenue;
return (
<Card padding={3} elevation="low">
<Card padding={3}>
<VStack gap={3}>
<HStack gap={2} align="start" wrap="wrap">
<span className="shop-mark" aria-hidden>

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { Search } from 'lucide-react';
import { SearchInput } from '@/components/SearchInput';
import { branchLabel, count, money } from '@/features/store-admin/format';
import { SYNC_LABEL, shortAge, type TerminalStatus } from '@/features/store-admin/posStatus';
import type { BranchRow, HealthTone } from '../consoleModel';
@@ -111,15 +111,13 @@ export function TillSync({ rows, showBranch, base }: TillSyncProps) {
</button>
</div>
<label className="search">
<Search size={14} aria-hidden />
<input
value={term}
onChange={(event) => setTerm(event.target.value)}
placeholder={showBranch ? 'Search till ID, branch…' : 'Search till ID…'}
aria-label="Search tills"
/>
</label>
<SearchInput
label="Search tills"
value={term}
onChange={setTerm}
placeholder={showBranch ? 'Till ID, branch…' : 'Till ID…'}
width={220}
/>
</div>
</header>