@appforgeapps/uiforge 0.1.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 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 sizes.
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: