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 +542 -21
- package/lib/commonjs/index.js +24 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/table/index.js +650 -0
- package/lib/commonjs/table/index.js.map +1 -0
- package/lib/commonjs/table/usepaginatedquery.js +181 -0
- package/lib/commonjs/table/usepaginatedquery.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/table/index.js +644 -0
- package/lib/module/table/index.js.map +1 -0
- package/lib/module/table/usepaginatedquery.js +178 -0
- package/lib/module/table/usepaginatedquery.js.map +1 -0
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/table/index.d.ts +107 -0
- package/lib/typescript/table/index.d.ts.map +1 -0
- package/lib/typescript/table/usepaginatedquery.d.ts +114 -0
- package/lib/typescript/table/usepaginatedquery.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/table/index.tsx +843 -0
- 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
|
|
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
|
|
591
|
+
### StyledHeader
|
|
590
592
|
|
|
591
|
-
A
|
|
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
|
|
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="
|
|
621
|
+
title="Notifications"
|
|
598
622
|
titleAlignment="center"
|
|
599
623
|
showBackArrow
|
|
600
624
|
onBackPress={() => navigation.goBack()}
|
|
601
625
|
rightIcon={<SettingsIcon />}
|
|
602
626
|
/>
|
|
603
627
|
|
|
604
|
-
//
|
|
628
|
+
// Multiple right icons
|
|
605
629
|
<StyledHeader
|
|
606
|
-
title="
|
|
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
|
-
|
|
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
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
|
620
|
-
|
|
621
|
-
| `
|
|
622
|
-
| `
|
|
623
|
-
| `
|
|
624
|
-
| `
|
|
625
|
-
| `
|
|
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.
|
package/lib/commonjs/index.js
CHANGED
|
@@ -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
|