@appforgeapps/uiforge 0.5.5 → 0.5.6

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
@@ -1711,8 +1711,25 @@ UIForge components support comprehensive theming through CSS variables. See [THE
1711
1711
  - CSS variable reference
1712
1712
  - System preference detection
1713
1713
  - Advanced customization techniques
1714
+ - **Example app themes** - Learn from real-world examples like the NexaLive music streaming app theme
1714
1715
 
1715
- Quick example:
1716
+ ### Example App Themes
1717
+
1718
+ UIForge provides concrete example themes in `examples/themes/` to demonstrate how to create custom themes for real applications:
1719
+
1720
+ - **NexaLive Theme** (`examples/themes/nexalive-theme.css`) - A purple/pink themed music streaming app example based on the [chriscase/nexalive](https://github.com/chriscase/nexalive) project
1721
+
1722
+ **Important:** Example themes are for **demonstration only**. Copy them into your own application and customize the colors to match your brand. See [THEMING.md - Example App Themes](./THEMING.md#example-app-themes) for detailed adoption instructions.
1723
+
1724
+ Quick example of import order:
1725
+
1726
+ ```tsx
1727
+ // Import UIForge core styles FIRST, then your custom theme
1728
+ import '@appforgeapps/uiforge/styles.css'
1729
+ import './my-custom-theme.css' // Your customized copy of an example theme
1730
+ ```
1731
+
1732
+ Quick component theming example:
1716
1733
 
1717
1734
  ```tsx
1718
1735
  import { useState } from 'react'
@@ -1817,6 +1834,8 @@ Simply open this repository in GitHub Codespaces to get started immediately with
1817
1834
 
1818
1835
  Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines.
1819
1836
 
1837
+ **Important for Component Contributors:** UIForge is a library-first project. Before contributing a component, review the [Component Design Guidelines](./COMPONENT_GUIDELINES.md) to ensure your component remains generic, reusable, and composable.
1838
+
1820
1839
  Quick start:
1821
1840
 
1822
1841
  1. Fork the repository
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { default as default_2 } from 'react';
2
2
  import { FC } from 'react';
3
+ import { ImgHTMLAttributes } from 'react';
3
4
  import { JSX } from 'react/jsx-runtime';
4
5
  import { RefObject } from 'react';
5
6
 
@@ -571,6 +572,263 @@ export declare function isAdultContent(url: string): boolean;
571
572
 
572
573
  export declare const IssueIcon: default_2.FC<IconProps>;
573
574
 
575
+ /**
576
+ * A generic, reusable MediaCard component for displaying media-driven content items.
577
+ *
578
+ * Features:
579
+ * - Responsive layout: media left + body right (desktop), stacked (mobile)
580
+ * - Theme token integration for consistent spacing, typography, and elevation
581
+ * - Full accessibility support (alt text, focus states, ARIA labels, keyboard navigation)
582
+ * - Flexible customization via render props and slots
583
+ * - Multiple variants for different use cases
584
+ *
585
+ * @example
586
+ * ```tsx
587
+ * <MediaCard
588
+ * title="Song Title"
589
+ * subtitle="Artist Name"
590
+ * mediaUrl="/album-art.jpg"
591
+ * mediaAlt="Album artwork"
592
+ * meta={{ duration: "3:45", year: "2024" }}
593
+ * actions={<button>Play</button>}
594
+ * />
595
+ * ```
596
+ */
597
+ export declare const MediaCard: default_2.FC<MediaCardProps>;
598
+
599
+ /**
600
+ * Props for the MediaCard component
601
+ */
602
+ export declare interface MediaCardProps {
603
+ /**
604
+ * Main title text displayed prominently
605
+ */
606
+ title: string;
607
+ /**
608
+ * Secondary subtitle text
609
+ */
610
+ subtitle?: string;
611
+ /**
612
+ * URL or path to the media (image/video)
613
+ */
614
+ mediaUrl: string;
615
+ /**
616
+ * Alt text for the media (required for accessibility)
617
+ */
618
+ mediaAlt: string;
619
+ /**
620
+ * Optional metadata as key-value pairs
621
+ */
622
+ meta?: Record<string, string>;
623
+ /**
624
+ * Optional action buttons or elements to display
625
+ */
626
+ actions?: default_2.ReactNode;
627
+ /**
628
+ * Visual variant of the card
629
+ */
630
+ variant?: 'default' | 'compact' | 'featured';
631
+ /**
632
+ * Custom className for additional styling
633
+ */
634
+ className?: string;
635
+ /**
636
+ * Theme variant ('light' or 'dark')
637
+ */
638
+ theme?: 'light' | 'dark';
639
+ /**
640
+ * Optional custom body content (render prop)
641
+ */
642
+ renderBody?: () => default_2.ReactNode;
643
+ /**
644
+ * Optional custom footer content (render prop)
645
+ */
646
+ renderFooter?: () => default_2.ReactNode;
647
+ /**
648
+ * Click handler for the entire card
649
+ */
650
+ onClick?: () => void;
651
+ /**
652
+ * Makes the card focusable and keyboard-navigable
653
+ */
654
+ tabIndex?: number;
655
+ /**
656
+ * ARIA label for the card
657
+ */
658
+ ariaLabel?: string;
659
+ }
660
+
661
+ /**
662
+ * MediaListSkeleton - A loading skeleton component for lists of MediaCard components
663
+ *
664
+ * Features:
665
+ * - Configurable number of skeleton items
666
+ * - CSS shimmer animation for visual feedback
667
+ * - Respects `prefers-reduced-motion` for accessibility
668
+ * - Uses UIForge design tokens for consistent sizing
669
+ * - Theme support (light/dark)
670
+ * - SSR-friendly (no client-side JavaScript required)
671
+ *
672
+ * Perfect for:
673
+ * - Loading states while fetching media lists
674
+ * - Progressive loading experiences
675
+ * - Skeleton screens for any MediaCard-based lists
676
+ *
677
+ * @example
678
+ * ```tsx
679
+ * // Basic usage
680
+ * <MediaListSkeleton count={5} />
681
+ *
682
+ * // Dark theme
683
+ * <MediaListSkeleton count={3} theme="dark" />
684
+ *
685
+ * // In a conditional render
686
+ * {isLoading ? (
687
+ * <MediaListSkeleton count={6} />
688
+ * ) : (
689
+ * songList.map(song => <SongCard key={song.id} {...song} />)
690
+ * )}
691
+ * ```
692
+ */
693
+ export declare const MediaListSkeleton: default_2.FC<MediaListSkeletonProps>;
694
+
695
+ /**
696
+ * Props for the MediaListSkeleton component
697
+ */
698
+ export declare interface MediaListSkeletonProps {
699
+ /**
700
+ * Number of skeleton placeholders to render
701
+ * @default 3
702
+ */
703
+ count?: number;
704
+ /**
705
+ * Theme variant ('light' or 'dark')
706
+ * @default 'light'
707
+ */
708
+ theme?: 'light' | 'dark';
709
+ /**
710
+ * Custom className for additional styling
711
+ */
712
+ className?: string;
713
+ /**
714
+ * ARIA label for accessibility
715
+ * @default 'Loading media items'
716
+ */
717
+ ariaLabel?: string;
718
+ }
719
+
720
+ /**
721
+ * MediaPlaceholder - A placeholder component for missing or loading media
722
+ *
723
+ * Features:
724
+ * - Three display modes: icon, initials, gradient
725
+ * - Multiple size options
726
+ * - Customizable border radius
727
+ * - Theme support (light/dark)
728
+ * - Accessibility support with alt text
729
+ *
730
+ * Perfect for:
731
+ * - Missing profile pictures
732
+ * - Loading states for images
733
+ * - Default avatars
734
+ * - Fallback media for MediaCard
735
+ *
736
+ * @example
737
+ * ```tsx
738
+ * // Icon placeholder (default)
739
+ * <MediaPlaceholder alt="Profile picture" />
740
+ *
741
+ * // Initials placeholder
742
+ * <MediaPlaceholder
743
+ * type="initials"
744
+ * name="John Doe"
745
+ * size="large"
746
+ * borderRadius="full"
747
+ * alt="John Doe's avatar"
748
+ * />
749
+ *
750
+ * // Gradient placeholder
751
+ * <MediaPlaceholder
752
+ * type="gradient"
753
+ * gradientColor="purple"
754
+ * size="medium"
755
+ * alt="Album artwork placeholder"
756
+ * />
757
+ *
758
+ * // Custom icon
759
+ * <MediaPlaceholder
760
+ * type="icon"
761
+ * icon={<MusicIcon />}
762
+ * alt="Music placeholder"
763
+ * />
764
+ * ```
765
+ */
766
+ export declare const MediaPlaceholder: default_2.FC<MediaPlaceholderProps>;
767
+
768
+ /**
769
+ * Props for the MediaPlaceholder component
770
+ */
771
+ export declare interface MediaPlaceholderProps {
772
+ /**
773
+ * Type of placeholder to display
774
+ * - 'icon': Shows an icon (default)
775
+ * - 'initials': Shows initials from a name
776
+ * - 'gradient': Shows a gradient background
777
+ */
778
+ type?: 'icon' | 'initials' | 'gradient';
779
+ /**
780
+ * Icon to display (only used when type='icon')
781
+ * Can be any React node (e.g., SVG, emoji, or icon component)
782
+ */
783
+ icon?: default_2.ReactNode;
784
+ /**
785
+ * Name to extract initials from (only used when type='initials')
786
+ * Examples: "John Doe" → "JD", "Taylor Swift" → "TS"
787
+ */
788
+ name?: string;
789
+ /**
790
+ * Custom initials text (overrides automatic extraction from name)
791
+ */
792
+ initials?: string;
793
+ /**
794
+ * Gradient color scheme (only used when type='gradient')
795
+ * - 'blue': Blue gradient
796
+ * - 'purple': Purple gradient
797
+ * - 'green': Green gradient
798
+ * - 'orange': Orange gradient
799
+ * - 'pink': Pink gradient
800
+ */
801
+ gradientColor?: 'blue' | 'purple' | 'green' | 'orange' | 'pink';
802
+ /**
803
+ * Size of the placeholder
804
+ * - 'small': 64px
805
+ * - 'medium': 84px (default)
806
+ * - 'large': 120px
807
+ * - 'xlarge': 160px
808
+ */
809
+ size?: 'small' | 'medium' | 'large' | 'xlarge';
810
+ /**
811
+ * Border radius style
812
+ * - 'small': 4px
813
+ * - 'medium': 8px (default)
814
+ * - 'large': 12px
815
+ * - 'full': 50% (circular)
816
+ */
817
+ borderRadius?: 'small' | 'medium' | 'large' | 'full';
818
+ /**
819
+ * Custom className for additional styling
820
+ */
821
+ className?: string;
822
+ /**
823
+ * Alt text for accessibility (required for screen readers)
824
+ */
825
+ alt?: string;
826
+ /**
827
+ * Theme variant ('light' or 'dark')
828
+ */
829
+ theme?: 'light' | 'dark';
830
+ }
831
+
574
832
  export declare const MeditationIcon: default_2.FC<IconProps>;
575
833
 
576
834
  export declare const MeetingIcon: default_2.FC<IconProps>;
@@ -1714,6 +1972,180 @@ export declare interface UseDynamicPageCountOptions {
1714
1972
  approxItemHeight?: number;
1715
1973
  }
1716
1974
 
1975
+ /**
1976
+ * useOptimizedImage - A hook that provides optimized image attributes
1977
+ *
1978
+ * This hook helps you implement best practices for image loading and performance:
1979
+ *
1980
+ * **Performance Best Practices:**
1981
+ * 1. **Lazy Loading**: Use `loading="lazy"` for images below the fold
1982
+ * 2. **Async Decoding**: Use `decoding="async"` to avoid blocking the main thread
1983
+ * 3. **Responsive Images**: Use `srcSet` and `sizes` for different screen sizes
1984
+ * 4. **Aspect Ratio**: Use `aspectRatio` to prevent layout shift (CLS)
1985
+ * 5. **LQIP (Low Quality Image Placeholder)**: Consider using a blur-up technique
1986
+ *
1987
+ * **Recommended Image Sizes:**
1988
+ * - Thumbnails: 64px, 84px, 120px
1989
+ * - Cards: 320px, 480px, 640px
1990
+ * - Hero images: 1024px, 1280px, 1920px
1991
+ * - Always provide WebP/AVIF formats when possible
1992
+ *
1993
+ * **Accessibility:**
1994
+ * - Always provide meaningful `alt` text
1995
+ * - Use empty `alt=""` for decorative images
1996
+ * - Consider adding `title` for additional context
1997
+ *
1998
+ * **LQIP Guidance:**
1999
+ * For optimal user experience, consider implementing a Low Quality Image Placeholder (LQIP):
2000
+ * 1. Generate a tiny version of the image (e.g., 20px wide, base64 encoded)
2001
+ * 2. Display it as background while the full image loads
2002
+ * 3. Blur it with CSS `filter: blur(10px)` for a smooth appearance
2003
+ * 4. Fade in the full image when loaded
2004
+ *
2005
+ * @example
2006
+ * ```tsx
2007
+ * // Basic usage
2008
+ * function BasicImage() {
2009
+ * const { imgProps } = useOptimizedImage({
2010
+ * src: '/image.jpg',
2011
+ * alt: 'Product photo',
2012
+ * loading: 'lazy',
2013
+ * decoding: 'async'
2014
+ * })
2015
+ * return <img {...imgProps} />
2016
+ * }
2017
+ *
2018
+ * // Responsive images with srcSet
2019
+ * function ResponsiveImage() {
2020
+ * const { imgProps } = useOptimizedImage({
2021
+ * src: '/image-800w.jpg',
2022
+ * alt: 'Responsive image',
2023
+ * srcSet: '/image-400w.jpg 400w, /image-800w.jpg 800w, /image-1200w.jpg 1200w',
2024
+ * sizes: '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
2025
+ * loading: 'lazy',
2026
+ * decoding: 'async'
2027
+ * })
2028
+ * return <img {...imgProps} />
2029
+ * }
2030
+ *
2031
+ * // With aspect ratio (prevents layout shift)
2032
+ * function AspectRatioImage() {
2033
+ * const { imgProps, containerProps } = useOptimizedImage({
2034
+ * src: '/image.jpg',
2035
+ * alt: 'Fixed aspect ratio',
2036
+ * aspectRatio: '16/9',
2037
+ * loading: 'lazy'
2038
+ * })
2039
+ * return (
2040
+ * <div {...containerProps}>
2041
+ * <img {...imgProps} />
2042
+ * </div>
2043
+ * )
2044
+ * }
2045
+ *
2046
+ * // Complete example with all options
2047
+ * function CompleteExample() {
2048
+ * const { imgProps, containerProps } = useOptimizedImage({
2049
+ * src: '/hero-1200w.jpg',
2050
+ * alt: 'Hero image with mountains',
2051
+ * srcSet: '/hero-640w.jpg 640w, /hero-1200w.jpg 1200w, /hero-1920w.jpg 1920w',
2052
+ * sizes: '(max-width: 640px) 100vw, (max-width: 1200px) 80vw, 1200px',
2053
+ * aspectRatio: '16/9',
2054
+ * loading: 'lazy',
2055
+ * decoding: 'async',
2056
+ * width: 1200,
2057
+ * className: 'hero-image'
2058
+ * })
2059
+ *
2060
+ * return (
2061
+ * <div {...containerProps}>
2062
+ * <img {...imgProps} />
2063
+ * </div>
2064
+ * )
2065
+ * }
2066
+ * ```
2067
+ *
2068
+ * @param options - Configuration options for the optimized image
2069
+ * @returns Object containing imgProps (and optionally containerProps)
2070
+ */
2071
+ export declare function useOptimizedImage(options: UseOptimizedImageOptions): UseOptimizedImageResult;
2072
+
2073
+ /**
2074
+ * Options for the useOptimizedImage hook
2075
+ */
2076
+ export declare interface UseOptimizedImageOptions {
2077
+ /**
2078
+ * Source URL for the image
2079
+ */
2080
+ src: string;
2081
+ /**
2082
+ * Alt text for accessibility (required)
2083
+ */
2084
+ alt: string;
2085
+ /**
2086
+ * Source set for responsive images
2087
+ * @example "image-320w.jpg 320w, image-640w.jpg 640w, image-1024w.jpg 1024w"
2088
+ */
2089
+ srcSet?: string;
2090
+ /**
2091
+ * Sizes attribute for responsive images
2092
+ * @example "(max-width: 640px) 100vw, 50vw"
2093
+ */
2094
+ sizes?: string;
2095
+ /**
2096
+ * Loading strategy
2097
+ * - 'lazy': Load when near viewport (default, recommended for most images)
2098
+ * - 'eager': Load immediately
2099
+ */
2100
+ loading?: 'lazy' | 'eager';
2101
+ /**
2102
+ * Decoding strategy
2103
+ * - 'async': Decode asynchronously (default, recommended for performance)
2104
+ * - 'sync': Decode synchronously
2105
+ * - 'auto': Browser decides
2106
+ */
2107
+ decoding?: 'async' | 'sync' | 'auto';
2108
+ /**
2109
+ * Aspect ratio for placeholder (prevents layout shift)
2110
+ * @example "16/9" or "1/1" or "4/3"
2111
+ */
2112
+ aspectRatio?: string;
2113
+ /**
2114
+ * Width of the image (used with aspectRatio to calculate height)
2115
+ */
2116
+ width?: number;
2117
+ /**
2118
+ * Height of the image (used with aspectRatio or standalone)
2119
+ */
2120
+ height?: number;
2121
+ /**
2122
+ * Additional CSS class names
2123
+ */
2124
+ className?: string;
2125
+ /**
2126
+ * Inline styles
2127
+ */
2128
+ style?: React.CSSProperties;
2129
+ }
2130
+
2131
+ /**
2132
+ * Return type for useOptimizedImage hook
2133
+ */
2134
+ export declare interface UseOptimizedImageResult {
2135
+ /**
2136
+ * Props to spread onto an <img> element
2137
+ */
2138
+ imgProps: ImgHTMLAttributes<HTMLImageElement>;
2139
+ /**
2140
+ * Container props (if aspectRatio is used)
2141
+ * Apply these to a wrapper div for proper aspect ratio handling
2142
+ */
2143
+ containerProps?: {
2144
+ style: React.CSSProperties;
2145
+ className?: string;
2146
+ };
2147
+ }
2148
+
1717
2149
  /**
1718
2150
  * A hook that determines whether a container element is "compact" by measuring its width.
1719
2151
  * Uses ResizeObserver to respond to container size changes.