Skip to content

Commit 41d2a3c

Browse files
MartinCastroAlvarezmartin-castro-laminr-aiclaude
authored
feat(web): list-UX polish — Lucide icons, no-wrap cells, FK-link styling (#188)
Addresses three repo-owner reports from live testing: 1. **Icons (Lucide).** Installed `lucide-react` (the DESIGN_SYSTEM §7 mandated library — one icon set, MIT, tree-shakeable). The mobile nav button now uses the `Menu` (3-line hamburger) icon; the list Filter button uses `ListFilter`. (Owner asked for "3 horizontal lines" — Lucide provides exactly that without adding a second icon system like Font Awesome.) 2. **Cell text no longer splits mid-value.** `@dar/ui` Table `<th>` / `<td>` get `whitespace-nowrap`; the table already lives in `overflow-x-auto`, so long values scroll horizontally (matches Django admin's changelist) instead of wrapping "Test Tenant" onto two lines. 3. **ForeignKey cells styled as links.** `FieldValueView` renders FK envelopes (`{id,label}`) with link styling; exported `isForeignKeyValue` from `@dar/data`. Full navigation to the related detail needs the column FK-target metadata (tracked in #184). Verified: `pnpm -r typecheck` + `pnpm --filter @dar/web build` pass. In-browser verification is the repo owner's. Tier 4 (frontend). `lucide-react` is an app-level dep (apps/web), not a generic-package dep; aligns with the existing DESIGN_SYSTEM choice. Refs #184; ACCEPTANCE §2.4 / §2.8. Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c9af1fd commit 41d2a3c

8 files changed

Lines changed: 42 additions & 10 deletions

File tree

frontend/apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@dar/api": "workspace:*",
1616
"@dar/data": "workspace:*",
1717
"@dar/ui": "workspace:*",
18+
"lucide-react": "^1.16.0",
1819
"react": "^18.3.0",
1920
"react-dom": "^18.3.0",
2021
"react-router-dom": "^6.26.0"

frontend/apps/web/src/Layout.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { PropsWithChildren } from 'react';
22
import { useEffect, useMemo, useState } from 'react';
3+
import { Menu } from 'lucide-react';
34
import { Link } from 'react-router-dom';
45

56
import { useRegistry } from '@dar/data';
@@ -116,9 +117,7 @@ export function Layout({ children }: PropsWithChildren) {
116117
onClick={() => setDrawerOpen(true)}
117118
className="-ml-2 rounded p-2 hover:bg-gray-800"
118119
>
119-
<span className="mb-1 block h-0.5 w-5 bg-current" />
120-
<span className="mb-1 block h-0.5 w-5 bg-current" />
121-
<span className="block h-0.5 w-5 bg-current" />
120+
<Menu className="h-5 w-5" aria-hidden />
122121
</button>
123122
<Link to="/" onClick={closeDrawer} className="flex items-center gap-2 font-semibold">
124123
{BRAND_LOGO_URL && <img src={BRAND_LOGO_URL} alt="" className="h-5 w-5 rounded" />}

frontend/apps/web/src/components/FieldValueView.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// text (e.g. a `CharField` holding `<script>`) stays inert. The trust
99
// boundary is identical to Django's `mark_safe`. See SECURITY.md + #172.
1010

11-
import { isHtmlValue, renderValue, type FieldValue } from '@dar/data';
11+
import { isForeignKeyValue, isHtmlValue, renderValue, type FieldValue } from '@dar/data';
1212

1313
interface FieldValueViewProps {
1414
value: FieldValue | undefined;
@@ -18,5 +18,13 @@ export function FieldValueView({ value }: FieldValueViewProps) {
1818
if (isHtmlValue(value)) {
1919
return <span dangerouslySetInnerHTML={{ __html: value.html }} />;
2020
}
21+
// ForeignKey cells read as links (the related object's str()). Style
22+
// them as links so they're visually navigable — matches Django
23+
// admin's list_display FK columns. Full navigation to the related
24+
// detail needs the column's FK-target metadata (tracked in #184);
25+
// until then the row click (→ this row's detail) is the action.
26+
if (isForeignKeyValue(value)) {
27+
return <span className="text-blue-600 underline decoration-dotted">{value.label}</span>;
28+
}
2129
return <>{renderValue(value)}</>;
2230
}

frontend/apps/web/src/pages/ListPage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// the data layer's job.
77

88
import { useMemo, useState } from 'react';
9+
import { ListFilter } from 'lucide-react';
910
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
1011

1112
import {
@@ -141,11 +142,12 @@ export function ListPage() {
141142
type="button"
142143
onClick={() => setFilterOpen(true)}
143144
aria-haspopup="dialog"
144-
className="shrink-0 rounded border border-gray-300 px-3 py-2 text-sm hover:bg-gray-100"
145+
className="inline-flex shrink-0 items-center gap-1.5 rounded border border-gray-300 px-3 py-2 text-sm hover:bg-gray-100"
145146
>
147+
<ListFilter className="h-4 w-4" aria-hidden />
146148
Filters
147149
{chips.length > 0 && (
148-
<span className="ml-1 rounded-full bg-blue-600 px-1.5 py-0.5 text-xs text-white">
150+
<span className="ml-0.5 rounded-full bg-blue-600 px-1.5 py-0.5 text-xs text-white">
149151
{chips.length}
150152
</span>
151153
)}

frontend/packages/data/src/format.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import type { FieldValue, ForeignKeyValue, HtmlValue } from '@dar/api';
99

1010
const EMPTY_PLACEHOLDER = '—';
1111

12-
function isForeignKeyValue(value: unknown): value is ForeignKeyValue {
12+
/**
13+
* True when the value is a ForeignKey envelope (`{ id, label }`) — the
14+
* related object's pk + `str()`. Callers render it as a link so FK
15+
* cells read as navigable, matching Django admin's `list_display` FK
16+
* columns.
17+
*/
18+
export function isForeignKeyValue(value: unknown): value is ForeignKeyValue {
1319
return typeof value === 'object' && value !== null && 'id' in value && 'label' in value;
1420
}
1521

frontend/packages/data/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type {
2727
HtmlValue,
2828
ListResponse,
2929
ListRow,
30+
LoginResponse,
3031
Permissions,
3132
RegistryAppEntry,
3233
RegistryModelEntry,
@@ -47,7 +48,7 @@ export type { DetailState } from './detail-context';
4748
export { createObject, updateObject, deleteObject } from './mutations';
4849
export type { CreateArgs, UpdateArgs, DeleteArgs } from './mutations';
4950

50-
export { renderValue, isHtmlValue } from './format';
51+
export { renderValue, isHtmlValue, isForeignKeyValue } from './format';
5152

5253
export { useSwrCache } from './swr-cache';
5354
export type { SwrState, UseSwrCacheArgs } from './swr-cache';

frontend/packages/ui/src/Table.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function Table<Row>({
5454
<th
5555
key={col.key}
5656
scope="col"
57-
className={`px-4 py-2 font-medium ${align} ${sortable ? 'cursor-pointer hover:bg-gray-100' : ''}`}
57+
className={`whitespace-nowrap px-4 py-2 font-medium ${align} ${sortable ? 'cursor-pointer hover:bg-gray-100' : ''}`}
5858
onClick={sortable ? () => onSort(col.key) : undefined}
5959
>
6060
<span className="inline-flex items-center gap-1">
@@ -78,7 +78,10 @@ export function Table<Row>({
7878
className={onRowClick ? 'cursor-pointer hover:bg-gray-50' : ''}
7979
>
8080
{columns.map((col) => (
81-
<td key={col.key} className={`px-4 py-2 ${ALIGN_CLASSES[col.align ?? 'left']}`}>
81+
<td
82+
key={col.key}
83+
className={`whitespace-nowrap px-4 py-2 ${ALIGN_CLASSES[col.align ?? 'left']}`}
84+
>
8285
{col.render(row)}
8386
</td>
8487
))}

frontend/pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)