fluent-styles 1.61.0 → 1.62.0

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
@@ -20,7 +20,8 @@ A comprehensive, TypeScript-first React Native UI library providing production-r
20
20
  - [StyledCard](#styledcard)
21
21
  - [StyledBadge / BadgeWithIcon](#styledbadge--badgewithicon)
22
22
  - [StyledImage / StyledImageBackground](#styledimage--styledimagebackground)
23
- - [StyledHeader / FullHeader](#styledheader--fullheader)
23
+ - [StyledHeader](#styledheader)
24
+ - [StyledForm](#styledform)
24
25
  - [StyledDropdown / StyledMultiSelectDropdown](#styleddropdown--styledmultiselectdropdown)
25
26
  - [Popup](#popup)
26
27
  - [Drawer](#drawer)
@@ -44,6 +45,7 @@ A comprehensive, TypeScript-first React Native UI library providing production-r
44
45
  - [StyledProgressBar](#styledprogressbar)
45
46
  - [StyledSlider](#styledslider)
46
47
  - [StyledDatePicker](#styleddatepicker)
48
+ - [StyledTable](#styledtable)
47
49
  - [Hooks](#hooks)
48
50
  - [useToast](#usetoast)
49
51
  - [useNotification](#usenotification)
@@ -586,43 +588,129 @@ import { StyledImage, StyledImageBackground } from 'fluent-styles'
586
588
 
587
589
  ---
588
590
 
589
- ### StyledHeader / FullHeader
591
+ ### StyledHeader
590
592
 
591
- A pre-built navigation header with optional status bar management.
593
+ A composable navigation header with status bar management, slot-based layout, and a `StyledHeader.Full` escape hatch for fully custom content.
592
594
 
593
595
  ```tsx
594
- import { StyledHeader, FullHeader } from 'fluent-styles'
596
+ import { StyledHeader } from 'fluent-styles'
595
597
 
598
+ // ── Basic title alignments ───────────────────────────────────────────────────
599
+ <StyledHeader title="Left aligned" titleAlignment="left" showStatusBar={false} />
600
+ <StyledHeader title="Center aligned" titleAlignment="center" showStatusBar={false} />
601
+ <StyledHeader title="Right aligned" titleAlignment="right" showStatusBar={false} />
602
+
603
+ // ── Back arrow ───────────────────────────────────────────────────────────────
604
+ <StyledHeader
605
+ title="Go back"
606
+ showBackArrow
607
+ onBackPress={() => navigation.goBack()}
608
+ />
609
+
610
+ // Custom back arrow colour + size
611
+ <StyledHeader
612
+ title="Profile"
613
+ titleAlignment="center"
614
+ showBackArrow
615
+ backArrowProps={{ size: 20, color: '#6366f1' }}
616
+ onBackPress={() => navigation.goBack()}
617
+ />
618
+
619
+ // ── Right / left icons ───────────────────────────────────────────────────────
596
620
  <StyledHeader
597
- title="Settings"
621
+ title="Notifications"
598
622
  titleAlignment="center"
599
623
  showBackArrow
600
624
  onBackPress={() => navigation.goBack()}
601
625
  rightIcon={<SettingsIcon />}
602
626
  />
603
627
 
604
- // Custom back button styling
628
+ // Multiple right icons
605
629
  <StyledHeader
606
- title="Profile"
630
+ title="Photos"
631
+ titleAlignment="center"
632
+ showBackArrow
633
+ onBackPress={() => navigation.goBack()}
634
+ rightIcon={
635
+ <Stack horizontal gap={8}>
636
+ <SearchIconBtn />
637
+ <MoreIconBtn />
638
+ </Stack>
639
+ }
640
+ />
641
+
642
+ // Logo + action on the right (no back arrow)
643
+ <StyledHeader
644
+ titleAlignment="left"
645
+ leftIcon={
646
+ <Stack horizontal gap={6} alignItems="center">
647
+ <BrandLogo />
648
+ <StyledText fontSize={16} fontWeight="800">fluent</StyledText>
649
+ </Stack>
650
+ }
651
+ rightIcon={<CartIconBtn />}
652
+ />
653
+
654
+ // ── Themed background ────────────────────────────────────────────────────────
655
+ <StyledHeader
656
+ title="Dark header"
657
+ titleAlignment="center"
607
658
  showBackArrow
608
- backArrowProps={{ size: 20, color: '#6366f1', onPress: () => navigation.goBack() }}
659
+ onBackPress={() => navigation.goBack()}
660
+ backgroundColor={theme.colors.gray[900]}
661
+ titleProps={{ color: '#fff', fontWeight: '700' }}
662
+ backArrowProps={{ color: '#fff' }}
609
663
  />
610
664
 
611
- // Full header manages status bar spacer automatically
612
- <FullHeader statusBarProps={{ barStyle: 'dark-content' }}>
613
- <MyCustomHeaderContent />
614
- </FullHeader>
665
+ // ── StyledHeader.Full — fully custom children ────────────────────────────────
666
+ // Renders children directly inside the container; skips the built-in layout slots.
667
+ // Useful for search bars, tab strips, chat headers, etc.
668
+ <StyledHeader showStatusBar={false} backgroundColor={palettes.white}>
669
+ <StyledHeader.Full>
670
+ <Stack flex={1} horizontal alignItems="center" paddingHorizontal={12} gap={10}>
671
+ <BackBtn />
672
+ <SearchBar />
673
+ <CancelBtn />
674
+ </Stack>
675
+ </StyledHeader.Full>
676
+ </StyledHeader>
677
+
678
+ // Chat-style header with avatar + call buttons
679
+ <StyledHeader showStatusBar={false} backgroundColor={palettes.white}>
680
+ <StyledHeader.Full>
681
+ <Stack flex={1} horizontal alignItems="center" paddingHorizontal={12} gap={10}>
682
+ <BackBtn />
683
+ <Avatar uri={avatarUri} size={36} />
684
+ <Stack flex={1} gap={1}>
685
+ <StyledText fontSize={14} fontWeight="700">Priya Kapoor</StyledText>
686
+ <StyledText fontSize={11} color={palettes.teal[500]}>Active now</StyledText>
687
+ </Stack>
688
+ <CallBtn />
689
+ <VideoBtn />
690
+ </Stack>
691
+ </StyledHeader.Full>
692
+ </StyledHeader>
615
693
  ```
616
694
 
617
- | Prop | Type | Description |
618
- |---|---|---|
619
- | `title` | `string` | Header title |
620
- | `titleAlignment` | `left \| center \| right` | Title alignment |
621
- | `showBackArrow` | `boolean` | Renders a back arrow |
622
- | `onBackPress` | `() => void` | Back arrow handler |
623
- | `leftIcon / rightIcon` | `ReactNode` | Custom icon slots |
624
- | `backArrowProps` | `BackArrowProps` | Size, colour, custom press handler |
625
- | `showStatusBar` | `boolean` | Include status bar spacer |
695
+ #### Props
696
+
697
+ | Prop | Type | Default | Description |
698
+ |---|---|---|---|
699
+ | `title` | `string` | | Header title |
700
+ | `titleAlignment` | `left \| center \| right` | `left` | Title position |
701
+ | `titleProps` | `StyledTextProps` | | Font/colour overrides for the title |
702
+ | `showBackArrow` | `boolean` | `false` | Renders a chevron back arrow |
703
+ | `onBackPress` | `() => void` | | Tapped when the back arrow is pressed |
704
+ | `backArrowProps` | `BackArrowProps` | — | `size`, `color`, `strokeWidth` overrides |
705
+ | `leftIcon` | `ReactNode` | — | Custom left-slot content (replaces back arrow) |
706
+ | `rightIcon` | `ReactNode` | — | Custom right-slot content |
707
+ | `showStatusBar` | `boolean` | `true` | Include status bar spacer above the header |
708
+ | `statusBarProps` | `StatusBarProps` | — | Forwarded to the internal `StatusBar` component |
709
+ | `skipStatusBarOnAndroid` | `boolean` | `true` | Skip spacer on Android |
710
+ | `skipStatusBarOnIOS` | `boolean` | `true` | Skip spacer on iOS |
711
+ | `children` | `ReactNode` | — | When present (via `StyledHeader.Full`), replaces built-in slot layout |
712
+
713
+ All flat `ViewStyle` props (`backgroundColor`, `paddingHorizontal`, `borderRadius`, …) are forwarded to the container.
626
714
 
627
715
  ---
628
716
 
@@ -2677,6 +2765,439 @@ const DURATION = 243
2677
2765
 
2678
2766
  ---
2679
2767
 
2768
+ ### StyledForm
2769
+
2770
+ A composable form wrapper that propagates `disabled` / `loading` state to all sub-components via React context, with optional keyboard avoidance and scroll wrapping.
2771
+
2772
+ ```tsx
2773
+ import { StyledForm } from 'fluent-styles'
2774
+ ```
2775
+
2776
+ #### Sub-components
2777
+
2778
+ | Component | Wraps | Description |
2779
+ |---|---|---|
2780
+ | `StyledForm.Row` | `Stack horizontal` | Side-by-side inputs |
2781
+ | `StyledForm.Section` | — | Grouped block with title, subtitle, and optional divider |
2782
+ | `StyledForm.Actions` | `Stack` | Slot for submit/cancel buttons |
2783
+ | `StyledForm.Input` | `StyledTextInput` | Text input that inherits form `disabled` / `loading` |
2784
+ | `StyledForm.Checkbox` | `StyledCheckBox` | Checkbox that inherits form `disabled` |
2785
+ | `StyledForm.Switch` | `Switch` | Toggle that inherits form `disabled` / `loading` |
2786
+ | `StyledForm.Select` | `StyledDropdown` | Dropdown picker that inherits form `disabled` |
2787
+ | `StyledForm.Radio` | `StyledRadioGroup` | Radio group |
2788
+ | `StyledForm.DatePicker` | `StyledDatePicker` | Date/time picker that inherits form `disabled` |
2789
+ | `StyledForm.Slider` | `StyledSlider` | Slider that inherits form `disabled` |
2790
+
2791
+ #### `StyledForm` root props
2792
+
2793
+ | Prop | Type | Default | Description |
2794
+ |---|---|---|---|
2795
+ | `disabled` | `boolean` | `false` | Disables all sub-components via context |
2796
+ | `loading` | `boolean` | `false` | Sets loading state on compatible sub-components |
2797
+ | `gap` | `number` | `16` | Vertical gap between form fields |
2798
+ | `avoidKeyboard` | `boolean` | `true` | Wraps content in `KeyboardAvoidingView` |
2799
+ | `scrollable` | `boolean` | `false` | Wraps content in a `ScrollView` |
2800
+ | `scrollPadding` | `number` | `40` | Bottom padding inside the scroll view |
2801
+
2802
+ #### Usage
2803
+
2804
+ ```tsx
2805
+ import { StyledForm, StyledButton, StyledText, StyledDivider, Stack, theme, palettes } from 'fluent-styles'
2806
+
2807
+ // ── 1. Sign-up form — inputs, checkbox, inline validation ────────────────────
2808
+ const [email, setEmail] = useState('')
2809
+ const [password, setPassword] = useState('')
2810
+ const [agreed, setAgreed] = useState(false)
2811
+ const [loading, setLoading] = useState(false)
2812
+
2813
+ const errors = {
2814
+ email: email.length > 0 && !email.includes('@') ? 'Enter a valid email' : undefined,
2815
+ password: password.length > 0 && password.length < 8 ? 'At least 8 characters' : undefined,
2816
+ }
2817
+
2818
+ <StyledForm gap={16} avoidKeyboard={false} disabled={loading}>
2819
+ <StyledForm.Input
2820
+ label="Email address"
2821
+ required
2822
+ variant="outline"
2823
+ placeholder="you@example.com"
2824
+ keyboardType="email-address"
2825
+ autoCapitalize="none"
2826
+ value={email}
2827
+ onChangeText={setEmail}
2828
+ error={!!errors.email}
2829
+ errorMessage={errors.email}
2830
+ />
2831
+
2832
+ <StyledForm.Input
2833
+ label="Password"
2834
+ required
2835
+ variant="outline"
2836
+ placeholder="8+ characters"
2837
+ secureTextEntry
2838
+ value={password}
2839
+ onChangeText={setPassword}
2840
+ error={!!errors.password}
2841
+ errorMessage={errors.password}
2842
+ />
2843
+
2844
+ <StyledDivider borderBottomColor={theme.colors.gray[100]} />
2845
+
2846
+ <Stack horizontal alignItems="center" gap={12}>
2847
+ <StyledForm.Checkbox checked={agreed} onCheck={setAgreed} />
2848
+ <StyledText fontSize={13}>I agree to the Terms of Service</StyledText>
2849
+ </Stack>
2850
+
2851
+ <StyledForm.Actions>
2852
+ <StyledButton block loading={loading} onPress={handleSubmit}
2853
+ backgroundColor={palettes.indigo[600]}>
2854
+ <StyledText color="#fff" fontSize={15} fontWeight="700">
2855
+ {loading ? 'Creating account…' : 'Create account'}
2856
+ </StyledText>
2857
+ </StyledButton>
2858
+ </StyledForm.Actions>
2859
+ </StyledForm>
2860
+
2861
+ // ── 2. Profile form — Row, Section, Select, DatePicker, Switch ───────────────
2862
+ <StyledForm gap={20} avoidKeyboard={false}>
2863
+ <StyledForm.Section title="Personal details" subtitle="How you appear to others">
2864
+ <StyledForm.Row>
2865
+ <StyledForm.Input label="First name" flex={1} variant="outline"
2866
+ value={firstName} onChangeText={setFirstName} />
2867
+ <StyledForm.Input label="Last name" flex={1} variant="outline"
2868
+ value={lastName} onChangeText={setLastName} />
2869
+ </StyledForm.Row>
2870
+
2871
+ <StyledForm.Input
2872
+ label="Bio" variant="outline" multiline showCounter maxLength={160}
2873
+ value={bio} onChangeText={setBio} helperText="Shown on your public profile"
2874
+ />
2875
+
2876
+ {/* StyledForm.Select uses the `data` prop (same as StyledDropdown) */}
2877
+ <StyledForm.Select
2878
+ label="Country"
2879
+ variant="outline"
2880
+ data={COUNTRY_OPTIONS}
2881
+ value={country}
2882
+ onChange={(item) => setCountry(item.value)}
2883
+ placeholder="Select country"
2884
+ />
2885
+
2886
+ <StyledForm.DatePicker
2887
+ label="Date of birth" mode="date" variant="input"
2888
+ value={dob} onChange={setDob} onConfirm={setDob}
2889
+ />
2890
+ </StyledForm.Section>
2891
+
2892
+ <StyledForm.Section title="Notifications">
2893
+ <Stack horizontal alignItems="center" justifyContent="space-between">
2894
+ <StyledText fontSize={14} fontWeight="600">Newsletter</StyledText>
2895
+ <StyledForm.Switch value={newsletter} onChange={setNewsletter}
2896
+ activeColor={palettes.indigo[600]} />
2897
+ </Stack>
2898
+ </StyledForm.Section>
2899
+
2900
+ <StyledForm.Actions horizontal gap={10}>
2901
+ <StyledButton outline compact flex={1} onPress={discard}>
2902
+ <StyledText fontSize={14} fontWeight="600">Discard</StyledText>
2903
+ </StyledButton>
2904
+ <StyledButton primary compact flex={1}
2905
+ backgroundColor={palettes.indigo[600]} onPress={save}>
2906
+ <StyledText fontSize={14} fontWeight="700" color="#fff">Save</StyledText>
2907
+ </StyledButton>
2908
+ </StyledForm.Actions>
2909
+ </StyledForm>
2910
+
2911
+ // ── 3. Subscription form — Radio, Slider, form-level disabled context ─────────
2912
+ // Setting `disabled` on <StyledForm> propagates down to every sub-component.
2913
+ <StyledForm gap={20} avoidKeyboard={false} disabled={locked}>
2914
+ <StyledForm.Section title="Choose a plan">
2915
+ <StyledForm.Radio
2916
+ options={PLAN_OPTIONS}
2917
+ value={plan}
2918
+ onChange={setPlan}
2919
+ variant="list"
2920
+ colors={{ active: palettes.indigo[600] }}
2921
+ />
2922
+ </StyledForm.Section>
2923
+
2924
+ <StyledForm.Section title="Team size">
2925
+ <StyledForm.Slider
2926
+ value={seats}
2927
+ min={2} max={50} step={1}
2928
+ onValueChange={setSeats}
2929
+ formatLabel={(v) => `${v} seats`}
2930
+ colors={{ fill: palettes.indigo[600] }}
2931
+ />
2932
+ </StyledForm.Section>
2933
+
2934
+ <StyledForm.Actions>
2935
+ <StyledButton primary block onPress={() => setLocked((v) => !v)}
2936
+ backgroundColor={locked ? theme.colors.gray[300] : palettes.indigo[600]}>
2937
+ <StyledText fontSize={15} fontWeight="700" color="#fff">
2938
+ {locked ? '🔒 Disabled (tap to re-enable)' : 'Subscribe'}
2939
+ </StyledText>
2940
+ </StyledButton>
2941
+ </StyledForm.Actions>
2942
+ </StyledForm>
2943
+
2944
+ // ── 4. Filter panel — compact, no sections ───────────────────────────────────
2945
+ <StyledForm gap={14} avoidKeyboard={false}>
2946
+ <StyledForm.Input
2947
+ variant="filled" placeholder="Search products…"
2948
+ value={query} onChangeText={setQuery} clearable
2949
+ />
2950
+
2951
+ <StyledForm.Select
2952
+ label="Region" variant="outline" size="sm"
2953
+ data={COUNTRY_OPTIONS} value={country}
2954
+ onChange={(item) => setCountry(item.value)}
2955
+ placeholder="All regions"
2956
+ />
2957
+
2958
+ <StyledForm.Slider
2959
+ variant="range"
2960
+ value={minPrice} valueHigh={maxPrice}
2961
+ min={0} max={1000} step={10}
2962
+ onRangeChange={(lo, hi) => { setMinPrice(lo); setMaxPrice(hi) }}
2963
+ formatLabel={(v) => `$${v}`}
2964
+ />
2965
+
2966
+ <Stack horizontal alignItems="center" justifyContent="space-between">
2967
+ <StyledText fontSize={14} fontWeight="600">In stock only</StyledText>
2968
+ <StyledForm.Switch value={inStock} onChange={setInStock} size="sm" />
2969
+ </Stack>
2970
+
2971
+ <StyledForm.Actions horizontal gap={10}>
2972
+ <StyledButton outline compact flex={1} onPress={resetFilters}>
2973
+ <StyledText fontSize={13} fontWeight="600">Reset</StyledText>
2974
+ </StyledButton>
2975
+ <StyledButton primary compact flex={2}
2976
+ backgroundColor={theme.colors.gray[900]}>
2977
+ <StyledText fontSize={13} fontWeight="700" color="#fff">Apply filters</StyledText>
2978
+ </StyledButton>
2979
+ </StyledForm.Actions>
2980
+ </StyledForm>
2981
+ ```
2982
+
2983
+ > **Note:** `StyledForm.Select` wraps `StyledDropdown` and therefore uses the `data` prop (not `options`). `StyledForm.Radio` wraps `StyledRadioGroup` and uses the `options` prop.
2984
+
2985
+ ---
2986
+
2987
+ ### StyledTable
2988
+
2989
+ A responsive data table with client-side and server-side pagination, sortable columns, row selection, striped / dark variants, and an automatic card layout on narrow screens.
2990
+
2991
+ ```tsx
2992
+ import { StyledTable, type TableColumn, usePaginatedQuery } from 'fluent-styles'
2993
+ ```
2994
+
2995
+ #### `TableColumn<T>` definition
2996
+
2997
+ | Field | Type | Description |
2998
+ |---|---|---|
2999
+ | `key` | `string` | Must match a key of the row data object |
3000
+ | `title` | `string` | Column header label |
3001
+ | `width` | `number` | Fixed column width in px. Omit to stretch (flex: 1) |
3002
+ | `align` | `left \| center \| right` | Text alignment (default `left`) |
3003
+ | `sortable` | `boolean` | Allow tapping the header to sort |
3004
+ | `render` | `(value, row, index) => ReactNode` | Custom cell renderer |
3005
+
3006
+ #### `StyledTable` props
3007
+
3008
+ | Prop | Type | Default | Description |
3009
+ |---|---|---|---|
3010
+ | `columns` | `TableColumn<T>[]` | — | Column definitions |
3011
+ | `data` | `T[]` | — | Row data (each row must have a unique `id` field) |
3012
+ | `selectable` | `boolean` | `false` | Show checkboxes for row selection |
3013
+ | `selectedIds` | `(string \| number)[]` | — | Controlled selection |
3014
+ | `onSelectionChange` | `(ids) => void` | — | Selection change callback |
3015
+ | `sortKey` | `string \| null` | — | Controlled sort column |
3016
+ | `sortDirection` | `asc \| desc \| null` | — | Controlled sort direction |
3017
+ | `onSort` | `(key, direction) => void` | — | Sort change callback |
3018
+ | `pagination` | `boolean` | `false` | Enable internal (client-side) pagination |
3019
+ | `pageSize` | `number` | `10` | Rows per page (internal pagination) |
3020
+ | `externalPagination` | `boolean` | `false` | Render `data` as-is — parent manages pages |
3021
+ | `currentPage` | `number` | — | 0-based current page (controlled) |
3022
+ | `totalPages` | `number` | — | Total pages from the datasource |
3023
+ | `totalCount` | `number` | — | Total record count |
3024
+ | `onPageChange` | `(page: number) => void` | — | Page change callback |
3025
+ | `loading` | `boolean` | `false` | Show loading skeleton over rows |
3026
+ | `virtualized` | `boolean` | auto | Use `FlatList` renderer (auto-enabled for >50 rows) |
3027
+ | `striped` | `boolean` | `false` | Alternating row background |
3028
+ | `showDivider` | `boolean` | `false` | Horizontal divider between rows |
3029
+ | `scrollable` | `boolean` | `false` | Horizontal scroll when content overflows |
3030
+ | `emptyText` | `string` | — | Text shown when `data` is empty |
3031
+ | `emptyNode` | `ReactNode` | — | Custom empty state node |
3032
+ | `colors` | `Partial<TableColors>` | — | Color token overrides |
3033
+ | `borderRadius` | `number` | `16` | Outer container border radius |
3034
+ | `bordered` | `boolean` | `true` | Show outer card border |
3035
+ | `cardBreakpoint` | `number` | `768` | Width threshold below which rows switch to card layout |
3036
+ | `forceTable` | `boolean` | `false` | Always render table rows regardless of screen width |
3037
+ | `forceCards` | `boolean` | `false` | Always render card layout regardless of screen width |
3038
+ | `cardRender` | `(row, index, selected, onToggle?) => ReactNode` | — | Custom card renderer for narrow screens |
3039
+ | `onRowPress` | `(row, index) => void` | — | Row tap callback |
3040
+
3041
+ #### Usage
3042
+
3043
+ ```tsx
3044
+ import { StyledTable, type TableColumn, theme, palettes } from 'fluent-styles'
3045
+
3046
+ // ── 1. Minimal — static data, no pagination ────────────────────────────────
3047
+ interface UserRow { id: number; name: string; email: string; role: string }
3048
+
3049
+ const USER_COLS: TableColumn<UserRow>[] = [
3050
+ { key: 'name', title: 'Name', render: (v) => <StyledText fontWeight="700">{v}</StyledText> },
3051
+ { key: 'email', title: 'Email', render: (v) => <StyledText color={theme.colors.gray[500]}>{v}</StyledText> },
3052
+ { key: 'role', title: 'Role', width: 90, align: 'center' },
3053
+ ]
3054
+
3055
+ <StyledTable columns={USER_COLS} data={users} showDivider />
3056
+
3057
+ // ── 2. Sortable + selectable ───────────────────────────────────────────────
3058
+ const [sel, setSel] = useState<(string | number)[]>([])
3059
+
3060
+ const PRODUCT_COLS: TableColumn<ProductRow>[] = [
3061
+ { key: 'name', title: 'Product' },
3062
+ { key: 'price', title: 'Price', width: 80, align: 'right', sortable: true,
3063
+ render: (v) => <StyledText fontWeight="700">${v}</StyledText> },
3064
+ { key: 'stock', title: 'Stock', width: 80, align: 'center', sortable: true },
3065
+ { key: 'status', title: 'Status', width: 110, align: 'center',
3066
+ render: (v) => <StatusBadge label={v} /> },
3067
+ ]
3068
+
3069
+ <StyledTable
3070
+ columns={PRODUCT_COLS}
3071
+ data={products}
3072
+ selectable
3073
+ selectedIds={sel}
3074
+ onSelectionChange={setSel}
3075
+ showDivider
3076
+ pagination
3077
+ pageSize={8}
3078
+ />
3079
+
3080
+ // ── 3. Client-side pagination ──────────────────────────────────────────────
3081
+ <StyledTable
3082
+ columns={ORDER_COLS}
3083
+ data={orders} // pass the full dataset
3084
+ pagination // StyledTable slices it internally
3085
+ pageSize={10}
3086
+ showDivider
3087
+ bordered
3088
+ />
3089
+
3090
+ // ── 4. External pagination (REST API) ─────────────────────────────────────
3091
+ // Use the `usePaginatedQuery` hook to manage page state, sorting, search,
3092
+ // and loading — then spread `table.tableProps` onto StyledTable.
3093
+ const table = usePaginatedQuery<Order>({
3094
+ pageSize: 10,
3095
+ fetcher: async ({ page, pageSize, sortKey, sortDir, search, filters }) => {
3096
+ const res = await api.get('/orders', { params: { page, pageSize, sortKey, sortDir, search, ...filters } })
3097
+ return { data: res.data.items, totalCount: res.data.total }
3098
+ },
3099
+ initialSortKey: 'date',
3100
+ initialSortDir: 'desc',
3101
+ })
3102
+
3103
+ <StyledTable
3104
+ columns={ORDER_COLS}
3105
+ {...table.tableProps} // wires data, loading, pagination state, sort, onSort, onPageChange
3106
+ showDivider
3107
+ bordered
3108
+ />
3109
+
3110
+ // ── 5. Realm / SQLite (synchronous) ───────────────────────────────────────
3111
+ const table = usePaginatedQuery<Employee>({
3112
+ pageSize: 15,
3113
+ realmQuery: ({ page, pageSize, sortKey, sortDir, search }) => {
3114
+ const results = realm.objects('Employee').sorted(sortKey ?? 'name', sortDir === 'desc')
3115
+ return { data: Array.from(results).slice(page * pageSize, (page + 1) * pageSize), totalCount: results.length }
3116
+ },
3117
+ })
3118
+
3119
+ <StyledTable columns={EMP_COLS} {...table.tableProps} showDivider bordered />
3120
+
3121
+ // ── 6. Custom card layout (narrow screens) ────────────────────────────────
3122
+ // `forceCards` renders the custom card renderer regardless of screen width.
3123
+ // `forceTable` always renders the table.
3124
+ // Default: auto-switches at the `cardBreakpoint` (768 px).
3125
+ <StyledTable
3126
+ columns={PRODUCT_COLS}
3127
+ data={products}
3128
+ pagination
3129
+ pageSize={6}
3130
+ forceCards
3131
+ bordered={false}
3132
+ cardRender={(row, index, isSelected, onToggle) => (
3133
+ <ProductCard row={row} isSelected={isSelected} onToggle={onToggle} />
3134
+ )}
3135
+ />
3136
+
3137
+ // ── 7. Striped + dark theme ────────────────────────────────────────────────
3138
+ // Striped
3139
+ <StyledTable columns={USER_COLS} data={users} striped showDivider={false} />
3140
+
3141
+ // Dark theme via color overrides
3142
+ <StyledTable
3143
+ columns={USER_COLS}
3144
+ data={users}
3145
+ showDivider
3146
+ colors={{
3147
+ background: theme.colors.gray[900],
3148
+ headerBg: theme.colors.gray[800],
3149
+ headerText: theme.colors.gray[400],
3150
+ rowBg: theme.colors.gray[900],
3151
+ border: theme.colors.gray[700],
3152
+ divider: theme.colors.gray[700],
3153
+ text: theme.colors.gray[100],
3154
+ sortActive: theme.colors.gray[100],
3155
+ sortInactive: theme.colors.gray[600],
3156
+ selectedBg: palettes.indigo[900],
3157
+ selectedBorder: palettes.indigo[500],
3158
+ }}
3159
+ />
3160
+
3161
+ // ── 8. Custom empty state ─────────────────────────────────────────────────
3162
+ <StyledTable
3163
+ columns={USER_COLS}
3164
+ data={[]}
3165
+ emptyNode={
3166
+ <Stack alignItems="center" gap={8}>
3167
+ <StyledText fontSize={32}>🗂️</StyledText>
3168
+ <StyledText fontSize={15} fontWeight="700">No users yet</StyledText>
3169
+ <StyledText fontSize={13} color={theme.colors.gray[400]}>Invite someone to get started.</StyledText>
3170
+ </Stack>
3171
+ }
3172
+ />
3173
+ ```
3174
+
3175
+ #### `usePaginatedQuery` hook
3176
+
3177
+ Manages page, sort, search, filters, loading, and error for external data sources. Returns `table.tableProps` which can be spread directly onto `StyledTable`.
3178
+
3179
+ ```tsx
3180
+ const table = usePaginatedQuery<T>(options)
3181
+
3182
+ // Spread onto StyledTable:
3183
+ <StyledTable columns={COLS} {...table.tableProps} />
3184
+ ```
3185
+
3186
+ | Option | Type | Description |
3187
+ |---|---|---|
3188
+ | `pageSize` | `number` | Rows per page |
3189
+ | `fetcher` | `async (params) => { data, totalCount }` | Async REST / GraphQL fetcher |
3190
+ | `realmQuery` | `(params) => { data, totalCount }` | Synchronous Realm / SQLite query |
3191
+ | `initialSortKey` | `string` | Initial sort column |
3192
+ | `initialSortDir` | `asc \| desc \| null` | Initial sort direction |
3193
+ | `initialSearch` | `string` | Initial search string |
3194
+ | `initialFilters` | `Record<string, any>` | Initial filter values |
3195
+ | `searchDebounce` | `number` | Search debounce ms (default 300) |
3196
+
3197
+ `table` return value exposes: `data`, `loading`, `error`, `totalCount`, `totalPages`, `page`, `setPage`, `sortKey`, `sortDir`, `setSort`, `search`, `setSearch`, `filters`, `setFilters`, `refresh`, and `tableProps` (ready to spread).
3198
+
3199
+ ---
3200
+
2680
3201
  ## Hooks
2681
3202
 
2682
3203
  All hooks require a `PortalManager` ancestor.
@@ -721,4 +721,28 @@ Object.keys(_cycle).forEach(function (key) {
721
721
  }
722
722
  });
723
723
  });
724
+ var _index41 = require("./table/index.js");
725
+ Object.keys(_index41).forEach(function (key) {
726
+ if (key === "default" || key === "__esModule") return;
727
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
728
+ if (key in exports && exports[key] === _index41[key]) return;
729
+ Object.defineProperty(exports, key, {
730
+ enumerable: true,
731
+ get: function () {
732
+ return _index41[key];
733
+ }
734
+ });
735
+ });
736
+ var _usepaginatedquery = require("./table/usepaginatedquery.js");
737
+ Object.keys(_usepaginatedquery).forEach(function (key) {
738
+ if (key === "default" || key === "__esModule") return;
739
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
740
+ if (key in exports && exports[key] === _usepaginatedquery[key]) return;
741
+ Object.defineProperty(exports, key, {
742
+ enumerable: true,
743
+ get: function () {
744
+ return _usepaginatedquery[key];
745
+ }
746
+ });
747
+ });
724
748
  //# sourceMappingURL=index.js.map