fluent-styles 1.61.0 → 1.62.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.
Files changed (46) hide show
  1. package/README.md +592 -33
  2. package/lib/commonjs/index.js +24 -0
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/tabBar/TabBar.js.map +1 -1
  5. package/lib/commonjs/table/index.js +650 -0
  6. package/lib/commonjs/table/index.js.map +1 -0
  7. package/lib/commonjs/table/usepaginatedquery.js +181 -0
  8. package/lib/commonjs/table/usepaginatedquery.js.map +1 -0
  9. package/lib/module/index.js +2 -0
  10. package/lib/module/index.js.map +1 -1
  11. package/lib/module/tabBar/TabBar.js.map +1 -1
  12. package/lib/module/table/index.js +644 -0
  13. package/lib/module/table/index.js.map +1 -0
  14. package/lib/module/table/usepaginatedquery.js +178 -0
  15. package/lib/module/table/usepaginatedquery.js.map +1 -0
  16. package/lib/typescript/header/index.d.ts +1 -3
  17. package/lib/typescript/header/index.d.ts.map +1 -1
  18. package/lib/typescript/icons/backArrow.d.ts +1 -1
  19. package/lib/typescript/icons/bellFill.d.ts +1 -1
  20. package/lib/typescript/icons/bellOutline.d.ts +1 -1
  21. package/lib/typescript/icons/checkmark.d.ts +1 -1
  22. package/lib/typescript/icons/delete.d.ts +1 -1
  23. package/lib/typescript/icons/downChevron.d.ts +1 -1
  24. package/lib/typescript/icons/error.d.ts +1 -1
  25. package/lib/typescript/icons/forwardArrow.d.ts +1 -1
  26. package/lib/typescript/icons/info.d.ts +1 -1
  27. package/lib/typescript/icons/leftChevron.d.ts +1 -1
  28. package/lib/typescript/icons/rightChevron.d.ts +1 -1
  29. package/lib/typescript/icons/save.d.ts +1 -1
  30. package/lib/typescript/icons/success.d.ts +1 -1
  31. package/lib/typescript/icons/upChevron.d.ts +1 -1
  32. package/lib/typescript/icons/warning.d.ts +1 -1
  33. package/lib/typescript/index.d.ts +2 -0
  34. package/lib/typescript/index.d.ts.map +1 -1
  35. package/lib/typescript/tabBar/TabBar.d.ts +2 -3
  36. package/lib/typescript/tabBar/TabBar.d.ts.map +1 -1
  37. package/lib/typescript/table/index.d.ts +107 -0
  38. package/lib/typescript/table/index.d.ts.map +1 -0
  39. package/lib/typescript/table/usepaginatedquery.d.ts +114 -0
  40. package/lib/typescript/table/usepaginatedquery.d.ts.map +1 -0
  41. package/lib/typescript/utiles/createIcon.d.ts +1 -1
  42. package/package.json +31 -41
  43. package/src/index.ts +2 -0
  44. package/src/tabBar/TabBar.tsx +1 -1
  45. package/src/table/index.tsx +843 -0
  46. package/src/table/usepaginatedquery.tsx +275 -0
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)
@@ -33,7 +34,7 @@ A comprehensive, TypeScript-first React Native UI library providing production-r
33
34
  - [StyledPressable](#styledpressable)
34
35
  - [StyledPage / StyledScrollView](#styledpage--styledscrollview)
35
36
  - [StyledSafeAreaView](#styledsafeareaview)
36
- - [Spacer](#spacer)
37
+ - [Spacer / StyledSpacer](#spacer--styledspacer)
37
38
  - [StyledShape](#styledshape)
38
39
  - [Loader](#loader)
39
40
  - [StyledCircularProgress](#styledcircularprogress)
@@ -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"
607
632
  showBackArrow
608
- backArrowProps={{ size: 20, color: '#6366f1', onPress: () => navigation.goBack() }}
633
+ onBackPress={() => navigation.goBack()}
634
+ rightIcon={
635
+ <Stack horizontal gap={8}>
636
+ <SearchIconBtn />
637
+ <MoreIconBtn />
638
+ </Stack>
639
+ }
609
640
  />
610
641
 
611
- // Full header manages status bar spacer automatically
612
- <FullHeader statusBarProps={{ barStyle: 'dark-content' }}>
613
- <MyCustomHeaderContent />
614
- </FullHeader>
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"
658
+ showBackArrow
659
+ onBackPress={() => navigation.goBack()}
660
+ backgroundColor={theme.colors.gray[900]}
661
+ titleProps={{ color: '#fff', fontWeight: '700' }}
662
+ backArrowProps={{ color: '#fff' }}
663
+ />
664
+
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
 
@@ -1127,18 +1215,42 @@ import { StyledPressable } from 'fluent-styles'
1127
1215
 
1128
1216
  ### StyledPage / StyledScrollView
1129
1217
 
1130
- Base layout containers.
1218
+ `StyledPage` is the recommended **top-level layout wrapper for every screen**. It wraps `StyledSafeAreaView` so safe area insets are handled automatically, and accepts all `ViewStyle` props for quick background colour, padding, and flex tweaks. Pair it with `StyledScrollView` for scrollable screens or nest a `StyledHeader` + content directly inside for fixed-layout screens.
1131
1219
 
1132
1220
  ```tsx
1133
- import { StyledPage, StyledScrollView } from 'fluent-styles'
1221
+ import { StyledPage, StyledScrollView, StyledHeader } from 'fluent-styles'
1222
+
1223
+ // ── Scrollable screen (most screens) ────────────────────────────────────────
1224
+ export default function SettingsScreen() {
1225
+ return (
1226
+ <StyledPage flex={1} backgroundColor="#f8f8f8">
1227
+ <StyledHeader title="Settings" titleAlignment="left" showStatusBar={false} />
1228
+ <StyledScrollView contentContainerStyle={{ padding: 16, paddingBottom: 40 }}>
1229
+ {/* screen content */}
1230
+ </StyledScrollView>
1231
+ </StyledPage>
1232
+ )
1233
+ }
1234
+
1235
+ // ── Fixed layout screen (e.g. chat, camera) ──────────────────────────────────
1236
+ export default function ChatScreen() {
1237
+ return (
1238
+ <StyledPage flex={1} backgroundColor="#fff">
1239
+ <StyledHeader title="Priya Kapoor" showBackArrow onBackPress={() => navigation.goBack()} />
1240
+ <MessageList style={{ flex: 1 }} />
1241
+ <MessageComposer />
1242
+ </StyledPage>
1243
+ )
1244
+ }
1134
1245
 
1135
- <StyledPage flex={1} backgroundColor="#f8f8f8">
1136
- <StyledScrollView contentContainerStyle={{ padding: 16 }}>
1137
- <MyContent />
1138
- </StyledScrollView>
1246
+ // ── Custom background / padding ──────────────────────────────────────────────
1247
+ <StyledPage flex={1} backgroundColor={theme.colors.gray[50]} paddingHorizontal={16}>
1248
+ {/* content */}
1139
1249
  </StyledPage>
1140
1250
  ```
1141
1251
 
1252
+ Accepts all `SafeAreaViewProps` and flat `ViewStyle` props.
1253
+
1142
1254
  ---
1143
1255
 
1144
1256
  ### StyledSafeAreaView
@@ -1155,17 +1267,31 @@ import { StyledSafeAreaView } from 'fluent-styles'
1155
1267
 
1156
1268
  ---
1157
1269
 
1158
- ### Spacer
1270
+ ### Spacer / StyledSpacer
1159
1271
 
1160
- Inserts fixed or flexible whitespace.
1272
+ Inserts fixed or flexible whitespace between elements.
1161
1273
 
1162
1274
  ```tsx
1163
- import { Spacer } from 'fluent-styles'
1275
+ import { StyledSpacer } from 'fluent-styles'
1276
+
1277
+ // Fixed height gap
1278
+ <StyledSpacer height={16} />
1279
+
1280
+ // Flexible — fills remaining space (like a CSS flex spacer)
1281
+ <StyledSpacer flex={1} />
1164
1282
 
1165
- <Spacer height={16} />
1166
- <Spacer flex={1} /> {/* fills remaining space */}
1283
+ // Horizontal gap inside a row
1284
+ <Stack horizontal alignItems="center">
1285
+ <Icon />
1286
+ <StyledSpacer width={8} />
1287
+ <StyledText>Label</StyledText>
1288
+ <StyledSpacer flex={1} />
1289
+ <ChevronIcon />
1290
+ </Stack>
1167
1291
  ```
1168
1292
 
1293
+ **Props:** `height`, `width`, `flex`, `margin`, `marginTop`, `marginBottom`, `marginHorizontal`, `marginVertical`, `backgroundColor`
1294
+
1169
1295
  ---
1170
1296
 
1171
1297
  ### StyledShape
@@ -2677,6 +2803,439 @@ const DURATION = 243
2677
2803
 
2678
2804
  ---
2679
2805
 
2806
+ ### StyledForm
2807
+
2808
+ A composable form wrapper that propagates `disabled` / `loading` state to all sub-components via React context, with optional keyboard avoidance and scroll wrapping.
2809
+
2810
+ ```tsx
2811
+ import { StyledForm } from 'fluent-styles'
2812
+ ```
2813
+
2814
+ #### Sub-components
2815
+
2816
+ | Component | Wraps | Description |
2817
+ |---|---|---|
2818
+ | `StyledForm.Row` | `Stack horizontal` | Side-by-side inputs |
2819
+ | `StyledForm.Section` | — | Grouped block with title, subtitle, and optional divider |
2820
+ | `StyledForm.Actions` | `Stack` | Slot for submit/cancel buttons |
2821
+ | `StyledForm.Input` | `StyledTextInput` | Text input that inherits form `disabled` / `loading` |
2822
+ | `StyledForm.Checkbox` | `StyledCheckBox` | Checkbox that inherits form `disabled` |
2823
+ | `StyledForm.Switch` | `Switch` | Toggle that inherits form `disabled` / `loading` |
2824
+ | `StyledForm.Select` | `StyledDropdown` | Dropdown picker that inherits form `disabled` |
2825
+ | `StyledForm.Radio` | `StyledRadioGroup` | Radio group |
2826
+ | `StyledForm.DatePicker` | `StyledDatePicker` | Date/time picker that inherits form `disabled` |
2827
+ | `StyledForm.Slider` | `StyledSlider` | Slider that inherits form `disabled` |
2828
+
2829
+ #### `StyledForm` root props
2830
+
2831
+ | Prop | Type | Default | Description |
2832
+ |---|---|---|---|
2833
+ | `disabled` | `boolean` | `false` | Disables all sub-components via context |
2834
+ | `loading` | `boolean` | `false` | Sets loading state on compatible sub-components |
2835
+ | `gap` | `number` | `16` | Vertical gap between form fields |
2836
+ | `avoidKeyboard` | `boolean` | `true` | Wraps content in `KeyboardAvoidingView` |
2837
+ | `scrollable` | `boolean` | `false` | Wraps content in a `ScrollView` |
2838
+ | `scrollPadding` | `number` | `40` | Bottom padding inside the scroll view |
2839
+
2840
+ #### Usage
2841
+
2842
+ ```tsx
2843
+ import { StyledForm, StyledButton, StyledText, StyledDivider, Stack, theme, palettes } from 'fluent-styles'
2844
+
2845
+ // ── 1. Sign-up form — inputs, checkbox, inline validation ────────────────────
2846
+ const [email, setEmail] = useState('')
2847
+ const [password, setPassword] = useState('')
2848
+ const [agreed, setAgreed] = useState(false)
2849
+ const [loading, setLoading] = useState(false)
2850
+
2851
+ const errors = {
2852
+ email: email.length > 0 && !email.includes('@') ? 'Enter a valid email' : undefined,
2853
+ password: password.length > 0 && password.length < 8 ? 'At least 8 characters' : undefined,
2854
+ }
2855
+
2856
+ <StyledForm gap={16} avoidKeyboard={false} disabled={loading}>
2857
+ <StyledForm.Input
2858
+ label="Email address"
2859
+ required
2860
+ variant="outline"
2861
+ placeholder="you@example.com"
2862
+ keyboardType="email-address"
2863
+ autoCapitalize="none"
2864
+ value={email}
2865
+ onChangeText={setEmail}
2866
+ error={!!errors.email}
2867
+ errorMessage={errors.email}
2868
+ />
2869
+
2870
+ <StyledForm.Input
2871
+ label="Password"
2872
+ required
2873
+ variant="outline"
2874
+ placeholder="8+ characters"
2875
+ secureTextEntry
2876
+ value={password}
2877
+ onChangeText={setPassword}
2878
+ error={!!errors.password}
2879
+ errorMessage={errors.password}
2880
+ />
2881
+
2882
+ <StyledDivider borderBottomColor={theme.colors.gray[100]} />
2883
+
2884
+ <Stack horizontal alignItems="center" gap={12}>
2885
+ <StyledForm.Checkbox checked={agreed} onCheck={setAgreed} />
2886
+ <StyledText fontSize={13}>I agree to the Terms of Service</StyledText>
2887
+ </Stack>
2888
+
2889
+ <StyledForm.Actions>
2890
+ <StyledButton block loading={loading} onPress={handleSubmit}
2891
+ backgroundColor={palettes.indigo[600]}>
2892
+ <StyledText color="#fff" fontSize={15} fontWeight="700">
2893
+ {loading ? 'Creating account…' : 'Create account'}
2894
+ </StyledText>
2895
+ </StyledButton>
2896
+ </StyledForm.Actions>
2897
+ </StyledForm>
2898
+
2899
+ // ── 2. Profile form — Row, Section, Select, DatePicker, Switch ───────────────
2900
+ <StyledForm gap={20} avoidKeyboard={false}>
2901
+ <StyledForm.Section title="Personal details" subtitle="How you appear to others">
2902
+ <StyledForm.Row>
2903
+ <StyledForm.Input label="First name" flex={1} variant="outline"
2904
+ value={firstName} onChangeText={setFirstName} />
2905
+ <StyledForm.Input label="Last name" flex={1} variant="outline"
2906
+ value={lastName} onChangeText={setLastName} />
2907
+ </StyledForm.Row>
2908
+
2909
+ <StyledForm.Input
2910
+ label="Bio" variant="outline" multiline showCounter maxLength={160}
2911
+ value={bio} onChangeText={setBio} helperText="Shown on your public profile"
2912
+ />
2913
+
2914
+ {/* StyledForm.Select uses the `data` prop (same as StyledDropdown) */}
2915
+ <StyledForm.Select
2916
+ label="Country"
2917
+ variant="outline"
2918
+ data={COUNTRY_OPTIONS}
2919
+ value={country}
2920
+ onChange={(item) => setCountry(item.value)}
2921
+ placeholder="Select country"
2922
+ />
2923
+
2924
+ <StyledForm.DatePicker
2925
+ label="Date of birth" mode="date" variant="input"
2926
+ value={dob} onChange={setDob} onConfirm={setDob}
2927
+ />
2928
+ </StyledForm.Section>
2929
+
2930
+ <StyledForm.Section title="Notifications">
2931
+ <Stack horizontal alignItems="center" justifyContent="space-between">
2932
+ <StyledText fontSize={14} fontWeight="600">Newsletter</StyledText>
2933
+ <StyledForm.Switch value={newsletter} onChange={setNewsletter}
2934
+ activeColor={palettes.indigo[600]} />
2935
+ </Stack>
2936
+ </StyledForm.Section>
2937
+
2938
+ <StyledForm.Actions horizontal gap={10}>
2939
+ <StyledButton outline compact flex={1} onPress={discard}>
2940
+ <StyledText fontSize={14} fontWeight="600">Discard</StyledText>
2941
+ </StyledButton>
2942
+ <StyledButton primary compact flex={1}
2943
+ backgroundColor={palettes.indigo[600]} onPress={save}>
2944
+ <StyledText fontSize={14} fontWeight="700" color="#fff">Save</StyledText>
2945
+ </StyledButton>
2946
+ </StyledForm.Actions>
2947
+ </StyledForm>
2948
+
2949
+ // ── 3. Subscription form — Radio, Slider, form-level disabled context ─────────
2950
+ // Setting `disabled` on <StyledForm> propagates down to every sub-component.
2951
+ <StyledForm gap={20} avoidKeyboard={false} disabled={locked}>
2952
+ <StyledForm.Section title="Choose a plan">
2953
+ <StyledForm.Radio
2954
+ options={PLAN_OPTIONS}
2955
+ value={plan}
2956
+ onChange={setPlan}
2957
+ variant="list"
2958
+ colors={{ active: palettes.indigo[600] }}
2959
+ />
2960
+ </StyledForm.Section>
2961
+
2962
+ <StyledForm.Section title="Team size">
2963
+ <StyledForm.Slider
2964
+ value={seats}
2965
+ min={2} max={50} step={1}
2966
+ onValueChange={setSeats}
2967
+ formatLabel={(v) => `${v} seats`}
2968
+ colors={{ fill: palettes.indigo[600] }}
2969
+ />
2970
+ </StyledForm.Section>
2971
+
2972
+ <StyledForm.Actions>
2973
+ <StyledButton primary block onPress={() => setLocked((v) => !v)}
2974
+ backgroundColor={locked ? theme.colors.gray[300] : palettes.indigo[600]}>
2975
+ <StyledText fontSize={15} fontWeight="700" color="#fff">
2976
+ {locked ? '🔒 Disabled (tap to re-enable)' : 'Subscribe'}
2977
+ </StyledText>
2978
+ </StyledButton>
2979
+ </StyledForm.Actions>
2980
+ </StyledForm>
2981
+
2982
+ // ── 4. Filter panel — compact, no sections ───────────────────────────────────
2983
+ <StyledForm gap={14} avoidKeyboard={false}>
2984
+ <StyledForm.Input
2985
+ variant="filled" placeholder="Search products…"
2986
+ value={query} onChangeText={setQuery} clearable
2987
+ />
2988
+
2989
+ <StyledForm.Select
2990
+ label="Region" variant="outline" size="sm"
2991
+ data={COUNTRY_OPTIONS} value={country}
2992
+ onChange={(item) => setCountry(item.value)}
2993
+ placeholder="All regions"
2994
+ />
2995
+
2996
+ <StyledForm.Slider
2997
+ variant="range"
2998
+ value={minPrice} valueHigh={maxPrice}
2999
+ min={0} max={1000} step={10}
3000
+ onRangeChange={(lo, hi) => { setMinPrice(lo); setMaxPrice(hi) }}
3001
+ formatLabel={(v) => `$${v}`}
3002
+ />
3003
+
3004
+ <Stack horizontal alignItems="center" justifyContent="space-between">
3005
+ <StyledText fontSize={14} fontWeight="600">In stock only</StyledText>
3006
+ <StyledForm.Switch value={inStock} onChange={setInStock} size="sm" />
3007
+ </Stack>
3008
+
3009
+ <StyledForm.Actions horizontal gap={10}>
3010
+ <StyledButton outline compact flex={1} onPress={resetFilters}>
3011
+ <StyledText fontSize={13} fontWeight="600">Reset</StyledText>
3012
+ </StyledButton>
3013
+ <StyledButton primary compact flex={2}
3014
+ backgroundColor={theme.colors.gray[900]}>
3015
+ <StyledText fontSize={13} fontWeight="700" color="#fff">Apply filters</StyledText>
3016
+ </StyledButton>
3017
+ </StyledForm.Actions>
3018
+ </StyledForm>
3019
+ ```
3020
+
3021
+ > **Note:** `StyledForm.Select` wraps `StyledDropdown` and therefore uses the `data` prop (not `options`). `StyledForm.Radio` wraps `StyledRadioGroup` and uses the `options` prop.
3022
+
3023
+ ---
3024
+
3025
+ ### StyledTable
3026
+
3027
+ 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.
3028
+
3029
+ ```tsx
3030
+ import { StyledTable, type TableColumn, usePaginatedQuery } from 'fluent-styles'
3031
+ ```
3032
+
3033
+ #### `TableColumn<T>` definition
3034
+
3035
+ | Field | Type | Description |
3036
+ |---|---|---|
3037
+ | `key` | `string` | Must match a key of the row data object |
3038
+ | `title` | `string` | Column header label |
3039
+ | `width` | `number` | Fixed column width in px. Omit to stretch (flex: 1) |
3040
+ | `align` | `left \| center \| right` | Text alignment (default `left`) |
3041
+ | `sortable` | `boolean` | Allow tapping the header to sort |
3042
+ | `render` | `(value, row, index) => ReactNode` | Custom cell renderer |
3043
+
3044
+ #### `StyledTable` props
3045
+
3046
+ | Prop | Type | Default | Description |
3047
+ |---|---|---|---|
3048
+ | `columns` | `TableColumn<T>[]` | — | Column definitions |
3049
+ | `data` | `T[]` | — | Row data (each row must have a unique `id` field) |
3050
+ | `selectable` | `boolean` | `false` | Show checkboxes for row selection |
3051
+ | `selectedIds` | `(string \| number)[]` | — | Controlled selection |
3052
+ | `onSelectionChange` | `(ids) => void` | — | Selection change callback |
3053
+ | `sortKey` | `string \| null` | — | Controlled sort column |
3054
+ | `sortDirection` | `asc \| desc \| null` | — | Controlled sort direction |
3055
+ | `onSort` | `(key, direction) => void` | — | Sort change callback |
3056
+ | `pagination` | `boolean` | `false` | Enable internal (client-side) pagination |
3057
+ | `pageSize` | `number` | `10` | Rows per page (internal pagination) |
3058
+ | `externalPagination` | `boolean` | `false` | Render `data` as-is — parent manages pages |
3059
+ | `currentPage` | `number` | — | 0-based current page (controlled) |
3060
+ | `totalPages` | `number` | — | Total pages from the datasource |
3061
+ | `totalCount` | `number` | — | Total record count |
3062
+ | `onPageChange` | `(page: number) => void` | — | Page change callback |
3063
+ | `loading` | `boolean` | `false` | Show loading skeleton over rows |
3064
+ | `virtualized` | `boolean` | auto | Use `FlatList` renderer (auto-enabled for >50 rows) |
3065
+ | `striped` | `boolean` | `false` | Alternating row background |
3066
+ | `showDivider` | `boolean` | `false` | Horizontal divider between rows |
3067
+ | `scrollable` | `boolean` | `false` | Horizontal scroll when content overflows |
3068
+ | `emptyText` | `string` | — | Text shown when `data` is empty |
3069
+ | `emptyNode` | `ReactNode` | — | Custom empty state node |
3070
+ | `colors` | `Partial<TableColors>` | — | Color token overrides |
3071
+ | `borderRadius` | `number` | `16` | Outer container border radius |
3072
+ | `bordered` | `boolean` | `true` | Show outer card border |
3073
+ | `cardBreakpoint` | `number` | `768` | Width threshold below which rows switch to card layout |
3074
+ | `forceTable` | `boolean` | `false` | Always render table rows regardless of screen width |
3075
+ | `forceCards` | `boolean` | `false` | Always render card layout regardless of screen width |
3076
+ | `cardRender` | `(row, index, selected, onToggle?) => ReactNode` | — | Custom card renderer for narrow screens |
3077
+ | `onRowPress` | `(row, index) => void` | — | Row tap callback |
3078
+
3079
+ #### Usage
3080
+
3081
+ ```tsx
3082
+ import { StyledTable, type TableColumn, theme, palettes } from 'fluent-styles'
3083
+
3084
+ // ── 1. Minimal — static data, no pagination ────────────────────────────────
3085
+ interface UserRow { id: number; name: string; email: string; role: string }
3086
+
3087
+ const USER_COLS: TableColumn<UserRow>[] = [
3088
+ { key: 'name', title: 'Name', render: (v) => <StyledText fontWeight="700">{v}</StyledText> },
3089
+ { key: 'email', title: 'Email', render: (v) => <StyledText color={theme.colors.gray[500]}>{v}</StyledText> },
3090
+ { key: 'role', title: 'Role', width: 90, align: 'center' },
3091
+ ]
3092
+
3093
+ <StyledTable columns={USER_COLS} data={users} showDivider />
3094
+
3095
+ // ── 2. Sortable + selectable ───────────────────────────────────────────────
3096
+ const [sel, setSel] = useState<(string | number)[]>([])
3097
+
3098
+ const PRODUCT_COLS: TableColumn<ProductRow>[] = [
3099
+ { key: 'name', title: 'Product' },
3100
+ { key: 'price', title: 'Price', width: 80, align: 'right', sortable: true,
3101
+ render: (v) => <StyledText fontWeight="700">${v}</StyledText> },
3102
+ { key: 'stock', title: 'Stock', width: 80, align: 'center', sortable: true },
3103
+ { key: 'status', title: 'Status', width: 110, align: 'center',
3104
+ render: (v) => <StatusBadge label={v} /> },
3105
+ ]
3106
+
3107
+ <StyledTable
3108
+ columns={PRODUCT_COLS}
3109
+ data={products}
3110
+ selectable
3111
+ selectedIds={sel}
3112
+ onSelectionChange={setSel}
3113
+ showDivider
3114
+ pagination
3115
+ pageSize={8}
3116
+ />
3117
+
3118
+ // ── 3. Client-side pagination ──────────────────────────────────────────────
3119
+ <StyledTable
3120
+ columns={ORDER_COLS}
3121
+ data={orders} // pass the full dataset
3122
+ pagination // StyledTable slices it internally
3123
+ pageSize={10}
3124
+ showDivider
3125
+ bordered
3126
+ />
3127
+
3128
+ // ── 4. External pagination (REST API) ─────────────────────────────────────
3129
+ // Use the `usePaginatedQuery` hook to manage page state, sorting, search,
3130
+ // and loading — then spread `table.tableProps` onto StyledTable.
3131
+ const table = usePaginatedQuery<Order>({
3132
+ pageSize: 10,
3133
+ fetcher: async ({ page, pageSize, sortKey, sortDir, search, filters }) => {
3134
+ const res = await api.get('/orders', { params: { page, pageSize, sortKey, sortDir, search, ...filters } })
3135
+ return { data: res.data.items, totalCount: res.data.total }
3136
+ },
3137
+ initialSortKey: 'date',
3138
+ initialSortDir: 'desc',
3139
+ })
3140
+
3141
+ <StyledTable
3142
+ columns={ORDER_COLS}
3143
+ {...table.tableProps} // wires data, loading, pagination state, sort, onSort, onPageChange
3144
+ showDivider
3145
+ bordered
3146
+ />
3147
+
3148
+ // ── 5. Realm / SQLite (synchronous) ───────────────────────────────────────
3149
+ const table = usePaginatedQuery<Employee>({
3150
+ pageSize: 15,
3151
+ realmQuery: ({ page, pageSize, sortKey, sortDir, search }) => {
3152
+ const results = realm.objects('Employee').sorted(sortKey ?? 'name', sortDir === 'desc')
3153
+ return { data: Array.from(results).slice(page * pageSize, (page + 1) * pageSize), totalCount: results.length }
3154
+ },
3155
+ })
3156
+
3157
+ <StyledTable columns={EMP_COLS} {...table.tableProps} showDivider bordered />
3158
+
3159
+ // ── 6. Custom card layout (narrow screens) ────────────────────────────────
3160
+ // `forceCards` renders the custom card renderer regardless of screen width.
3161
+ // `forceTable` always renders the table.
3162
+ // Default: auto-switches at the `cardBreakpoint` (768 px).
3163
+ <StyledTable
3164
+ columns={PRODUCT_COLS}
3165
+ data={products}
3166
+ pagination
3167
+ pageSize={6}
3168
+ forceCards
3169
+ bordered={false}
3170
+ cardRender={(row, index, isSelected, onToggle) => (
3171
+ <ProductCard row={row} isSelected={isSelected} onToggle={onToggle} />
3172
+ )}
3173
+ />
3174
+
3175
+ // ── 7. Striped + dark theme ────────────────────────────────────────────────
3176
+ // Striped
3177
+ <StyledTable columns={USER_COLS} data={users} striped showDivider={false} />
3178
+
3179
+ // Dark theme via color overrides
3180
+ <StyledTable
3181
+ columns={USER_COLS}
3182
+ data={users}
3183
+ showDivider
3184
+ colors={{
3185
+ background: theme.colors.gray[900],
3186
+ headerBg: theme.colors.gray[800],
3187
+ headerText: theme.colors.gray[400],
3188
+ rowBg: theme.colors.gray[900],
3189
+ border: theme.colors.gray[700],
3190
+ divider: theme.colors.gray[700],
3191
+ text: theme.colors.gray[100],
3192
+ sortActive: theme.colors.gray[100],
3193
+ sortInactive: theme.colors.gray[600],
3194
+ selectedBg: palettes.indigo[900],
3195
+ selectedBorder: palettes.indigo[500],
3196
+ }}
3197
+ />
3198
+
3199
+ // ── 8. Custom empty state ─────────────────────────────────────────────────
3200
+ <StyledTable
3201
+ columns={USER_COLS}
3202
+ data={[]}
3203
+ emptyNode={
3204
+ <Stack alignItems="center" gap={8}>
3205
+ <StyledText fontSize={32}>🗂️</StyledText>
3206
+ <StyledText fontSize={15} fontWeight="700">No users yet</StyledText>
3207
+ <StyledText fontSize={13} color={theme.colors.gray[400]}>Invite someone to get started.</StyledText>
3208
+ </Stack>
3209
+ }
3210
+ />
3211
+ ```
3212
+
3213
+ #### `usePaginatedQuery` hook
3214
+
3215
+ Manages page, sort, search, filters, loading, and error for external data sources. Returns `table.tableProps` which can be spread directly onto `StyledTable`.
3216
+
3217
+ ```tsx
3218
+ const table = usePaginatedQuery<T>(options)
3219
+
3220
+ // Spread onto StyledTable:
3221
+ <StyledTable columns={COLS} {...table.tableProps} />
3222
+ ```
3223
+
3224
+ | Option | Type | Description |
3225
+ |---|---|---|
3226
+ | `pageSize` | `number` | Rows per page |
3227
+ | `fetcher` | `async (params) => { data, totalCount }` | Async REST / GraphQL fetcher |
3228
+ | `realmQuery` | `(params) => { data, totalCount }` | Synchronous Realm / SQLite query |
3229
+ | `initialSortKey` | `string` | Initial sort column |
3230
+ | `initialSortDir` | `asc \| desc \| null` | Initial sort direction |
3231
+ | `initialSearch` | `string` | Initial search string |
3232
+ | `initialFilters` | `Record<string, any>` | Initial filter values |
3233
+ | `searchDebounce` | `number` | Search debounce ms (default 300) |
3234
+
3235
+ `table` return value exposes: `data`, `loading`, `error`, `totalCount`, `totalPages`, `page`, `setPage`, `sortKey`, `sortDir`, `setSort`, `search`, `setSearch`, `filters`, `setFilters`, `refresh`, and `tableProps` (ready to spread).
3236
+
3237
+ ---
3238
+
2680
3239
  ## Hooks
2681
3240
 
2682
3241
  All hooks require a `PortalManager` ancestor.