Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
*/
import React from 'react';
import type { PermissionRisk } from '../lib/access';
import { sectionLabelCls } from '@/shared/components/surfaces';

export const inputCls =
'w-full rounded-md border border-slate-700 bg-slate-950 px-2.5 py-1.5 text-[12px] text-slate-100 outline-none accent-focus';

/** The micro-label above a control. Exported because two panels had their own copy. */
export const labelCls = 'text-[10px] font-bold uppercase tracking-wide text-slate-500';
/**
* Re-exported rather than respelled: this string is the same micro-label the
* rest of the app uses, and it drifted into eight variants precisely because
* each feature kept its own copy.
*/
export const labelCls = sectionLabelCls;

export const RISK_STYLE: Record<PermissionRisk, string> = {
low: 'text-emerald-300 border-emerald-500/40 bg-emerald-500/10',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
import React from 'react';
import type { TimelineVersion } from './VersionTimeline';
import { StatCard } from '@/shared/components/surfaces';

const CHART_H = 180;
const BAR_W = 18;
Expand Down Expand Up @@ -108,16 +109,32 @@ export function VersionBriefing({
const removed = selected.removed ?? 0;
return (
<div
className="flex shrink-0 flex-wrap items-center gap-3 border-b border-slate-800 px-6 py-2"
className="shrink-0 border-b border-slate-800 px-6 py-3"
data-testid="lokee-version-briefing"
>
<span className="text-[12px] font-bold text-slate-100">v{selected.number}</span>
<span className="truncate text-[11px] text-slate-400">
{selected.name || selected.source || 'Snapshot'}
</span>
<span className="font-bold text-emerald-400">+{added}</span>
<span className="font-bold text-amber-400">~{modified}</span>
<span className="font-bold text-rose-400">−{removed}</span>
<div className="mb-2 flex flex-wrap items-baseline gap-2">
<span className="text-[13px] font-bold text-slate-100">v{selected.number}</span>
<span className="truncate text-[11px] text-slate-400">
{selected.name || selected.source || 'Snapshot'}
</span>
</div>
{/* Counts as cards, not a run-on of coloured numbers: the sign alone
("+3 ~2 −0") makes the reader supply the nouns. */}
<div className="grid grid-cols-3 gap-2">
<StatCard testId="lokee-briefing-added" label="Added" tone="positive" value={`+${added}`} />
<StatCard
testId="lokee-briefing-modified"
label="Modified"
tone="warning"
value={`~${modified}`}
/>
<StatCard
testId="lokee-briefing-removed"
label="Removed"
tone="danger"
value={`−${removed}`}
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Loader2 } from 'lucide-react';
import { fetchTableInsight, type TableInsightResponse } from '@/shared/api/schemaApi';
import { tableNameParts } from '@/shared/lib/tablePreview';
import { useSqlEditorStore } from '@/app/store/useSqlEditorStore';
import { StatCard } from '@/shared/components/surfaces';

function tableRef(tableName: string, fallbackSchema?: string): { table: string; schema?: string } {
const parts = tableNameParts(tableName);
Expand Down Expand Up @@ -98,48 +99,34 @@ export const PeekInsight: React.FC<{
className="grid grid-cols-3 gap-2 mb-3"
data-testid="data-peek-insight-cards"
>
<div
className="rounded-lg border border-slate-800 bg-slate-950/50 px-2.5 py-2"
data-testid="data-peek-insight-card-rows"
>
<p className="text-[10px] font-bold uppercase tracking-wide text-slate-500">Rows</p>
<p className="mt-1 font-mono text-sm font-semibold text-slate-100">
{data.estimatedRows == null ? '—' : data.estimatedRows.toLocaleString()}
</p>
<p className="mt-0.5 text-[10px] text-slate-500">Estimated from catalog</p>
</div>
<div
className="rounded-lg border border-slate-800 bg-slate-950/50 px-2.5 py-2"
data-testid="data-peek-insight-card-nulls"
>
<p className="text-[10px] font-bold uppercase tracking-wide text-slate-500">
Null-heavy
</p>
<p className="mt-1 font-mono text-sm font-semibold text-amber-200">
{cards.nullHeavy.length === 0
<StatCard
testId="data-peek-insight-card-rows"
label="Rows"
value={data.estimatedRows == null ? '—' : data.estimatedRows.toLocaleString()}
hint="Estimated from catalog"
/>
<StatCard
testId="data-peek-insight-card-nulls"
label="Null-heavy"
tone="warning"
value={
cards.nullHeavy.length === 0
? 'None ≥20%'
: cards.nullHeavy.map((c) => c.name).join(', ')}
</p>
<p className="mt-0.5 text-[10px] text-slate-500">
Avg null {pct(cards.avgNull)}
</p>
</div>
<div
className="rounded-lg border border-slate-800 bg-slate-950/50 px-2.5 py-2"
data-testid="data-peek-insight-card-distinct"
>
<p className="text-[10px] font-bold uppercase tracking-wide text-slate-500">
High distinct
</p>
<p className="mt-1 font-mono text-sm font-semibold text-sky-200">
{cards.distinctHeavy.length === 0
: cards.nullHeavy.map((c) => c.name).join(', ')
}
hint={`Avg null ${pct(cards.avgNull)}`}
/>
<StatCard
testId="data-peek-insight-card-distinct"
label="High distinct"
tone="info"
value={
cards.distinctHeavy.length === 0
? '—'
: cards.distinctHeavy
.map((c) => `${c.name} (${c.nDistinct})`)
.join(', ')}
</p>
<p className="mt-0.5 text-[10px] text-slate-500">Top nDistinct columns</p>
</div>
: cards.distinctHeavy.map((c) => `${c.name} (${c.nDistinct})`).join(', ')
}
hint="Top nDistinct columns"
/>
</div>

<p className="mb-2 text-[12px] text-slate-300" data-testid="data-peek-insight-rows">
Expand Down
82 changes: 82 additions & 0 deletions apps/web/src/frontend/shared/components/surfaces.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Fox Schema (foxschema)
* Copyright 2024-2026 Huy Phan <huyplb@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*
* What the shared surfaces guarantee to the screens built out of them.
*
* These are presentation primitives, so the useful assertions are the ones a
* redesign could break silently: that a tone still reaches the value, that the
* provenance line is optional rather than rendered empty, and that every
* caller's label comes out spelled the same way — the drift that motivated
* extracting them in the first place.
*/
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import { SectionLabel, StatCard, Panel, sectionLabelCls } from './surfaces';

describe('SectionLabel', () => {
it('is one spelling, so two callers cannot drift apart', () => {
const { container } = render(
<>
<SectionLabel>Rows</SectionLabel>
<SectionLabel>Null-heavy</SectionLabel>
</>
);
const classes = [...container.querySelectorAll('p')].map((p) => p.className);
expect(new Set(classes).size).toBe(1);
expect(classes[0]).toBe(sectionLabelCls);
});

it('adds caller classes without dropping its own', () => {
render(<SectionLabel className="mb-1">Scope</SectionLabel>);
const el = screen.getByText('Scope');
expect(el.className).toContain('mb-1');
expect(el.className).toContain('uppercase');
});
});

describe('StatCard', () => {
it('carries the tone to the value, not the label', () => {
render(<StatCard testId="c" label="Null-heavy" tone="warning" value="email" />);
expect(screen.getByText('email').className).toContain('text-amber-200');
expect(screen.getByText('Null-heavy').className).not.toContain('text-amber-200');
});

it('defaults to the neutral tone', () => {
render(<StatCard testId="c" label="Rows" value="2.4M" />);
expect(screen.getByText('2.4M').className).toContain('text-slate-100');
});

it('renders no hint line at all when there is no provenance to give', () => {
// Not an empty <p>: a blank line under the number reads as a value that
// failed to load, which is the opposite of "this figure needs no caveat".
const { container } = render(<StatCard testId="c" label="Rows" value="12" />);
expect(container.querySelectorAll('p')).toHaveLength(2);
});

it('keeps the hint when one is given', () => {
render(<StatCard testId="c" label="Rows" value="12" hint="Estimated from catalog" />);
expect(screen.getByText('Estimated from catalog')).toBeTruthy();
});

it('accepts a zero value rather than treating it as absent', () => {
// `−0 Removed` is a real answer on a snapshot briefing, and a falsy check
// here would blank it.
render(<StatCard testId="zero" label="Removed" value={0} />);
expect(screen.getByTestId('zero').textContent).toContain('0');
});
});

describe('Panel', () => {
it('can drop its padding for callers that own their own spacing', () => {
const { rerender } = render(<Panel testId="p">x</Panel>);
expect(screen.getByTestId('p').className).toContain('px-2.5');
rerender(
<Panel testId="p" padded={false}>
x
</Panel>
);
expect(screen.getByTestId('p').className).not.toContain('px-2.5');
});
});
80 changes: 80 additions & 0 deletions apps/web/src/frontend/shared/components/surfaces.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Fox Schema (foxschema)
* Copyright 2024-2026 Huy Phan <huyplb@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*
* The three surfaces every screen in this app is built out of.
*
* They were extracted by counting, not by taste. The frontend held 85 distinct
* card/panel class strings, and the small uppercase label above a group came in
* eight spellings — `text-[10px]` and `text-[11px]`, `tracking-wide` and
* `tracking-wider`, `text-slate-400` and `text-slate-500` — across some seventy
* uses. None of that variation meant anything; it is what a screen looks like
* when each one is written on its own.
*
* `labelCls` in the access feature already said this once, but it lives inside
* a feature, so nothing else could reach it without crossing a boundary the
* architecture test forbids. Shared is where a primitive every feature needs
* belongs.
*/
import React from 'react';

/** The uppercase micro-label that titles a group. One spelling, everywhere. */
export const sectionLabelCls = 'text-[10px] font-bold uppercase tracking-wide text-slate-500';

/** The app's card treatment: one border, one ground, one radius. */
export const panelCls = 'rounded-lg border border-slate-800 bg-slate-950/50';

export const SectionLabel: React.FC<{
children: React.ReactNode;
className?: string;
}> = ({ children, className }) => (
<p className={className ? `${sectionLabelCls} ${className}` : sectionLabelCls}>{children}</p>
);

export const Panel: React.FC<{
children: React.ReactNode;
/** Extra classes; padding is included by default so callers rarely need it. */
className?: string;
padded?: boolean;
testId?: string;
}> = ({ children, className, padded = true, testId }) => (
<div
className={[panelCls, padded ? 'px-2.5 py-2' : '', className].filter(Boolean).join(' ')}
data-testid={testId}
>
{children}
</div>
);

/**
* How much of a thing there is, and what that number came from.
*
* Label, value, and a line of provenance under it — the shape Peek Insight and
* the snapshot briefing had each grown separately. The hint is not decoration:
* a row count read from a catalog estimate and one read by counting are
* different claims, and the card is where that gets said.
*/
export type StatTone = 'default' | 'positive' | 'warning' | 'danger' | 'info';

const TONE_CLS: Record<StatTone, string> = {
default: 'text-slate-100',
positive: 'text-emerald-300',
warning: 'text-amber-200',
danger: 'text-rose-300',
info: 'text-sky-200',
};

export const StatCard: React.FC<{
label: React.ReactNode;
value: React.ReactNode;
hint?: React.ReactNode;
tone?: StatTone;
testId?: string;
}> = ({ label, value, hint, tone = 'default', testId }) => (
<Panel testId={testId}>
<SectionLabel>{label}</SectionLabel>
<p className={`mt-1 font-mono text-sm font-semibold ${TONE_CLS[tone]}`}>{value}</p>
{hint != null && <p className="mt-0.5 text-[10px] text-slate-500">{hint}</p>}
</Panel>
);
Loading
Loading