From b85775a4131d19d613f6b5bd13f99689801e8d15 Mon Sep 17 00:00:00 2001 From: Bhanu Prakash Sai Potteri Date: Wed, 8 Jul 2026 11:34:43 +0530 Subject: [PATCH] Modernize mobile UI - StatsTiles: on-brand compact tiles (token accents) vs generic gradients - RecordView: pill search, filter chips, elevated list cards w/ chevron - Bottom nav: active sunrise pill + elevation --- src/components/reusable/StatsTiles.tsx | 55 ++++----- src/components/rv/RecordView.tsx | 159 +++++++++++++------------ src/screens/ConsoleLayout.tsx | 33 +++-- 3 files changed, 126 insertions(+), 121 deletions(-) diff --git a/src/components/reusable/StatsTiles.tsx b/src/components/reusable/StatsTiles.tsx index ea6a3eb..82e446d 100644 --- a/src/components/reusable/StatsTiles.tsx +++ b/src/components/reusable/StatsTiles.tsx @@ -1,60 +1,45 @@ -import { Card } from "./Card"; import type { TileItem } from "../../api/types"; export interface StatsTilesProps { tiles?: TileItem[]; } -const colors = [ - "from-blue-500 to-cyan-500", - "from-emerald-500 to-green-500", - "from-orange-500 to-amber-500", - "from-violet-500 to-fuchsia-500", - "from-pink-500 to-rose-500", - "from-indigo-500 to-blue-500", +const TONES = [ + "bg-sunrise-500", + "bg-emerald-500", + "bg-sky-500", + "bg-amber-500", + "bg-navy-600", + "bg-ruby-500", ]; export function StatsTiles({ tiles }: StatsTilesProps) { if (!tiles?.length) return null; return ( -
+
{tiles.map((tile, idx) => { const displayLabel = (tile.key || `tile_${idx}`) .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()); - const gradient = colors[idx % colors.length]; - return ( - - {/* Top Gradient */} -
- - {/* Background Decoration */} -
- -
-

- {displayLabel} -

- -

- {(tile.value as React.ReactNode) ?? "-"} -

- -
-
- +

+ {displayLabel} +

+

+ {(tile.value as React.ReactNode) ?? "-"} +

+
); })}
); -} \ No newline at end of file +} diff --git a/src/components/rv/RecordView.tsx b/src/components/rv/RecordView.tsx index e8b78f4..9c5b61c 100644 --- a/src/components/rv/RecordView.tsx +++ b/src/components/rv/RecordView.tsx @@ -1,10 +1,9 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Search } from 'lucide-react'; +import { Search, ChevronRight } from 'lucide-react'; import { cn } from '../../lib/cn'; import { formatValue } from '../../lib/format'; import type { ZinoClient } from '../../api/client'; import type { RecordViewField, RecordViewResponse } from '../../api/types'; -import { Card } from '../reusable/Card'; import { Pagination } from '../reusable/Pagination'; import { Spinner } from '../reusable/Spinner'; import { EmptyState } from '../reusable/EmptyState'; @@ -119,27 +118,37 @@ export function RecordView({ const chartData = resp?.chart_data; + const filterEntries = resp?.config?.filter_options + ? Object.entries(resp.config.filter_options).filter(([, o]) => o && o.length > 0) + : []; + return ( -
+
- -
-
- {resp?.config?.filter_options && Object.entries(resp.config.filter_options).map(([key, options]) => { - if (!options || options.length === 0) return null; - - // Find the field in config to get its proper label - const fieldDef = resp.config.fields.find(f => f.field_key === key); +
+

{title}

+ {headerActions} +
+ +
+
+ + setSearch(e.target.value)} + placeholder="Search…" + className="w-full h-11 rounded-pill bg-transparent pl-11 pr-4 font-sans text-sm text-strong outline-none placeholder:text-faint" + /> +
+ {filterEntries.length > 0 && ( +
+ {filterEntries.map(([key, options]) => { + const fieldDef = resp?.config.fields.find(f => f.field_key === key); const label = fieldDef?.output_label || key; - - const opts = options.map(o => ({ value: o, label: o })); + const opts = options!.map(o => ({ value: o, label: o })); return ( setSearch(e.target.value)} - placeholder="Search…" - className="w-full h-9 rounded-md border border-border-default bg-card pl-9 pr-3 font-sans text-sm text-strong outline-none focus-ring" - /> -
-
+ )} +
- {error ? ( - - ) : loading && !resp ? ( -
- -
- ) : rows.length === 0 ? ( - - ) : ( -
- {rows.map((row, i) => { - const [primary, ...secondary] = fields; - return ( -
onRowClick(row, i) : undefined} - className={cn( - 'rounded-lg border border-border-subtle bg-card p-4 min-h-[56px] transition-colors duration-150', - onRowClick && 'cursor-pointer active:opacity-80', - )} - > - {primary && ( -

- {formatValue(row[primary.field_key])} -

- )} - {secondary.length > 0 && ( -
- {secondary.map((f) => ( -

- {f.output_label}: {formatValue(row[f.field_key])} -

- ))} -
- )} - {rowActions && ( -
e.stopPropagation()} - className="mt-3 pt-3 border-t border-border-subtle flex justify-end gap-2" - > - {rowActions(row)} -
+ {error ? ( + + ) : loading && !resp ? ( +
+ +
+ ) : rows.length === 0 ? ( + + ) : ( +
+ {rows.map((row, i) => { + const [primary, ...secondary] = fields; + return ( +
onRowClick(row, i) : undefined} + className={cn( + 'rounded-xl border border-border-subtle bg-card p-4 shadow-sm transition-transform duration-150', + onRowClick && 'cursor-pointer active:scale-[0.99]', + )} + > +
+
+ {primary && ( +

+ {formatValue(row[primary.field_key])} +

+ )} + {secondary.length > 0 && ( +
+ {secondary.map((f) => ( +

+ {f.output_label}: {formatValue(row[f.field_key])} +

+ ))} +
+ )} +
+ {onRowClick && ( + )}
- ); - })} -
- )} - - + {rowActions && ( +
e.stopPropagation()} + className="mt-3 pt-3 border-t border-border-subtle flex justify-end gap-2" + > + {rowActions(row)} +
+ )} +
+ ); + })} +
+ )} +
); } diff --git a/src/screens/ConsoleLayout.tsx b/src/screens/ConsoleLayout.tsx index a64e313..3e3b075 100644 --- a/src/screens/ConsoleLayout.tsx +++ b/src/screens/ConsoleLayout.tsx @@ -25,7 +25,7 @@ export function ConsoleLayout() { return (
-
+
Krishna Sales Sandbox {APP_ID}{user?.name ? ` · ${user.name}` : ''} @@ -50,7 +50,7 @@ export function ConsoleLayout() {