ux-toolkit 0.1.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -7
- package/agents/card-reviewer.md +173 -0
- package/agents/comparison-reviewer.md +143 -0
- package/agents/density-reviewer.md +207 -0
- package/agents/detail-page-reviewer.md +143 -0
- package/agents/editor-reviewer.md +165 -0
- package/agents/form-reviewer.md +156 -0
- package/agents/game-ui-reviewer.md +181 -0
- package/agents/list-page-reviewer.md +132 -0
- package/agents/navigation-reviewer.md +145 -0
- package/agents/panel-reviewer.md +182 -0
- package/agents/replay-reviewer.md +174 -0
- package/agents/settings-reviewer.md +166 -0
- package/agents/ux-auditor.md +145 -45
- package/agents/ux-engineer.md +211 -38
- package/dist/cli.js +172 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +172 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -4
- package/dist/index.d.ts +128 -4
- package/dist/index.js +172 -5
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/skills/canvas-grid-patterns/SKILL.md +367 -0
- package/skills/comparison-patterns/SKILL.md +354 -0
- package/skills/data-density-patterns/SKILL.md +493 -0
- package/skills/detail-page-patterns/SKILL.md +522 -0
- package/skills/drag-drop-patterns/SKILL.md +406 -0
- package/skills/editor-workspace-patterns/SKILL.md +552 -0
- package/skills/event-timeline-patterns/SKILL.md +542 -0
- package/skills/form-patterns/SKILL.md +608 -0
- package/skills/info-card-patterns/SKILL.md +531 -0
- package/skills/keyboard-shortcuts-patterns/SKILL.md +365 -0
- package/skills/list-page-patterns/SKILL.md +351 -0
- package/skills/modal-patterns/SKILL.md +750 -0
- package/skills/navigation-patterns/SKILL.md +476 -0
- package/skills/page-structure-patterns/SKILL.md +271 -0
- package/skills/playback-replay-patterns/SKILL.md +695 -0
- package/skills/react-ux-patterns/SKILL.md +434 -0
- package/skills/split-panel-patterns/SKILL.md +609 -0
- package/skills/status-visualization-patterns/SKILL.md +635 -0
- package/skills/toast-notification-patterns/SKILL.md +207 -0
- package/skills/turn-based-ui-patterns/SKILL.md +506 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-density-patterns
|
|
3
|
+
description: Patterns for displaying dense information on screens without overlap, with proper scrolling, z-index management, and responsive condensing
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Data Density UX Patterns
|
|
8
|
+
|
|
9
|
+
When screens need to display a lot of information, proper density management prevents overlap, ensures readability, and maintains usability.
|
|
10
|
+
|
|
11
|
+
## Core Principles
|
|
12
|
+
|
|
13
|
+
### The Density Hierarchy
|
|
14
|
+
```
|
|
15
|
+
1. Essential information visible immediately (no scroll)
|
|
16
|
+
2. Important information accessible with minimal scroll
|
|
17
|
+
3. Secondary information in collapsible sections
|
|
18
|
+
4. Tertiary information behind "show more" or modals
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Screen Real Estate Budget
|
|
22
|
+
| Screen Size | Visible Height | Recommended Sections |
|
|
23
|
+
|-------------|----------------|---------------------|
|
|
24
|
+
| Mobile (< 640px) | ~500px | 2-3 collapsed sections |
|
|
25
|
+
| Tablet (768px) | ~600px | 3-4 sections with scrolling |
|
|
26
|
+
| Desktop (1024px+) | ~700px | Multi-column layout |
|
|
27
|
+
| Large (1440px+) | ~800px | Dashboard-style grid |
|
|
28
|
+
|
|
29
|
+
## Preventing Overlap
|
|
30
|
+
|
|
31
|
+
### Z-Index Scale (MUST FOLLOW)
|
|
32
|
+
```css
|
|
33
|
+
/* Establish consistent z-index layers */
|
|
34
|
+
:root {
|
|
35
|
+
--z-base: 0;
|
|
36
|
+
--z-dropdown: 10; /* Dropdowns, tooltips */
|
|
37
|
+
--z-sticky: 20; /* Sticky headers, navigation */
|
|
38
|
+
--z-overlay: 30; /* Page overlays */
|
|
39
|
+
--z-modal: 40; /* Modal dialogs */
|
|
40
|
+
--z-popover: 50; /* Popovers on modals */
|
|
41
|
+
--z-toast: 60; /* Toast notifications */
|
|
42
|
+
--z-tooltip: 70; /* Tooltips (always on top) */
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Stacking Context Rules
|
|
47
|
+
```tsx
|
|
48
|
+
// RULE: Each layer creates a stacking context
|
|
49
|
+
// Avoid z-index wars by using proper containment
|
|
50
|
+
|
|
51
|
+
// BAD: Random z-index values
|
|
52
|
+
<div style={{ zIndex: 9999 }}>...</div>
|
|
53
|
+
|
|
54
|
+
// GOOD: Semantic layer classes
|
|
55
|
+
<div className="z-dropdown">...</div>
|
|
56
|
+
<div className="z-modal">...</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Overlap Prevention Checklist
|
|
60
|
+
- [ ] Fixed/sticky elements don't overlap content
|
|
61
|
+
- [ ] Dropdowns don't get cut off by containers
|
|
62
|
+
- [ ] Modals appear above all page content
|
|
63
|
+
- [ ] Tooltips appear above modals when needed
|
|
64
|
+
- [ ] Toasts don't block important UI
|
|
65
|
+
|
|
66
|
+
## Ensuring Nothing Is Off-Screen
|
|
67
|
+
|
|
68
|
+
### Viewport-Aware Positioning
|
|
69
|
+
```tsx
|
|
70
|
+
function useViewportAwarePosition(elementRef: RefObject<HTMLElement>) {
|
|
71
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!elementRef.current) return;
|
|
75
|
+
|
|
76
|
+
const rect = elementRef.current.getBoundingClientRect();
|
|
77
|
+
const viewport = {
|
|
78
|
+
width: window.innerWidth,
|
|
79
|
+
height: window.innerHeight,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let x = position.x;
|
|
83
|
+
let y = position.y;
|
|
84
|
+
|
|
85
|
+
// Prevent right overflow
|
|
86
|
+
if (rect.right > viewport.width) {
|
|
87
|
+
x -= (rect.right - viewport.width + 16);
|
|
88
|
+
}
|
|
89
|
+
// Prevent bottom overflow
|
|
90
|
+
if (rect.bottom > viewport.height) {
|
|
91
|
+
y -= (rect.bottom - viewport.height + 16);
|
|
92
|
+
}
|
|
93
|
+
// Prevent left overflow
|
|
94
|
+
if (rect.left < 0) {
|
|
95
|
+
x += Math.abs(rect.left) + 16;
|
|
96
|
+
}
|
|
97
|
+
// Prevent top overflow
|
|
98
|
+
if (rect.top < 0) {
|
|
99
|
+
y += Math.abs(rect.top) + 16;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setPosition({ x, y });
|
|
103
|
+
}, [elementRef]);
|
|
104
|
+
|
|
105
|
+
return position;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Container Scroll Handling
|
|
110
|
+
```tsx
|
|
111
|
+
// RULE: Content containers must have explicit overflow handling
|
|
112
|
+
|
|
113
|
+
// Dense content area with scroll
|
|
114
|
+
<div className="
|
|
115
|
+
max-h-[calc(100vh-200px)] /* Account for header/footer */
|
|
116
|
+
overflow-y-auto
|
|
117
|
+
overflow-x-hidden
|
|
118
|
+
scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent
|
|
119
|
+
">
|
|
120
|
+
{/* Dense content */}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
// Horizontal scroll for wide tables
|
|
124
|
+
<div className="overflow-x-auto -mx-4 px-4">
|
|
125
|
+
<table className="min-w-[800px]">...</table>
|
|
126
|
+
</div>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Safe Area Insets (Mobile)
|
|
130
|
+
```css
|
|
131
|
+
/* Account for notches, home indicators */
|
|
132
|
+
.page-container {
|
|
133
|
+
padding-left: max(1rem, env(safe-area-inset-left));
|
|
134
|
+
padding-right: max(1rem, env(safe-area-inset-right));
|
|
135
|
+
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Readability in Dense Layouts
|
|
140
|
+
|
|
141
|
+
### Minimum Spacing Rules
|
|
142
|
+
```css
|
|
143
|
+
/* NEVER go below these minimums */
|
|
144
|
+
--min-touch-target: 44px; /* Minimum tappable area */
|
|
145
|
+
--min-text-spacing: 4px; /* Between text lines */
|
|
146
|
+
--min-element-gap: 8px; /* Between UI elements */
|
|
147
|
+
--min-section-gap: 16px; /* Between sections */
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Typography for Dense Data
|
|
151
|
+
```tsx
|
|
152
|
+
// Data-dense typography scale
|
|
153
|
+
const denseTypography = {
|
|
154
|
+
heading: 'text-base font-semibold', // Smaller than normal
|
|
155
|
+
label: 'text-xs font-medium uppercase tracking-wide text-text-muted',
|
|
156
|
+
value: 'text-sm font-mono', // Mono for data alignment
|
|
157
|
+
secondary: 'text-xs text-text-secondary',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Example: Dense stat display
|
|
161
|
+
<div className="grid grid-cols-4 gap-2">
|
|
162
|
+
<div className="p-2 bg-surface-raised rounded">
|
|
163
|
+
<span className={denseTypography.label}>Speed</span>
|
|
164
|
+
<span className={denseTypography.value}>142</span>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Color Contrast in Dense UI
|
|
170
|
+
```tsx
|
|
171
|
+
// High contrast for dense data
|
|
172
|
+
const denseColors = {
|
|
173
|
+
background: 'bg-surface-deep', // Darkest background
|
|
174
|
+
card: 'bg-surface-base', // Slightly lighter
|
|
175
|
+
highlight: 'bg-accent/10', // Subtle highlight
|
|
176
|
+
border: 'border-border/50', // Subtle borders
|
|
177
|
+
text: 'text-white', // Maximum contrast
|
|
178
|
+
muted: 'text-text-secondary', // De-emphasized
|
|
179
|
+
};
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Condensing Information Patterns
|
|
183
|
+
|
|
184
|
+
### Collapsible Sections
|
|
185
|
+
```tsx
|
|
186
|
+
function CollapsibleDataSection({ title, defaultOpen = false, children }) {
|
|
187
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className="border border-border rounded-lg overflow-hidden">
|
|
191
|
+
<button
|
|
192
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
193
|
+
className="w-full flex items-center justify-between px-4 py-3 bg-surface-raised hover:bg-surface-raised/80"
|
|
194
|
+
>
|
|
195
|
+
<span className="text-sm font-medium text-white">{title}</span>
|
|
196
|
+
<ChevronDownIcon className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
197
|
+
</button>
|
|
198
|
+
|
|
199
|
+
{isOpen && (
|
|
200
|
+
<div className="px-4 py-3 bg-surface-base">
|
|
201
|
+
{children}
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Truncation with Expansion
|
|
210
|
+
```tsx
|
|
211
|
+
function TruncatedText({ text, maxLength = 100 }) {
|
|
212
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
213
|
+
const shouldTruncate = text.length > maxLength;
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div>
|
|
217
|
+
<p className="text-sm text-text-primary">
|
|
218
|
+
{isExpanded || !shouldTruncate ? text : `${text.slice(0, maxLength)}...`}
|
|
219
|
+
</p>
|
|
220
|
+
{shouldTruncate && (
|
|
221
|
+
<button
|
|
222
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
223
|
+
className="text-xs text-accent hover:underline mt-1"
|
|
224
|
+
>
|
|
225
|
+
{isExpanded ? 'Show less' : 'Show more'}
|
|
226
|
+
</button>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Progressive Disclosure
|
|
234
|
+
```tsx
|
|
235
|
+
// Show summary, expand for details
|
|
236
|
+
function DataCard({ summary, details }) {
|
|
237
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="p-4 bg-surface-raised rounded-lg">
|
|
241
|
+
{/* Always visible summary */}
|
|
242
|
+
<div className="flex items-center justify-between">
|
|
243
|
+
<div>
|
|
244
|
+
<h4 className="text-sm font-medium text-white">{summary.title}</h4>
|
|
245
|
+
<p className="text-xs text-text-muted">{summary.subtitle}</p>
|
|
246
|
+
</div>
|
|
247
|
+
<button
|
|
248
|
+
onClick={() => setShowDetails(!showDetails)}
|
|
249
|
+
className="text-xs text-accent"
|
|
250
|
+
>
|
|
251
|
+
{showDetails ? 'Less' : 'More'}
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Expandable details */}
|
|
256
|
+
{showDetails && (
|
|
257
|
+
<div className="mt-3 pt-3 border-t border-border text-xs space-y-2">
|
|
258
|
+
{details.map((d, i) => (
|
|
259
|
+
<div key={i} className="flex justify-between">
|
|
260
|
+
<span className="text-text-muted">{d.label}</span>
|
|
261
|
+
<span className="text-white">{d.value}</span>
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Dense Grid Layouts
|
|
272
|
+
|
|
273
|
+
### Responsive Dense Grid
|
|
274
|
+
```tsx
|
|
275
|
+
// Auto-fit columns based on available space
|
|
276
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2">
|
|
277
|
+
{items.map(item => (
|
|
278
|
+
<DenseCard key={item.id} item={item} />
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
// Fixed minimum width columns
|
|
283
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))' }}>
|
|
284
|
+
{items.map(item => (
|
|
285
|
+
<DenseCard key={item.id} item={item} />
|
|
286
|
+
))}
|
|
287
|
+
</div>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Dense Stats Grid
|
|
291
|
+
```tsx
|
|
292
|
+
function StatsGrid({ stats }) {
|
|
293
|
+
return (
|
|
294
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-1">
|
|
295
|
+
{stats.map((stat) => (
|
|
296
|
+
<div
|
|
297
|
+
key={stat.label}
|
|
298
|
+
className="p-2 bg-surface-raised/50 rounded text-center"
|
|
299
|
+
>
|
|
300
|
+
<div className="text-lg font-bold text-white font-mono">
|
|
301
|
+
{stat.value}
|
|
302
|
+
</div>
|
|
303
|
+
<div className="text-[10px] text-text-muted uppercase tracking-wide">
|
|
304
|
+
{stat.label}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Dense Tables
|
|
314
|
+
|
|
315
|
+
### Compact Table Pattern
|
|
316
|
+
```tsx
|
|
317
|
+
function DenseTable({ columns, data }) {
|
|
318
|
+
return (
|
|
319
|
+
<div className="overflow-x-auto">
|
|
320
|
+
<table className="w-full text-xs">
|
|
321
|
+
<thead>
|
|
322
|
+
<tr className="bg-surface-raised">
|
|
323
|
+
{columns.map((col) => (
|
|
324
|
+
<th
|
|
325
|
+
key={col.key}
|
|
326
|
+
className="px-2 py-1.5 text-left font-medium text-text-muted uppercase tracking-wide whitespace-nowrap"
|
|
327
|
+
>
|
|
328
|
+
{col.label}
|
|
329
|
+
</th>
|
|
330
|
+
))}
|
|
331
|
+
</tr>
|
|
332
|
+
</thead>
|
|
333
|
+
<tbody className="divide-y divide-border/30">
|
|
334
|
+
{data.map((row, i) => (
|
|
335
|
+
<tr key={i} className="hover:bg-surface-raised/30">
|
|
336
|
+
{columns.map((col) => (
|
|
337
|
+
<td
|
|
338
|
+
key={col.key}
|
|
339
|
+
className="px-2 py-1.5 text-text-primary whitespace-nowrap"
|
|
340
|
+
>
|
|
341
|
+
{row[col.key]}
|
|
342
|
+
</td>
|
|
343
|
+
))}
|
|
344
|
+
</tr>
|
|
345
|
+
))}
|
|
346
|
+
</tbody>
|
|
347
|
+
</table>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Virtualized Long Lists
|
|
354
|
+
```tsx
|
|
355
|
+
// For lists with 100+ items, use virtualization
|
|
356
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
357
|
+
|
|
358
|
+
function VirtualizedList({ items, renderItem, itemHeight = 40 }) {
|
|
359
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
360
|
+
|
|
361
|
+
const virtualizer = useVirtualizer({
|
|
362
|
+
count: items.length,
|
|
363
|
+
getScrollElement: () => parentRef.current,
|
|
364
|
+
estimateSize: () => itemHeight,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<div
|
|
369
|
+
ref={parentRef}
|
|
370
|
+
className="h-[400px] overflow-auto"
|
|
371
|
+
>
|
|
372
|
+
<div
|
|
373
|
+
style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}
|
|
374
|
+
>
|
|
375
|
+
{virtualizer.getVirtualItems().map((virtualRow) => (
|
|
376
|
+
<div
|
|
377
|
+
key={virtualRow.key}
|
|
378
|
+
style={{
|
|
379
|
+
position: 'absolute',
|
|
380
|
+
top: 0,
|
|
381
|
+
left: 0,
|
|
382
|
+
width: '100%',
|
|
383
|
+
height: `${virtualRow.size}px`,
|
|
384
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
{renderItem(items[virtualRow.index])}
|
|
388
|
+
</div>
|
|
389
|
+
))}
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Navigation in Dense UIs
|
|
397
|
+
|
|
398
|
+
### Sticky Section Headers
|
|
399
|
+
```tsx
|
|
400
|
+
function StickySection({ title, children }) {
|
|
401
|
+
return (
|
|
402
|
+
<div className="relative">
|
|
403
|
+
<div className="sticky top-0 z-10 bg-surface-deep/95 backdrop-blur-sm py-2 -mx-4 px-4 border-b border-border">
|
|
404
|
+
<h3 className="text-sm font-semibold text-white">{title}</h3>
|
|
405
|
+
</div>
|
|
406
|
+
<div className="pt-3">
|
|
407
|
+
{children}
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Jump Navigation
|
|
415
|
+
```tsx
|
|
416
|
+
function JumpNav({ sections, activeSection, onJump }) {
|
|
417
|
+
return (
|
|
418
|
+
<nav className="sticky top-0 z-20 bg-surface-deep border-b border-border py-2 -mx-4 px-4 mb-4">
|
|
419
|
+
<div className="flex gap-1 overflow-x-auto scrollbar-none">
|
|
420
|
+
{sections.map((section) => (
|
|
421
|
+
<button
|
|
422
|
+
key={section.id}
|
|
423
|
+
onClick={() => onJump(section.id)}
|
|
424
|
+
className={`
|
|
425
|
+
px-2.5 py-1 text-xs font-medium rounded-md whitespace-nowrap
|
|
426
|
+
${activeSection === section.id
|
|
427
|
+
? 'bg-accent/20 text-accent'
|
|
428
|
+
: 'text-text-muted hover:text-white hover:bg-surface-raised'}
|
|
429
|
+
`}
|
|
430
|
+
>
|
|
431
|
+
{section.label}
|
|
432
|
+
</button>
|
|
433
|
+
))}
|
|
434
|
+
</div>
|
|
435
|
+
</nav>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Responsive Density Adjustments
|
|
441
|
+
|
|
442
|
+
### Breakpoint-Based Density
|
|
443
|
+
```tsx
|
|
444
|
+
// More dense on larger screens, less on mobile
|
|
445
|
+
const densityClasses = {
|
|
446
|
+
mobile: 'p-4 space-y-4 text-sm', // Looser spacing
|
|
447
|
+
desktop: 'p-2 space-y-2 text-xs', // Tighter spacing
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
<div className={`
|
|
451
|
+
${densityClasses.mobile}
|
|
452
|
+
lg:${densityClasses.desktop}
|
|
453
|
+
`}>
|
|
454
|
+
{content}
|
|
455
|
+
</div>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Hide Secondary Info on Mobile
|
|
459
|
+
```tsx
|
|
460
|
+
<div className="flex items-center gap-4">
|
|
461
|
+
<span className="text-white">{primary}</span>
|
|
462
|
+
<span className="hidden sm:inline text-text-muted">{secondary}</span>
|
|
463
|
+
<span className="hidden lg:inline text-text-muted">{tertiary}</span>
|
|
464
|
+
</div>
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Audit Checklist for Dense UIs
|
|
468
|
+
|
|
469
|
+
### Critical (Must Fix)
|
|
470
|
+
- [ ] Z-index values follow established scale - elements hidden/overlapping
|
|
471
|
+
- [ ] Modals/dialogs appear above all content - blocked interactions
|
|
472
|
+
- [ ] All content accessible via scroll - data inaccessible
|
|
473
|
+
- [ ] Text meets minimum size (12px body, 10px labels) - unreadable
|
|
474
|
+
- [ ] Sufficient contrast ratios maintained - accessibility violation
|
|
475
|
+
|
|
476
|
+
### Major (Should Fix)
|
|
477
|
+
- [ ] Fixed elements don't overlap scrollable content - content hidden
|
|
478
|
+
- [ ] Dropdowns/popovers don't get clipped - unusable controls
|
|
479
|
+
- [ ] No horizontal scroll unless intentional (tables) - poor mobile UX
|
|
480
|
+
- [ ] Tooltips/popovers stay within viewport - information cut off
|
|
481
|
+
- [ ] Mobile safe areas respected - notch/home bar overlap
|
|
482
|
+
- [ ] Adequate spacing between elements - hard to tap/click
|
|
483
|
+
- [ ] Long lists are virtualized (100+ items) - performance issues
|
|
484
|
+
- [ ] No layout shift during loading - disorienting jumps
|
|
485
|
+
|
|
486
|
+
### Minor (Nice to Have)
|
|
487
|
+
- [ ] Line height appropriate for text size - readability polish
|
|
488
|
+
- [ ] Jump navigation for long pages - convenience
|
|
489
|
+
- [ ] Section headers are sticky - navigation convenience
|
|
490
|
+
- [ ] Current section is highlighted - orientation
|
|
491
|
+
- [ ] Scroll position preserved on back navigation - continuity
|
|
492
|
+
- [ ] Images are lazy loaded - performance optimization
|
|
493
|
+
- [ ] Animations are reduced in dense areas - performance polish
|