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.
- package/README.md +592 -33
- package/lib/commonjs/index.js +24 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/tabBar/TabBar.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/tabBar/TabBar.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/header/index.d.ts +1 -3
- package/lib/typescript/header/index.d.ts.map +1 -1
- package/lib/typescript/icons/backArrow.d.ts +1 -1
- package/lib/typescript/icons/bellFill.d.ts +1 -1
- package/lib/typescript/icons/bellOutline.d.ts +1 -1
- package/lib/typescript/icons/checkmark.d.ts +1 -1
- package/lib/typescript/icons/delete.d.ts +1 -1
- package/lib/typescript/icons/downChevron.d.ts +1 -1
- package/lib/typescript/icons/error.d.ts +1 -1
- package/lib/typescript/icons/forwardArrow.d.ts +1 -1
- package/lib/typescript/icons/info.d.ts +1 -1
- package/lib/typescript/icons/leftChevron.d.ts +1 -1
- package/lib/typescript/icons/rightChevron.d.ts +1 -1
- package/lib/typescript/icons/save.d.ts +1 -1
- package/lib/typescript/icons/success.d.ts +1 -1
- package/lib/typescript/icons/upChevron.d.ts +1 -1
- package/lib/typescript/icons/warning.d.ts +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/tabBar/TabBar.d.ts +2 -3
- package/lib/typescript/tabBar/TabBar.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/lib/typescript/utiles/createIcon.d.ts +1 -1
- package/package.json +31 -41
- package/src/index.ts +2 -0
- package/src/tabBar/TabBar.tsx +1 -1
- 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)
|
|
@@ -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
|
|
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"
|
|
607
632
|
showBackArrow
|
|
608
|
-
|
|
633
|
+
onBackPress={() => navigation.goBack()}
|
|
634
|
+
rightIcon={
|
|
635
|
+
<Stack horizontal gap={8}>
|
|
636
|
+
<SearchIconBtn />
|
|
637
|
+
<MoreIconBtn />
|
|
638
|
+
</Stack>
|
|
639
|
+
}
|
|
609
640
|
/>
|
|
610
641
|
|
|
611
|
-
//
|
|
612
|
-
<
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -1127,18 +1215,42 @@ import { StyledPressable } from 'fluent-styles'
|
|
|
1127
1215
|
|
|
1128
1216
|
### StyledPage / StyledScrollView
|
|
1129
1217
|
|
|
1130
|
-
|
|
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
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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 {
|
|
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
|
-
|
|
1166
|
-
<
|
|
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.
|