@appforgeapps/uiforge 0.5.0 → 0.5.2
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 +512 -1
- package/dist/index.d.ts +426 -2
- package/dist/uiforge.cjs +5 -5
- package/dist/uiforge.css +1 -1
- package/dist/uiforge.js +2246 -1464
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -450,7 +450,7 @@ import { Button } from '@appforgeapps/uiforge'
|
|
|
450
450
|
|
|
451
451
|
### Button
|
|
452
452
|
|
|
453
|
-
A customizable button component with multiple variants and
|
|
453
|
+
A customizable button component with multiple variants, sizes, and accessibility-focused touch targets.
|
|
454
454
|
|
|
455
455
|
```tsx
|
|
456
456
|
import { Button } from '@appforgeapps/uiforge'
|
|
@@ -467,8 +467,135 @@ import { Button } from '@appforgeapps/uiforge'
|
|
|
467
467
|
|
|
468
468
|
// Disabled state
|
|
469
469
|
<Button disabled>Disabled</Button>
|
|
470
|
+
|
|
471
|
+
// Touch target density
|
|
472
|
+
// Default: 44×44px minimum for accessibility compliance
|
|
473
|
+
<Button>Accessible Touch Target</Button>
|
|
474
|
+
|
|
475
|
+
// Condensed: Smaller touch targets for dense UIs (not recommended for touch devices)
|
|
476
|
+
<Button density="condensed">Condensed</Button>
|
|
470
477
|
```
|
|
471
478
|
|
|
479
|
+
**Accessibility Features:**
|
|
480
|
+
|
|
481
|
+
- Default 44×44px minimum touch target for WCAG compliance
|
|
482
|
+
- Use `density="condensed"` only when space is critical and touch interaction is not primary
|
|
483
|
+
- Full keyboard support
|
|
484
|
+
- Focus visible styling
|
|
485
|
+
|
|
486
|
+
### HamburgerButton
|
|
487
|
+
|
|
488
|
+
An accessible hamburger menu button for controlling drawer/menu components.
|
|
489
|
+
|
|
490
|
+
```tsx
|
|
491
|
+
import { HamburgerButton, UIForgeSidebar } from '@appforgeapps/uiforge'
|
|
492
|
+
|
|
493
|
+
function Navigation() {
|
|
494
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<>
|
|
498
|
+
<HamburgerButton
|
|
499
|
+
isOpen={isOpen}
|
|
500
|
+
controlsId="main-drawer"
|
|
501
|
+
ariaLabel="Toggle navigation menu"
|
|
502
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
503
|
+
/>
|
|
504
|
+
<UIForgeSidebar
|
|
505
|
+
id="main-drawer"
|
|
506
|
+
variant="drawer"
|
|
507
|
+
open={isOpen}
|
|
508
|
+
onOpenChange={setIsOpen}
|
|
509
|
+
>
|
|
510
|
+
<nav>Navigation content</nav>
|
|
511
|
+
</UIForgeSidebar>
|
|
512
|
+
</>
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**Props:**
|
|
518
|
+
|
|
519
|
+
| Prop | Type | Default | Description |
|
|
520
|
+
| ------------ | ------------------------- | --------------- | ---------------------------------------- |
|
|
521
|
+
| `isOpen` | `boolean` | required | Whether the controlled menu/drawer is open |
|
|
522
|
+
| `controlsId` | `string` | required | ID of the element this button controls |
|
|
523
|
+
| `ariaLabel` | `string` | `"Toggle menu"` | Accessible label for the button |
|
|
524
|
+
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Size variant of the button |
|
|
525
|
+
|
|
526
|
+
**Accessibility Features:**
|
|
527
|
+
|
|
528
|
+
- Proper `aria-expanded` attribute reflecting open state
|
|
529
|
+
- `aria-controls` attribute linking to the controlled element
|
|
530
|
+
- 44×44px minimum touch target by default
|
|
531
|
+
- Animated hamburger-to-X transformation for visual feedback
|
|
532
|
+
- Focus visible styling
|
|
533
|
+
|
|
534
|
+
### UIForgeSidebar
|
|
535
|
+
|
|
536
|
+
A reusable sidebar component with multiple variants: static, drawer, and bottom sheet.
|
|
537
|
+
|
|
538
|
+
```tsx
|
|
539
|
+
import { UIForgeSidebar } from '@appforgeapps/uiforge'
|
|
540
|
+
|
|
541
|
+
// Static sidebar (always visible)
|
|
542
|
+
<UIForgeSidebar variant="static" width="280px">
|
|
543
|
+
<nav>Navigation</nav>
|
|
544
|
+
</UIForgeSidebar>
|
|
545
|
+
|
|
546
|
+
// Drawer sidebar (slide-in panel)
|
|
547
|
+
<UIForgeSidebar
|
|
548
|
+
variant="drawer"
|
|
549
|
+
open={isOpen}
|
|
550
|
+
onOpenChange={setIsOpen}
|
|
551
|
+
position="left"
|
|
552
|
+
>
|
|
553
|
+
<nav>Mobile navigation</nav>
|
|
554
|
+
</UIForgeSidebar>
|
|
555
|
+
|
|
556
|
+
// Bottom sheet
|
|
557
|
+
<UIForgeSidebar
|
|
558
|
+
variant="bottom"
|
|
559
|
+
open={isOpen}
|
|
560
|
+
onOpenChange={setIsOpen}
|
|
561
|
+
height="300px"
|
|
562
|
+
>
|
|
563
|
+
<div>Bottom sheet content</div>
|
|
564
|
+
</UIForgeSidebar>
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Props Reference:**
|
|
568
|
+
|
|
569
|
+
| Prop | Type | Default | Description |
|
|
570
|
+
| ---------------------- | --------------------------------- | ------------------------ | -------------------------------------- |
|
|
571
|
+
| `variant` | `'static' \| 'drawer' \| 'bottom'` | `'static'` | Sidebar variant |
|
|
572
|
+
| `open` | `boolean` | `true` | Whether sidebar is open (drawer/bottom) |
|
|
573
|
+
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
|
|
574
|
+
| `position` | `'left' \| 'right'` | `'left'` | Position (static/drawer variants) |
|
|
575
|
+
| `width` | `string` | `'280px'` | Width (static/drawer variants) |
|
|
576
|
+
| `height` | `string` | `'200px'` | Height (bottom variant only) |
|
|
577
|
+
| `showBackdrop` | `boolean` | `true` | Show backdrop overlay (drawer/bottom) |
|
|
578
|
+
| `closeOnBackdropClick` | `boolean` | `true` | Close on backdrop click |
|
|
579
|
+
| `closeOnEscape` | `boolean` | `true` | Close on ESC key press |
|
|
580
|
+
| `trapFocus` | `boolean` | `true` | Trap focus within sidebar (drawer/bottom) |
|
|
581
|
+
| `ariaLabel` | `string` | `'Sidebar navigation'` | ARIA label for accessibility |
|
|
582
|
+
| `className` | `string` | - | Additional CSS class names |
|
|
583
|
+
|
|
584
|
+
**Keyboard Interactions:**
|
|
585
|
+
|
|
586
|
+
- `Escape` - Close the drawer/bottom sheet
|
|
587
|
+
- `Tab` - Navigate through focusable elements (focus is trapped within the drawer when open)
|
|
588
|
+
- `Shift + Tab` - Navigate backwards through focusable elements
|
|
589
|
+
|
|
590
|
+
**Accessibility Features:**
|
|
591
|
+
|
|
592
|
+
- `role="dialog"` and `aria-modal="true"` for drawer/bottom variants
|
|
593
|
+
- Focus trapping prevents tab navigation outside the drawer when open
|
|
594
|
+
- Focus returns to the triggering element when drawer closes
|
|
595
|
+
- ESC key closes the drawer
|
|
596
|
+
- Backdrop click closes the drawer
|
|
597
|
+
- Body scroll is disabled when drawer is open
|
|
598
|
+
|
|
472
599
|
### UIForgeBlocksEditor
|
|
473
600
|
|
|
474
601
|
A rich, block-based content editor for flexible layouts and content creation.
|
|
@@ -1112,6 +1239,390 @@ import { UIForgeVideo, UIForgeVideoPreview } from '@appforgeapps/uiforge'
|
|
|
1112
1239
|
|
|
1113
1240
|
See `examples/VideoExample.tsx` for a complete interactive demo with multiple examples.
|
|
1114
1241
|
|
|
1242
|
+
## Mobile Responsiveness
|
|
1243
|
+
|
|
1244
|
+
UIForge components are designed with mobile-first responsive behavior built in. Components automatically adapt to different screen sizes using **container-based responsiveness** rather than viewport-based media queries. This allows components to respond to their container's width, making them work seamlessly in sidebars, modals, or any constrained layout.
|
|
1245
|
+
|
|
1246
|
+
### Responsive Hooks
|
|
1247
|
+
|
|
1248
|
+
UIForge provides two powerful hooks for building responsive layouts:
|
|
1249
|
+
|
|
1250
|
+
#### useResponsive
|
|
1251
|
+
|
|
1252
|
+
A container-width based responsive helper that determines whether a container is "compact" by measuring its width.
|
|
1253
|
+
|
|
1254
|
+
```tsx
|
|
1255
|
+
import { useRef } from 'react'
|
|
1256
|
+
import { useResponsive } from '@appforgeapps/uiforge'
|
|
1257
|
+
|
|
1258
|
+
function ResponsiveCard() {
|
|
1259
|
+
const cardRef = useRef<HTMLDivElement>(null)
|
|
1260
|
+
const isCompact = useResponsive(cardRef, 640) // default breakpoint is 640px
|
|
1261
|
+
|
|
1262
|
+
return (
|
|
1263
|
+
<div ref={cardRef} className="card">
|
|
1264
|
+
{isCompact ? (
|
|
1265
|
+
<div>Mobile Layout - Stack vertically</div>
|
|
1266
|
+
) : (
|
|
1267
|
+
<div>Desktop Layout - Side by side</div>
|
|
1268
|
+
)}
|
|
1269
|
+
</div>
|
|
1270
|
+
)
|
|
1271
|
+
}
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
**Key Benefits:**
|
|
1275
|
+
- Container-aware: responds to container width, not just window width
|
|
1276
|
+
- Works in any context: sidebars, modals, grid cells, etc.
|
|
1277
|
+
- Uses ResizeObserver for efficient updates
|
|
1278
|
+
- SSR-safe with sensible defaults
|
|
1279
|
+
|
|
1280
|
+
#### useDynamicPageCount
|
|
1281
|
+
|
|
1282
|
+
Automatically calculates optimal page size for paginated lists based on container height and item measurements.
|
|
1283
|
+
|
|
1284
|
+
```tsx
|
|
1285
|
+
import { useRef } from 'react'
|
|
1286
|
+
import { useDynamicPageCount } from '@appforgeapps/uiforge'
|
|
1287
|
+
|
|
1288
|
+
function PaginatedList({ items }) {
|
|
1289
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
1290
|
+
const pageSize = useDynamicPageCount(containerRef, {
|
|
1291
|
+
sampleCount: 3, // Measure first 3 items
|
|
1292
|
+
min: 5, // Show at least 5 items
|
|
1293
|
+
max: 20, // Show at most 20 items
|
|
1294
|
+
approxItemHeight: 100
|
|
1295
|
+
})
|
|
1296
|
+
|
|
1297
|
+
return (
|
|
1298
|
+
<div ref={containerRef} style={{ height: '600px', overflow: 'auto' }}>
|
|
1299
|
+
{items.slice(0, pageSize).map(item => (
|
|
1300
|
+
<ListItem key={item.id} {...item} />
|
|
1301
|
+
))}
|
|
1302
|
+
</div>
|
|
1303
|
+
)
|
|
1304
|
+
}
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
See `examples/UseDynamicPageCountExample.tsx` for a complete demo.
|
|
1308
|
+
|
|
1309
|
+
### Responsive Components
|
|
1310
|
+
|
|
1311
|
+
Several UIForge components have built-in responsive behavior:
|
|
1312
|
+
|
|
1313
|
+
#### ActivityStream Responsive Features
|
|
1314
|
+
|
|
1315
|
+
The `UIForgeActivityStream` component automatically adapts to narrow containers:
|
|
1316
|
+
|
|
1317
|
+
```tsx
|
|
1318
|
+
import { UIForgeActivityStream } from '@appforgeapps/uiforge'
|
|
1319
|
+
|
|
1320
|
+
function ActivityFeed() {
|
|
1321
|
+
return (
|
|
1322
|
+
<UIForgeActivityStream
|
|
1323
|
+
events={events}
|
|
1324
|
+
|
|
1325
|
+
// Density modes
|
|
1326
|
+
density="comfortable" // 'comfortable' | 'compact' | 'condensed'
|
|
1327
|
+
|
|
1328
|
+
// Automatic responsive behavior (enabled by default)
|
|
1329
|
+
responsive={true} // Auto-switch to compact on narrow containers
|
|
1330
|
+
compactBreakpointPx={640} // Threshold for switching (default: 640px)
|
|
1331
|
+
|
|
1332
|
+
// Control metadata visibility
|
|
1333
|
+
showMeta={true} // Show/hide timestamps and descriptions
|
|
1334
|
+
|
|
1335
|
+
// Virtualization for large lists
|
|
1336
|
+
virtualization={false} // Enable for 100+ items
|
|
1337
|
+
virtualItemHeight={48} // Item height when virtualized
|
|
1338
|
+
/>
|
|
1339
|
+
)
|
|
1340
|
+
}
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
**Density Modes:**
|
|
1344
|
+
- `comfortable`: Default spacing with full metadata (desktop)
|
|
1345
|
+
- `compact`: Reduced spacing, ideal for tablets and narrow screens
|
|
1346
|
+
- `condensed`: Minimal spacing for maximum information density
|
|
1347
|
+
|
|
1348
|
+
**Responsive Behavior:**
|
|
1349
|
+
When `responsive={true}` (default), the component automatically switches from `comfortable` to `compact` density when the container width falls below `compactBreakpointPx`.
|
|
1350
|
+
|
|
1351
|
+
**Example Use Cases:**
|
|
1352
|
+
- Main content area: Use `comfortable` density with responsive switching
|
|
1353
|
+
- Sidebar panel: Use `compact` density or enable responsive
|
|
1354
|
+
- Dashboard widget: Use `condensed` for maximum information density
|
|
1355
|
+
- Large datasets: Enable `virtualization` for 100+ items
|
|
1356
|
+
|
|
1357
|
+
```tsx
|
|
1358
|
+
// Sidebar example - always compact
|
|
1359
|
+
<UIForgeActivityStream
|
|
1360
|
+
events={events}
|
|
1361
|
+
density="compact"
|
|
1362
|
+
responsive={false}
|
|
1363
|
+
/>
|
|
1364
|
+
|
|
1365
|
+
// Responsive main feed - adapts automatically
|
|
1366
|
+
<UIForgeActivityStream
|
|
1367
|
+
events={events}
|
|
1368
|
+
density="comfortable"
|
|
1369
|
+
responsive={true}
|
|
1370
|
+
compactBreakpointPx={640}
|
|
1371
|
+
/>
|
|
1372
|
+
|
|
1373
|
+
// Large list with virtualization
|
|
1374
|
+
<UIForgeActivityStream
|
|
1375
|
+
events={manyEvents}
|
|
1376
|
+
virtualization={true}
|
|
1377
|
+
virtualItemHeight={48}
|
|
1378
|
+
maxHeight="600px"
|
|
1379
|
+
density="compact"
|
|
1380
|
+
/>
|
|
1381
|
+
```
|
|
1382
|
+
|
|
1383
|
+
See `examples/ActivityStreamExample.tsx` for an interactive demo with density controls.
|
|
1384
|
+
|
|
1385
|
+
#### Sidebar Responsive Variants
|
|
1386
|
+
|
|
1387
|
+
The `UIForgeSidebar` component provides three variants optimized for different screen sizes:
|
|
1388
|
+
|
|
1389
|
+
```tsx
|
|
1390
|
+
import { useState } from 'react'
|
|
1391
|
+
import { UIForgeSidebar, HamburgerButton } from '@appforgeapps/uiforge'
|
|
1392
|
+
|
|
1393
|
+
function ResponsiveLayout() {
|
|
1394
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
1395
|
+
|
|
1396
|
+
return (
|
|
1397
|
+
<>
|
|
1398
|
+
{/* Desktop: Static sidebar */}
|
|
1399
|
+
<div className="desktop-only">
|
|
1400
|
+
<UIForgeSidebar variant="static" width="280px">
|
|
1401
|
+
<nav>Navigation items</nav>
|
|
1402
|
+
</UIForgeSidebar>
|
|
1403
|
+
</div>
|
|
1404
|
+
|
|
1405
|
+
{/* Mobile: Drawer sidebar */}
|
|
1406
|
+
<div className="mobile-only">
|
|
1407
|
+
<HamburgerButton
|
|
1408
|
+
isOpen={mobileMenuOpen}
|
|
1409
|
+
controlsId="mobile-nav"
|
|
1410
|
+
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
1411
|
+
/>
|
|
1412
|
+
<UIForgeSidebar
|
|
1413
|
+
id="mobile-nav"
|
|
1414
|
+
variant="drawer"
|
|
1415
|
+
open={mobileMenuOpen}
|
|
1416
|
+
onOpenChange={setMobileMenuOpen}
|
|
1417
|
+
position="left"
|
|
1418
|
+
>
|
|
1419
|
+
<nav>Navigation items</nav>
|
|
1420
|
+
</UIForgeSidebar>
|
|
1421
|
+
</div>
|
|
1422
|
+
|
|
1423
|
+
{/* Mobile: Bottom sheet variant */}
|
|
1424
|
+
<UIForgeSidebar
|
|
1425
|
+
variant="bottom"
|
|
1426
|
+
open={bottomSheetOpen}
|
|
1427
|
+
onOpenChange={setBottomSheetOpen}
|
|
1428
|
+
height="300px"
|
|
1429
|
+
>
|
|
1430
|
+
<div>Bottom sheet content</div>
|
|
1431
|
+
</UIForgeSidebar>
|
|
1432
|
+
</>
|
|
1433
|
+
)
|
|
1434
|
+
}
|
|
1435
|
+
```
|
|
1436
|
+
|
|
1437
|
+
**Sidebar Variants:**
|
|
1438
|
+
- `static`: Always visible, takes up layout space (desktop)
|
|
1439
|
+
- `drawer`: Slide-in overlay panel (mobile/tablet)
|
|
1440
|
+
- `bottom`: Bottom sheet for mobile actions
|
|
1441
|
+
|
|
1442
|
+
See `examples/SidebarExample.tsx` for complete examples of all variants.
|
|
1443
|
+
|
|
1444
|
+
#### Video Player Responsive Behavior
|
|
1445
|
+
|
|
1446
|
+
The `UIForgeVideo` component automatically adjusts its aspect ratio and embed behavior for different screen sizes:
|
|
1447
|
+
|
|
1448
|
+
```tsx
|
|
1449
|
+
import { UIForgeVideo } from '@appforgeapps/uiforge'
|
|
1450
|
+
|
|
1451
|
+
function VideoSection() {
|
|
1452
|
+
return (
|
|
1453
|
+
<UIForgeVideo
|
|
1454
|
+
url="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
1455
|
+
aspectRatio="16:9" // Maintains aspect ratio on all devices
|
|
1456
|
+
controls={true}
|
|
1457
|
+
autoplay={false}
|
|
1458
|
+
/>
|
|
1459
|
+
)
|
|
1460
|
+
}
|
|
1461
|
+
```
|
|
1462
|
+
|
|
1463
|
+
The video player automatically:
|
|
1464
|
+
- Maintains aspect ratio on all screen sizes
|
|
1465
|
+
- Uses responsive embed containers
|
|
1466
|
+
- Adapts controls for touch devices
|
|
1467
|
+
- Handles safe-area insets on mobile devices
|
|
1468
|
+
|
|
1469
|
+
### Best Practices for Responsive Design
|
|
1470
|
+
|
|
1471
|
+
1. **Use Container Queries**: Leverage `useResponsive` hook instead of viewport-based media queries for components that may appear in different contexts (sidebars, modals, grid cells).
|
|
1472
|
+
|
|
1473
|
+
2. **Choose Appropriate Density**:
|
|
1474
|
+
- Desktop/wide layouts: `comfortable`
|
|
1475
|
+
- Tablet/medium layouts: `compact`
|
|
1476
|
+
- Mobile/narrow layouts: `compact` or `condensed`
|
|
1477
|
+
- Enable `responsive={true}` to automatically switch
|
|
1478
|
+
|
|
1479
|
+
3. **Virtualize Large Lists**: Enable virtualization for ActivityStream with 100+ items to maintain smooth scrolling on mobile devices.
|
|
1480
|
+
|
|
1481
|
+
4. **Test in Constrained Contexts**: Components should work well in sidebars, modals, and grid cells, not just full-width layouts.
|
|
1482
|
+
|
|
1483
|
+
5. **Consider Touch Targets**: UIForge components follow WCAG guidelines with minimum 44×44px touch targets by default.
|
|
1484
|
+
|
|
1485
|
+
### Migration from Viewport-Based Responsive Design
|
|
1486
|
+
|
|
1487
|
+
If you're currently using viewport-based media queries, here's how to migrate to container-based responsiveness:
|
|
1488
|
+
|
|
1489
|
+
**Before (viewport-based):**
|
|
1490
|
+
```tsx
|
|
1491
|
+
function MyComponent() {
|
|
1492
|
+
const isMobile = window.innerWidth < 768
|
|
1493
|
+
|
|
1494
|
+
return (
|
|
1495
|
+
<div>
|
|
1496
|
+
{isMobile ? <MobileView /> : <DesktopView />}
|
|
1497
|
+
</div>
|
|
1498
|
+
)
|
|
1499
|
+
}
|
|
1500
|
+
```
|
|
1501
|
+
|
|
1502
|
+
**After (container-based):**
|
|
1503
|
+
```tsx
|
|
1504
|
+
import { useRef } from 'react'
|
|
1505
|
+
import { useResponsive } from '@appforgeapps/uiforge'
|
|
1506
|
+
|
|
1507
|
+
function MyComponent() {
|
|
1508
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
1509
|
+
const isCompact = useResponsive(containerRef, 768)
|
|
1510
|
+
|
|
1511
|
+
return (
|
|
1512
|
+
<div ref={containerRef}>
|
|
1513
|
+
{isCompact ? <MobileView /> : <DesktopView />}
|
|
1514
|
+
</div>
|
|
1515
|
+
)
|
|
1516
|
+
}
|
|
1517
|
+
```
|
|
1518
|
+
|
|
1519
|
+
This approach ensures your component adapts to its container, making it reusable in different contexts like sidebars, modals, or grid layouts.
|
|
1520
|
+
|
|
1521
|
+
## Hooks
|
|
1522
|
+
|
|
1523
|
+
### useResponsive
|
|
1524
|
+
|
|
1525
|
+
A container-width based responsive helper hook that determines whether a container element is "compact" by measuring its width with a `ResizeObserver`. This allows components to adapt to the width of their container rather than the global `window.innerWidth`.
|
|
1526
|
+
|
|
1527
|
+
```tsx
|
|
1528
|
+
import { useRef } from 'react'
|
|
1529
|
+
import { useResponsive } from '@appforgeapps/uiforge'
|
|
1530
|
+
|
|
1531
|
+
function ResponsiveComponent() {
|
|
1532
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
1533
|
+
|
|
1534
|
+
// Returns true when container width < 640px
|
|
1535
|
+
const isCompact = useResponsive(containerRef, 640)
|
|
1536
|
+
|
|
1537
|
+
return (
|
|
1538
|
+
<div ref={containerRef}>
|
|
1539
|
+
{isCompact ? (
|
|
1540
|
+
<MobileLayout />
|
|
1541
|
+
) : (
|
|
1542
|
+
<DesktopLayout />
|
|
1543
|
+
)}
|
|
1544
|
+
</div>
|
|
1545
|
+
)
|
|
1546
|
+
}
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
**Features:**
|
|
1550
|
+
|
|
1551
|
+
- **Container-based** - Responds to container width, not viewport width
|
|
1552
|
+
- **ResizeObserver** - Efficient observation of element size changes
|
|
1553
|
+
- **SSR Safe** - Returns `false` by default when ref is null
|
|
1554
|
+
- **Customizable** - Specify any breakpoint in pixels
|
|
1555
|
+
|
|
1556
|
+
**API Reference:**
|
|
1557
|
+
|
|
1558
|
+
| Parameter | Type | Default | Description |
|
|
1559
|
+
| -------------- | ---------------------------------------- | ------- | ------------------------------------------ |
|
|
1560
|
+
| `containerRef` | `RefObject<HTMLElement \| null> \| null` | - | Ref to the container element to observe |
|
|
1561
|
+
| `breakpointPx` | `number` | `640` | Width threshold in pixels |
|
|
1562
|
+
|
|
1563
|
+
**Returns:** `boolean` - `true` when `containerRef.current.clientWidth < breakpointPx`, `false` otherwise.
|
|
1564
|
+
|
|
1565
|
+
See `examples/UseResponsiveExample.tsx` for a complete interactive demo.
|
|
1566
|
+
|
|
1567
|
+
### useDynamicPageCount
|
|
1568
|
+
|
|
1569
|
+
A hook that dynamically calculates the optimal page size for paginated lists based on the container's available height and measured item heights. This ensures you always show the right number of items to fill the viewport without excessive scrolling.
|
|
1570
|
+
|
|
1571
|
+
```tsx
|
|
1572
|
+
import { useRef } from 'react'
|
|
1573
|
+
import { useDynamicPageCount } from '@appforgeapps/uiforge'
|
|
1574
|
+
|
|
1575
|
+
function DynamicPaginatedList({ items }) {
|
|
1576
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
1577
|
+
|
|
1578
|
+
const pageSize = useDynamicPageCount(containerRef, {
|
|
1579
|
+
sampleCount: 3, // Number of items to measure for height
|
|
1580
|
+
min: 3, // Minimum page size
|
|
1581
|
+
max: 15, // Maximum page size
|
|
1582
|
+
approxItemHeight: 120 // Fallback height when items aren't rendered
|
|
1583
|
+
})
|
|
1584
|
+
|
|
1585
|
+
return (
|
|
1586
|
+
<div ref={containerRef} style={{ height: '600px', overflow: 'auto' }}>
|
|
1587
|
+
{items.slice(0, pageSize).map(item => (
|
|
1588
|
+
<ListItem key={item.id} {...item} />
|
|
1589
|
+
))}
|
|
1590
|
+
{items.length > pageSize && (
|
|
1591
|
+
<button onClick={loadMore}>Load More</button>
|
|
1592
|
+
)}
|
|
1593
|
+
</div>
|
|
1594
|
+
)
|
|
1595
|
+
}
|
|
1596
|
+
```
|
|
1597
|
+
|
|
1598
|
+
**Features:**
|
|
1599
|
+
|
|
1600
|
+
- **Dynamic Calculation** - Automatically recalculates when container resizes
|
|
1601
|
+
- **Smart Sampling** - Measures actual rendered items for accurate height estimation
|
|
1602
|
+
- **Responsive** - Adapts to different screen sizes and orientations
|
|
1603
|
+
- **Performant** - Uses ResizeObserver and MutationObserver efficiently
|
|
1604
|
+
- **Configurable Bounds** - Set min/max limits to control page sizes
|
|
1605
|
+
|
|
1606
|
+
**API Reference:**
|
|
1607
|
+
|
|
1608
|
+
| Parameter | Type | Default | Description |
|
|
1609
|
+
| -------------------- | ---------------------------------------- | ------- | ------------------------------------------------ |
|
|
1610
|
+
| `containerRef` | `RefObject<HTMLElement \| null> \| null` | - | Ref to the scrollable container element |
|
|
1611
|
+
| `options.sampleCount`| `number` | `3` | Number of items to measure for average height |
|
|
1612
|
+
| `options.min` | `number` | `3` | Minimum page size to return |
|
|
1613
|
+
| `options.max` | `number` | `15` | Maximum page size to return |
|
|
1614
|
+
| `options.approxItemHeight` | `number` | `120` | Approximate item height for fallback calculations|
|
|
1615
|
+
|
|
1616
|
+
**Returns:** `number` - The calculated page size, clamped to `[min, max]` range.
|
|
1617
|
+
|
|
1618
|
+
**Use Cases:**
|
|
1619
|
+
- Activity feeds with variable-height items
|
|
1620
|
+
- Product listings with images
|
|
1621
|
+
- Search results with descriptions
|
|
1622
|
+
- Any paginated content where you want to fill the viewport optimally
|
|
1623
|
+
|
|
1624
|
+
See `examples/UseDynamicPageCountExample.tsx` for a complete interactive demo.
|
|
1625
|
+
|
|
1115
1626
|
## Theming
|
|
1116
1627
|
|
|
1117
1628
|
UIForge components support comprehensive theming through CSS variables. See [THEMING.md](./THEMING.md) for a complete guide on:
|