fluent-styles 1.62.1 → 1.62.2

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 (48) hide show
  1. package/README.md +81 -3709
  2. package/lib/commonjs/circularProgress/index.js +102 -83
  3. package/lib/commonjs/circularProgress/index.js.map +1 -1
  4. package/lib/commonjs/utiles/styled.js +26 -5
  5. package/lib/commonjs/utiles/styled.js.map +1 -1
  6. package/lib/module/circularProgress/index.js +103 -84
  7. package/lib/module/circularProgress/index.js.map +1 -1
  8. package/lib/module/utiles/styled.js +26 -5
  9. package/lib/module/utiles/styled.js.map +1 -1
  10. package/lib/typescript/badge/index.d.ts +3 -1
  11. package/lib/typescript/badge/index.d.ts.map +1 -1
  12. package/lib/typescript/button/index.d.ts +5 -1
  13. package/lib/typescript/button/index.d.ts.map +1 -1
  14. package/lib/typescript/card/index.d.ts +3 -1
  15. package/lib/typescript/card/index.d.ts.map +1 -1
  16. package/lib/typescript/circularProgress/index.d.ts +4 -5
  17. package/lib/typescript/circularProgress/index.d.ts.map +1 -1
  18. package/lib/typescript/dialog/index.d.ts +3 -1
  19. package/lib/typescript/dialog/index.d.ts.map +1 -1
  20. package/lib/typescript/divider/index.d.ts +3 -1
  21. package/lib/typescript/divider/index.d.ts.map +1 -1
  22. package/lib/typescript/header/index.d.ts +3 -1
  23. package/lib/typescript/header/index.d.ts.map +1 -1
  24. package/lib/typescript/image/index.d.ts +3 -1
  25. package/lib/typescript/image/index.d.ts.map +1 -1
  26. package/lib/typescript/pressable/index.d.ts +3 -1
  27. package/lib/typescript/pressable/index.d.ts.map +1 -1
  28. package/lib/typescript/safeAreaProvider/index.d.ts +3 -1
  29. package/lib/typescript/safeAreaProvider/index.d.ts.map +1 -1
  30. package/lib/typescript/safeAreaView/index.d.ts +3 -1
  31. package/lib/typescript/safeAreaView/index.d.ts.map +1 -1
  32. package/lib/typescript/scrollView/index.d.ts +3 -1
  33. package/lib/typescript/scrollView/index.d.ts.map +1 -1
  34. package/lib/typescript/shape/cycle.d.ts +3 -1
  35. package/lib/typescript/shape/cycle.d.ts.map +1 -1
  36. package/lib/typescript/shape/index.d.ts +3 -1
  37. package/lib/typescript/shape/index.d.ts.map +1 -1
  38. package/lib/typescript/spacer/index.d.ts +3 -1
  39. package/lib/typescript/spacer/index.d.ts.map +1 -1
  40. package/lib/typescript/stack/index.d.ts +9 -3
  41. package/lib/typescript/stack/index.d.ts.map +1 -1
  42. package/lib/typescript/text/index.d.ts +3 -1
  43. package/lib/typescript/text/index.d.ts.map +1 -1
  44. package/lib/typescript/utiles/styled.d.ts +4 -2
  45. package/lib/typescript/utiles/styled.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/circularProgress/index.tsx +177 -164
  48. package/src/utiles/styled.tsx +22 -6
package/README.md CHANGED
@@ -1,3749 +1,121 @@
1
- # Fluent Styles
1
+ # fluent-styles
2
2
 
3
- A comprehensive, TypeScript-first React Native UI library providing production-ready components, hooks, and an imperative service layer — all powered by a portal-based rendering system.
3
+ **A comprehensive TypeScript-first React Native UI library**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/fluent-styles)](https://www.npmjs.com/package/fluent-styles)
6
- [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
+ [![npm downloads](https://img.shields.io/npm/dm/fluent-styles)](https://www.npmjs.com/package/fluent-styles)
7
+ [![license](https://img.shields.io/npm/l/fluent-styles)](LICENSE)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-first-blue)](https://www.typescriptlang.org/)
7
9
 
8
10
  ---
9
11
 
10
- ## Table of Contents
12
+ ## What is it?
11
13
 
12
- - [Installation](#installation)
13
- - [Quick Start](#quick-start)
14
- - [Portal System](#portal-system)
15
- - [Components](#components)
16
- - [StyledButton](#styledbutton)
17
- - [StyledTextInput](#styledtextinput)
18
- - [Switch](#switch)
19
- - [StyledCheckBox](#styledcheckbox)
20
- - [StyledCard](#styledcard)
21
- - [StyledBadge / BadgeWithIcon](#styledbadge--badgewithicon)
22
- - [StyledImage / StyledImageBackground](#styledimage--styledimagebackground)
23
- - [StyledHeader](#styledheader)
24
- - [StyledForm](#styledform)
25
- - [StyledDropdown / StyledMultiSelectDropdown](#styleddropdown--styledmultiselectdropdown)
26
- - [Popup](#popup)
27
- - [Drawer](#drawer)
28
- - [Collapse / CollapseGroup](#collapse--collapsegroup)
29
- - [TabBar](#tabbar)
30
- - [StyledDivider](#styleddivider)
31
- - [StyledSeperator](#styledseperator)
32
- - [Stack](#stack)
33
- - [StyledText](#styledtext)
34
- - [StyledPressable](#styledpressable)
35
- - [StyledPage / StyledScrollView](#styledpage--styledscrollview)
36
- - [StyledSafeAreaView](#styledsafeareaview)
37
- - [Spacer / StyledSpacer](#spacer--styledspacer)
38
- - [StyledShape](#styledshape)
39
- - [Loader](#loader)
40
- - [StyledCircularProgress](#styledcircularprogress)
41
- - [StyledChip](#styledchip)
42
- - [StyledBar](#styledbar)
43
- - [StyledTimeline](#styledtimeline)
44
- - [StyledRadio / StyledRadioGroup](#styledradio--styledradiogroup)
45
- - [StyledProgressBar](#styledprogressbar)
46
- - [StyledSlider](#styledslider)
47
- - [StyledDatePicker](#styleddatepicker)
48
- - [StyledTable](#styledtable)
49
- - [Hooks](#hooks)
50
- - [useToast](#usetoast)
51
- - [useNotification](#usenotification)
52
- - [useDialogue](#usedialogue)
53
- - [useActionSheet](#useactionsheet)
54
- - [useLoader](#useloader)
55
- - [Imperative Services](#imperative-services)
56
- - [toastService](#toastservice)
57
- - [notificationService](#notificationservice)
58
- - [dialogueService](#dialogueservice)
59
- - [actionSheetService](#actionsheetservice)
60
- - [loaderService](#loaderservice)
61
- - [Theme & Tokens](#theme--tokens)
62
- - [Contributing](#contributing)
63
- - [License](#license)
14
+ `fluent-styles` provides 35+ production-ready React Native components built with TypeScript from the ground up. It includes portal-based overlays and imperative service APIs for toasts, dialogs, loaders, and action sheets — no boilerplate required. Drop in the provider and start building immediately.
64
15
 
65
16
  ---
66
17
 
67
18
  ## Installation
68
19
 
69
- ```bash
20
+ ```sh
70
21
  npm install fluent-styles
71
22
  # or
72
23
  yarn add fluent-styles
73
24
  ```
74
25
 
75
- Peer dependencies (install separately if not already present):
76
-
77
- ```bash
78
- npm install react-native-safe-area-context
79
- ```
80
-
81
26
  ---
82
27
 
83
28
  ## Quick Start
84
29
 
85
- Wrap your root component with `GlobalPortalProvider`. This single wrapper enables **all** portal-based UI — toasts, notifications, loaders, dialogues, drawers, and action sheets.
86
-
87
- ```tsx
88
- import { GlobalPortalProvider } from 'fluent-styles'
89
-
90
- export default function App() {
91
- return (
92
- <GlobalPortalProvider>
93
- <YourNavigator />
94
- </GlobalPortalProvider>
95
- )
96
- }
97
- ```
98
-
99
- If you also use the declarative hooks (`useToast`, `useDialogue`, etc.) inside descendant components, nest a `PortalManager` as well:
100
-
101
30
  ```tsx
102
- import { GlobalPortalProvider, PortalManager } from 'fluent-styles'
31
+ import { GlobalPortalProvider, PortalManager } from 'fluent-styles';
103
32
 
104
33
  export default function App() {
105
34
  return (
106
35
  <GlobalPortalProvider>
107
36
  <PortalManager>
108
- <YourNavigator />z
37
+ <YourNavigator />
109
38
  </PortalManager>
110
39
  </GlobalPortalProvider>
111
- )
40
+ );
112
41
  }
113
42
  ```
114
43
 
115
44
  ---
116
45
 
117
- ## Portal System
118
-
119
- Fluent Styles uses a dual-layer portal architecture:
120
-
121
- | Layer | Provider | Use case |
122
- |---|---|---|
123
- | **Global singleton** | `GlobalPortalProvider` | Imperative services (`toastService`, `loaderService`, etc.) — usable outside React |
124
- | **Declarative hooks** | `PortalManager` | Hook APIs (`useToast`, `useDialogue`, etc.) — usable inside React components |
125
-
126
- The low-level `portal` singleton is also exported for advanced use:
127
-
128
- ```ts
129
- import { portal } from 'fluent-styles'
130
-
131
- const id = portal.show(<MyWidget />, { position: 'top', backdrop: false })
132
- setTimeout(() => portal.hide(id), 3000)
133
- ```
134
-
135
- ---
136
-
137
46
  ## Components
138
47
 
139
- ### StyledButton
140
-
141
- A fully themed button with variant, shape, size, and icon support.
142
-
143
- ```tsx
144
- import { StyledButton, theme } from 'fluent-styles'
145
-
146
- // Variants (compact style)
147
- <StyledButton primary compact>
148
- <StyledButton.Text color={theme.colors.white} fontSize={theme.fontSize.small} fontWeight={theme.fontWeight.semiBold}>
149
- Primary
150
- </StyledButton.Text>
151
- </StyledButton>
152
- <StyledButton secondary compact>Secondary</StyledButton>
153
- <StyledButton outline compact>Outline</StyledButton>
154
- <StyledButton ghost compact>Ghost</StyledButton>
155
- <StyledButton link compact>Link</StyledButton>
156
- <StyledButton danger compact>Danger</StyledButton>
157
- <StyledButton success compact>Success</StyledButton>
158
- <StyledButton warning compact>Warning</StyledButton>
159
- <StyledButton disabled compact>Disabled</StyledButton>
160
-
161
- // Sizes: xs | sm | md | lg | xl
162
- {(['xs', 'sm', 'md', 'lg', 'xl'] as const).map((s) => (
163
- <StyledButton key={s} primary compact {...{ [s]: true }}>
164
- <StyledButton.Text color={theme.colors.white}>{s.toUpperCase()}</StyledButton.Text>
165
- </StyledButton>
166
- ))}
167
-
168
- // Shapes
169
- <StyledButton primary compact pill>Pill</StyledButton>
170
- <StyledButton primary compact rounded>Rounded</StyledButton>
171
- <StyledButton backgroundColor={theme.colors.yellow[500]} borderWidth={0} square>Square</StyledButton>
172
-
173
- // Icons — left, right, or both
174
- <StyledButton primary compact leftIcon={<Icon emoji="🚀" />}>Deploy</StyledButton>
175
- <StyledButton outline compact rightIcon={<Icon emoji="→" />}>Continue</StyledButton>
176
- <StyledButton secondary compact leftIcon={<Icon emoji="⬇" />} rightIcon={<Icon emoji="📦" />}>
177
- Download package
178
- </StyledButton>
179
-
180
- // Icon-only circular buttons
181
- <StyledButton icon backgroundColor={theme.colors.indigo[500]}><Icon emoji="✉️" size={18} /></StyledButton>
182
- <StyledButton icon backgroundColor={theme.colors.amber[400]}><Icon emoji="🔔" size={18} /></StyledButton>
183
- <StyledButton icon backgroundColor={theme.colors.red[500]}><Icon emoji="🗑️" size={18} /></StyledButton>
184
-
185
- // Loading state (async example)
186
- const [loading, setLoading] = useState(false)
187
- <StyledButton primary compact loading={loading} onPress={() => { setLoading(true); setTimeout(() => setLoading(false), 2000) }}>
188
- <StyledButton.Text color={theme.colors.white}>{loading ? 'Saving…' : 'Save changes'}</StyledButton.Text>
189
- </StyledButton>
190
-
191
- // Full-width block
192
- <StyledButton primary block>
193
- <StyledButton.Text color={theme.colors.white} fontSize={theme.fontSize.medium} fontWeight={theme.fontWeight.bold}>
194
- Create account
195
- </StyledButton.Text>
196
- </StyledButton>
197
- <StyledButton outline block>
198
- <StyledButton.Text color={theme.colors.gray[800]} fontSize={theme.fontSize.medium}>Sign in instead</StyledButton.Text>
199
- </StyledButton>
200
-
201
- // Disabled
202
- <StyledButton primary disabled>Disabled</StyledButton>
203
- ```
204
-
205
- Accepts all `TouchableOpacityProps` and flat `ViewStyle` props.
206
-
207
- ---
208
-
209
- ### StyledTextInput
210
-
211
- A rich text input with label, validation, addons, and an imperative ref handle.
212
-
213
- ```tsx
214
- import { StyledTextInput } from 'fluent-styles'
215
-
216
- // Variants: outline | filled | underline | ghost
217
- <StyledTextInput variant="outline" label="Email" placeholder="you@example.com" />
218
-
219
- // Sizes: sm | md | lg
220
- <StyledTextInput size="md" label="Medium" />
221
-
222
- // Floating label (Material Design style)
223
- <StyledTextInput floatLabel label="Floating Label" />
224
-
225
- // Validation
226
- <StyledTextInput
227
- label="Username"
228
- required
229
- helperText="Must be unique"
230
- errorMessage="Already taken"
231
- error
232
- />
233
-
234
- // Character counter
235
- <StyledTextInput label="Bio" showCounter maxLength={200} multiline />
236
-
237
- // Icons & addons
238
- <StyledTextInput leftIcon={<SearchIcon />} placeholder="Search…" />
239
- <StyledTextInput
240
- leftAddon={{ text: 'https://', bg: '#f4f4f5' }}
241
- rightAddon={{ text: '.com', bg: '#f4f4f5' }}
242
- placeholder="yoursite"
243
- />
244
-
245
- // States
246
- <StyledTextInput clearable value={value} onChangeText={setValue} />
247
- <StyledTextInput loading />
248
-
249
- // Imperative handle
250
- const inputRef = useRef<StyledTextInputHandle>(null)
251
- <StyledTextInput ref={inputRef} label="Password" secureTextEntry />
252
- inputRef.current?.focus()
253
- inputRef.current?.clear()
254
- inputRef.current?.isFocused()
255
- ```
256
-
257
- **Key props:**
258
-
259
- | Prop | Type | Description |
260
- |---|---|---|
261
- | `label` | `string` | Input label |
262
- | `floatLabel` | `boolean` | Animated floating label |
263
- | `required` | `boolean` | Appends `*` to label |
264
- | `helperText` | `string` | Hint text below input |
265
- | `errorMessage` | `string` | Error text (visible when `error` is true) |
266
- | `error` | `boolean` | Activates error state |
267
- | `showCounter` | `boolean` | Character counter (requires `maxLength`) |
268
- | `variant` | `outline \| filled \| underline \| ghost` | Visual style |
269
- | `size` | `sm \| md \| lg` | Input size |
270
- | `leftIcon / rightIcon` | `ReactNode` | Icon slots |
271
- | `leftAddon / rightAddon` | `{ text?, node?, bg?, color?, onPress? }` | Prefix/suffix addons |
272
- | `clearable` | `boolean` | Clear button when value is present |
273
- | `loading` | `boolean` | Right-side activity spinner |
274
- | `focusColor` | `string` | Border colour on focus |
275
-
276
- ---
277
-
278
- ### Switch
279
-
280
- A generic animated toggle with async confirmation guard and customisable labels.
281
-
282
- ```tsx
283
- import { Switch } from 'fluent-styles'
284
-
285
- // Uncontrolled boolean
286
- <Switch defaultValue={false} onChange={(val) => console.log(val)} />
287
-
288
- // Controlled
289
- <Switch value={isOn} onChange={setIsOn} />
290
-
291
- // Non-boolean (generic types)
292
- <Switch<'yes', 'no'>
293
- activeValue="yes"
294
- inactiveValue="no"
295
- defaultValue="no"
296
- onChange={(val) => console.log(val)}
297
- />
298
-
299
- // Sizes: sm | md | lg
300
- <Switch size="lg" defaultValue />
301
-
302
- // Async guard — return false to cancel toggle
303
- <Switch
304
- beforeChange={async (next) => {
305
- const ok = await confirmDialog()
306
- return ok
307
- }}
308
- />
309
-
310
- // Inline labels & colours
311
- <Switch activeLabel="ON" inactiveLabel="OFF" />
312
- <Switch activeColor="#10b981" inactiveColor="#d4d4d8" />
313
-
314
- // Fine-grained color token overrides
315
- <Switch
316
- defaultValue
317
- colors={{
318
- activeTrack: palettes.rose[500],
319
- inactiveTrack: palettes.rose[100],
320
- inactiveBorder: palettes.rose[200],
321
- activeLabelText: '#fff',
322
- }}
323
- />
324
- <Switch defaultValue activeColor={palettes.amber[400]} inactiveColor={palettes.amber[100]} />
325
-
326
- // Teal with labels
327
- <Switch
328
- size="lg"
329
- defaultValue
330
- activeLabel="ON"
331
- inactiveLabel="OFF"
332
- colors={{
333
- activeTrack: palettes.teal[500],
334
- inactiveTrack: palettes.teal[100],
335
- activeLabelText: '#fff',
336
- inactiveLabelText: palettes.teal[400],
337
- }}
338
- />
339
-
340
- // On a dark background (slate palette)
341
- <Switch
342
- defaultValue
343
- colors={{
344
- activeTrack: palettes.indigo[400],
345
- inactiveTrack: palettes.blueGray[700],
346
- inactiveBorder: palettes.blueGray[600],
347
- thumb: '#ffffff',
348
- }}
349
- />
350
-
351
- // Always-rejected guard (demonstrates async guard returning false)
352
- <Switch defaultValue={false} beforeChange={() => Promise.resolve(false)} />
353
-
354
- // States
355
- <Switch loading />
356
- <Switch disabled />
357
- ```
358
-
359
- **Key props:**
360
-
361
- | Prop | Type | Description |
362
- |---|---|---|
363
- | `value` | `T` | Controlled value |
364
- | `defaultValue` | `T` | Uncontrolled initial value |
365
- | `activeValue / inactiveValue` | `T` | Values for on/off (default `true`/`false`) |
366
- | `onChange` | `(val: T) => void` | Change callback |
367
- | `beforeChange` | `(next: T) => boolean \| Promise<boolean>` | Async guard |
368
- | `size` | `sm \| md \| lg` | Track size preset |
369
- | `activeLabel / inactiveLabel` | `string \| ReactNode` | Label inside track |
370
- | `activeColor / inactiveColor` | `string` | Track colour overrides |
371
- | `loading` | `boolean` | Replaces thumb with a spinner |
372
- | `disabled` | `boolean` | Disables interaction |
373
- | `colors` | `Partial<SwitchColors>` | Fine-grained token overrides |
374
-
375
- ---
376
-
377
- ### StyledCheckBox
378
-
379
- An accessible checkbox with customisable size and colour.
380
-
381
- ```tsx
382
- import { StyledCheckBox, StyledCard, StyledText, Stack, theme } from 'fluent-styles'
383
-
384
- // Basic
385
- <StyledCheckBox checked={isChecked} onCheck={setChecked} />
386
-
387
- // Sizes: 18 | 24 | 32 | 40
388
- <Stack horizontal gap={16} alignItems="center">
389
- <StyledCheckBox checked size={18} onCheck={() => {}} />
390
- <StyledCheckBox checked size={24} onCheck={() => {}} />
391
- <StyledCheckBox checked size={32} onCheck={() => {}} />
392
- <StyledCheckBox checked size={40} onCheck={() => {}} />
393
- </Stack>
394
-
395
- // Custom colors
396
- <StyledCheckBox checked checkedColor={theme.colors.green[500]} checkMarkColor="#fff" onCheck={() => {}} />
397
- <StyledCheckBox checked checkedColor={theme.colors.blue[600]} checkMarkColor="#fff" onCheck={() => {}} />
398
- <StyledCheckBox checked checkedColor={theme.colors.rose[500]} checkMarkColor="#fff" onCheck={() => {}} />
399
-
400
- // Disabled states
401
- <StyledCheckBox checked={false} disabled onCheck={() => {}} />
402
- <StyledCheckBox checked disabled onCheck={() => {}} />
403
-
404
- // --- Real-world: Settings preferences card ---
405
- <StyledCard backgroundColor={theme.colors.white} borderRadius={18} padding={16} shadow="light">
406
- <Stack gap={18}>
407
- <StyledText fontSize={18} fontWeight={800}>Preferences</StyledText>
408
- {[{ label: 'Product updates', checked: updates, setter: setUpdates },
409
- { label: 'Marketing emails', checked: marketing, setter: setMarketing },
410
- { label: 'Push notifications', checked: notifs, setter: setNotifs }]
411
- .map(({ label, checked, setter }) => (
412
- <Stack key={label} horizontal alignItems="center" gap={12}>
413
- <StyledCheckBox checked={checked} onCheck={setter} />
414
- <StyledText fontSize={15} fontWeight={600}>{label}</StyledText>
415
- </Stack>
416
- ))}
417
- </Stack>
418
- </StyledCard>
419
-
420
- // --- Real-world: Task list with green checkmarks ---
421
- <Stack gap={16}>
422
- {tasks.map(({ key, label, helper }) => (
423
- <Stack key={key} horizontal alignItems="center" gap={12}>
424
- <StyledCheckBox
425
- checked={done[key]}
426
- onCheck={(v) => setDone(prev => ({ ...prev, [key]: v }))}
427
- checkedColor={theme.colors.green[500]}
428
- checkMarkColor="#fff"
429
- />
430
- <Stack flex={1}>
431
- <StyledText fontSize={15} fontWeight={600}>{label}</StyledText>
432
- {helper && <StyledText fontSize={13} color={theme.colors.gray[500]}>{helper}</StyledText>}
433
- </Stack>
434
- </Stack>
435
- ))}
436
- </Stack>
437
-
438
- // --- Compact inline usage ---
439
- <Stack horizontal alignItems="center" gap={10}>
440
- <StyledCheckBox checked={remember} onCheck={setRemember} size={20} />
441
- <StyledText>Remember me</StyledText>
442
- </Stack>
443
- ```
444
-
445
- ---
446
-
447
- ### StyledCard
448
-
449
- A flexible container with optional shadow levels and pressable wrapper.
450
-
451
- ```tsx
452
- import { StyledCard } from 'fluent-styles'
453
-
454
- <StyledCard shadow="light" padding={16} borderRadius={12}>
455
- <StyledText>Card content</StyledText>
456
- </StyledCard>
457
-
458
- // Pressable card
459
- <StyledCard
460
- shadow="medium"
461
- pressable
462
- pressableProps={{ onPress: () => navigate('Detail') }}
463
- >
464
- <StyledText>Tap me</StyledText>
465
- </StyledCard>
466
- ```
467
-
468
- **Shadow levels:** `light` | `lightMedium` | `medium` | `mediumDark` | `dark` | `veryDark`
469
-
470
- Accepts all `ViewProps` and flat `ViewStyle` props.
471
-
472
- ---
473
-
474
- ### StyledBadge / BadgeWithIcon / BadgeIcon
475
-
476
- Styled text badges, icon badges, and notification count overlays.
477
-
478
- ```tsx
479
- import { StyledBadge, BadgeWithIcon, BadgeIcon, StyledImage, Stack, theme } from 'fluent-styles'
480
-
481
- // --- Pill status badges ---
482
- <Stack horizontal gap={10} flexWrap="wrap">
483
- <StyledBadge
484
- backgroundColor={theme.colors.green[50]}
485
- color={theme.colors.green[700]}
486
- paddingHorizontal={10} paddingVertical={6} borderRadius={999}
487
- >Active</StyledBadge>
488
-
489
- <StyledBadge
490
- backgroundColor={theme.colors.red[50]}
491
- color={theme.colors.red[600]}
492
- paddingHorizontal={10} paddingVertical={6} borderRadius={999}
493
- >Rejected</StyledBadge>
494
-
495
- <StyledBadge
496
- backgroundColor={theme.colors.blue[50]}
497
- color={theme.colors.blue[700]}
498
- paddingHorizontal={10} paddingVertical={6} borderRadius={999}
499
- >New</StyledBadge>
500
- </Stack>
501
-
502
- // --- Link badge ---
503
- <StyledBadge link>View details</StyledBadge>
504
-
505
- // --- BadgeWithIcon: feature / status badges ---
506
- <BadgeWithIcon
507
- title="Featured"
508
- iconLeft={<Text>⭐</Text>}
509
- backgroundColor={theme.colors.yellow[50]}
510
- paddingHorizontal={12} paddingVertical={7} borderRadius={999} gap={6}
511
- />
512
- <BadgeWithIcon
513
- title="Verified"
514
- iconLeft={<Text>✅</Text>}
515
- backgroundColor={theme.colors.green[50]}
516
- paddingHorizontal={12} paddingVertical={7} borderRadius={999} gap={6}
517
- />
518
-
519
- // --- Status badges (workflow states) ---
520
- <BadgeWithIcon title="In progress" iconLeft={<Text>🟡</Text>} color={theme.colors.yellow[700]}
521
- backgroundColor={theme.colors.yellow[50]} paddingHorizontal={12} paddingVertical={8} borderRadius={999} gap={6} />
522
- <BadgeWithIcon title="Completed" iconLeft={<Text>🟢</Text>} color={theme.colors.green[700]}
523
- backgroundColor={theme.colors.green[50]} paddingHorizontal={12} paddingVertical={8} borderRadius={999} gap={6} />
524
- <BadgeWithIcon title="Blocked" iconLeft={<Text>🔴</Text>} color={theme.colors.red[700]}
525
- backgroundColor={theme.colors.red[50]} paddingHorizontal={12} paddingVertical={8} borderRadius={999} gap={6} />
526
-
527
- // --- BadgeIcon: count bubbles ---
528
- <Stack horizontal gap={24} alignItems="center">
529
- <BadgeIcon char="1" size={24} />
530
- <BadgeIcon char="3" backgroundColor={theme.colors.blue[600]} size={24} />
531
- <BadgeIcon char="9+" backgroundColor={theme.colors.gray[800]} size={24} />
532
- </Stack>
533
-
534
- // --- BadgeIcon over an icon (notification dot) ---
535
- <BadgeIcon icon={<Text style={{ fontSize: 24 }}>🔔</Text>} char="2" right={20} top={-12} size={16} />
536
- <BadgeIcon icon={<Text style={{ fontSize: 24 }}>🛒</Text>} char="4" backgroundColor={theme.colors.green[600]} right={16} top={-12} size={16} />
537
-
538
- // --- Overlay badge on an image ---
539
- <Stack>
540
- <StyledImage source={{ uri: '…' }} width={220} height={150} borderRadius={18} />
541
- <Stack position="absolute" top={10} right={10}>
542
- <StyledBadge backgroundColor="rgba(17,24,39,0.78)" color="#fff"
543
- paddingHorizontal={10} paddingVertical={6} borderRadius={999} fontWeight="700">New</StyledBadge>
544
- </Stack>
545
- </Stack>
546
-
547
- // --- Avatar with notification count ---
548
- <Stack>
549
- <StyledImage source={{ uri: '…' }} cycle size={64} borderRadius={999} />
550
- <Stack position="absolute" top={2} right={2}>
551
- <BadgeIcon char="3" size={18} />
552
- </Stack>
553
- </Stack>
554
-
555
- // --- Product badges ---
556
- <Stack horizontal gap={10} flexWrap="wrap">
557
- <StyledBadge backgroundColor={theme.colors.red[50]} color={theme.colors.red[600]}
558
- paddingHorizontal={10} paddingVertical={6} borderRadius={999} fontWeight="700">Sale</StyledBadge>
559
- <StyledBadge backgroundColor={theme.colors.gray[900]} color={theme.colors.white}
560
- paddingHorizontal={10} paddingVertical={6} borderRadius={999} fontWeight="700">Limited</StyledBadge>
561
- <BadgeWithIcon title="Free delivery" iconLeft={<Text>🚚</Text>} backgroundColor={theme.colors.green[50]}
562
- paddingHorizontal={12} paddingVertical={7} borderRadius={999} gap={6} />
563
- </Stack>
564
- ```
565
-
566
- **`BadgeIcon` props:** `char`, `icon?`, `size?`, `backgroundColor?`, `top?`, `right?`
567
-
568
- ---
569
-
570
- ### StyledImage / StyledImageBackground
571
-
572
- Styled wrappers around React Native's `Image` and `ImageBackground`.
573
-
574
- ```tsx
575
- import { StyledImage, StyledImageBackground } from 'fluent-styles'
576
-
577
- // Fixed dimensions
578
- <StyledImage source={{ uri: 'https://…' }} width={120} height={80} borderRadius={8} />
579
-
580
- // Circular avatar (cycle + size)
581
- <StyledImage source={{ uri: 'https://…' }} cycle size={64} />
582
-
583
- // Background image with overlay
584
- <StyledImageBackground source={require('./assets/hero.jpg')} height={200} borderRadius={12}>
585
- <StyledText color="#fff">Overlay text</StyledText>
586
- </StyledImageBackground>
587
- ```
588
-
589
- ---
590
-
591
- ### StyledHeader
592
-
593
- A composable navigation header with status bar management, slot-based layout, and a `StyledHeader.Full` escape hatch for fully custom content.
594
-
595
- ```tsx
596
- import { StyledHeader } from 'fluent-styles'
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 ───────────────────────────────────────────────────────
620
- <StyledHeader
621
- title="Notifications"
622
- titleAlignment="center"
623
- showBackArrow
624
- onBackPress={() => navigation.goBack()}
625
- rightIcon={<SettingsIcon />}
626
- />
627
-
628
- // Multiple right icons
629
- <StyledHeader
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"
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>
693
- ```
694
-
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.
714
-
715
- ---
716
-
717
- ### StyledDropdown / StyledMultiSelectDropdown
718
-
719
- Modal-based dropdowns with optional search, icons, and subtitles.
720
-
721
- ```tsx
722
- import { StyledDropdown, StyledMultiSelectDropdown } from 'fluent-styles'
723
-
724
- const options = [
725
- { value: 'react', label: 'React Native' },
726
- { value: 'flutter', label: 'Flutter', subtitle: 'by Google' },
727
- { value: 'ionic', label: 'Ionic', disabled: true },
728
- ]
729
-
730
- // Single-select
731
- <StyledDropdown
732
- label="Framework"
733
- placeholder="Pick one…"
734
- options={options}
735
- value={selected}
736
- onChange={(item) => setSelected(item.value)}
737
- variant="outline"
738
- size="md"
739
- />
740
-
741
- // With search
742
- <StyledDropdown label="Country" options={countryOptions} searchable searchPlaceholder="Filter…" />
743
-
744
- // Multi-select
745
- <StyledMultiSelectDropdown label="Tags" options={options} value={selectedTags} onChange={setSelectedTags} />
746
- ```
747
-
748
- **DropdownOptionItem:** `value`, `label`, `icon?`, `subtitle?`, `disabled?`, `meta?`
749
-
750
- **Trigger props:** `variant` (`outline | filled | underline | ghost`), `size` (`sm | md | lg`), `label`, `placeholder`, `disabled`
751
-
752
- ---
753
-
754
- ### Popup
755
-
756
- A versatile overlay with multiple positions, animation styles, and a built-in header.
757
-
758
- ```tsx
759
- import { Popup } from 'fluent-styles'
760
-
761
- // --- Bottom sheet variants ---
762
- <Popup visible={visible} onClose={hide}>Plain content — no header</Popup>
763
- <Popup visible={visible} onClose={hide} title="Post options" showClose><ActionList /></Popup>
764
- <Popup visible={visible} onClose={hide} title="Share post" subtitle="Choose where to send" showClose><ShareList /></Popup>
765
- <Popup visible={visible} onClose={hide} title="Safe area" showClose safeAreaBottom>…</Popup>
766
-
767
- // No backdrop
768
- <Popup visible={visible} onClose={hide} title="No backdrop" overlay={false} showClose>…</Popup>
769
-
770
- // Prevent dismiss on backdrop tap
771
- <Popup visible={visible} onClose={hide} title="Tap overlay — nothing" showClose closeOnPressOverlay={false}>…</Popup>
772
-
773
- // --- Positions ---
774
- <Popup visible={visible} onClose={hide} position="top" title="Notification" showClose>…</Popup>
775
- <Popup visible={visible} onClose={hide} position="left" title="Side menu" showClose>…</Popup>
776
- <Popup visible={visible} onClose={hide} position="right" title="Filters" showClose>…</Popup>
777
- <Popup visible={visible} onClose={hide} position="center" title="Confirm" showClose round>…</Popup>
778
-
779
- // --- Animation styles ---
780
- <Popup visible={visible} onClose={hide} animation="slide" title="Slide" showClose>…</Popup>
781
- <Popup visible={visible} onClose={hide} animation="fade" title="Fade" showClose>…</Popup>
782
- <Popup visible={visible} onClose={hide} position="center" animation="scale" title="Scale" showClose round>…</Popup>
783
- <Popup visible={visible} onClose={hide} animation="none" title="Instant" showClose>…</Popup>
784
-
785
- // Spring physics
786
- <Popup visible={visible} onClose={hide} title="Spring" showClose spring={{ damping: 18, stiffness: 280 }}>…</Popup>
787
-
788
- // --- Corner rounding ---
789
- <Popup visible={visible} onClose={hide} round title="Default 20 px" showClose>…</Popup>
790
- <Popup visible={visible} onClose={hide} round={false} title="Square corners" showClose>…</Popup>
791
- <Popup visible={visible} onClose={hide} round roundRadius={36} title="Large radius" showClose>…</Popup>
792
-
793
- // --- Render strategy ---
794
- <Popup visible={visible} onClose={hide} lazyRender title="Lazy (default)" showClose>…</Popup>
795
- <Popup visible={visible} onClose={hide} destroyOnClose title="Destroy on close" showClose>…</Popup>
796
-
797
- // --- Color token overrides ---
798
- <Popup
799
- visible={visible} onClose={hide}
800
- title="Dark slate" subtitle="Token overrides" showClose
801
- colors={{
802
- background: palettes.blueGray[900],
803
- overlay: 'rgba(0,0,0,0.75)',
804
- handle: palettes.blueGray[600],
805
- headerTitle: palettes.blueGray[100],
806
- headerSubtitle: palettes.blueGray[400],
807
- headerBorder: palettes.blueGray[700],
808
- closeIcon: palettes.blueGray[300],
809
- closeIconBg: palettes.blueGray[700],
810
- }}
811
- >…</Popup>
812
-
813
- <Popup
814
- visible={visible} onClose={hide} title="Indigo surface" showClose
815
- colors={{ background: palettes.indigo[50], headerTitle: palettes.indigo[900], handle: palettes.indigo[300], closeIconBg: palettes.indigo[100] }}
816
- >…</Popup>
817
-
818
- // --- Lifecycle callbacks ---
819
- <Popup
820
- visible={visible} onClose={hide} title="Lifecycle" showClose
821
- onOpen={() => console.log('onOpen')}
822
- onOpened={() => console.log('onOpened — animation done')}
823
- onClosed={() => console.log('onClosed — animation done')}
824
- >…</Popup>
825
- ```
826
-
827
- | Prop | Type | Default | Description |
828
- |---|---|---|---|
829
- | `visible` | `boolean` | — | Controls visibility |
830
- | `position` | `top \| bottom \| left \| right \| center` | `bottom` | Entry edge |
831
- | `animation` | `slide \| fade \| scale \| none` | auto | Animation style |
832
- | `overlay` | `boolean` | `true` | Show backdrop |
833
- | `closeOnPressOverlay` | `boolean` | `true` | Dismiss on backdrop tap |
834
- | `round / roundRadius` | `boolean / number` | `true / 20` | Rounded interior corners |
835
- | `title / subtitle` | `ReactNode` | — | Built-in header content |
836
- | `showHandle` | `boolean` | auto | Drag handle pill |
837
- | `showClose` | `boolean` | `false` | Close (×) button |
838
- | `safeAreaBottom` | `boolean` | `false` | Padding for home bar |
839
- | `lazyRender` | `boolean` | `true` | Mount children on first open |
840
- | `destroyOnClose` | `boolean` | `false` | Unmount children when closed |
841
- | `spring` | `{ damping, stiffness, mass? }` | — | Spring physics override |
842
- | `colors` | `Partial<PopupColors>` | — | Token overrides |
843
-
844
- **Lifecycle callbacks:** `onOpen`, `onOpened`, `onClose`, `onClosed`
845
-
846
- ---
847
-
848
- ### Drawer
849
-
850
- A swipeable side panel with built-in navigation list, header, and footer slot.
851
-
852
- ```tsx
853
- import { Drawer } from 'fluent-styles'
854
-
855
- <Drawer
856
- visible={isOpen}
857
- side="left"
858
- title="Menu"
859
- onClose={() => setOpen(false)}
860
- navItems={[
861
- { key: 'home', label: 'Home', icon: '🏠', active: true, onPress: () => navigate('Home') },
862
- { key: 'profile', label: 'Profile', icon: '👤', onPress: () => navigate('Profile') },
863
- { key: 'logout', label: 'Logout', icon: '🚪', section: 'Account', onPress: logout },
864
- ]}
865
- footer={<UserProfileRow />}
866
- />
867
- ```
868
-
869
- | Prop | Type | Default | Description |
870
- |---|---|---|---|
871
- | `visible` | `boolean` | — | Controls visibility |
872
- | `side` | `left \| right` | `left` | Entry edge |
873
- | `width` | `number \| string` | `'78%'` | Drawer width |
874
- | `navItems` | `DrawerNavItem[]` | — | Built-in nav list (auto-grouped by `section`) |
875
- | `footer` | `ReactNode` | — | Pinned footer content |
876
- | `swipeToClose` | `boolean` | `true` | Pan gesture dismiss |
877
- | `swipeThreshold` | `number` | `0.4` | Fraction of width to trigger dismiss |
878
- | `title / subtitle` | `ReactNode` | — | Header content |
879
- | `headerRight` | `ReactNode` | — | Header right slot |
880
- | `colors` | `Partial<DrawerColors>` | — | Token overrides |
881
-
882
- **DrawerNavItem:** `key`, `label`, `icon?`, `badge?`, `section?`, `active?`, `disabled?`, `onPress?`
883
-
884
- ---
885
-
886
- ### Collapse / CollapseGroup
887
-
888
- Animated accordion panels with full render-slot control.
889
-
890
- ```tsx
891
- import { Collapse, CollapseGroup, CollapseItem, palettes, theme } from 'fluent-styles'
892
-
893
- // --- Variants ---
894
- <Collapse title="Cell (default)" variant="cell">…</Collapse>
895
- <Collapse title="Card" subtitle="Shadow + radius" variant="card">…</Collapse>
896
- <Collapse title="Bordered" variant="bordered">…</Collapse>
897
- <Collapse title="Ghost" variant="ghost">…</Collapse>
898
-
899
- // --- Sizes ---
900
- <Collapse title="Small" variant="bordered" size="sm">…</Collapse>
901
- <Collapse title="Medium" variant="bordered" size="md">…</Collapse>
902
- <Collapse title="Large" variant="bordered" size="lg">…</Collapse>
903
-
904
- // --- Header slots: leading · subtitle · trailing ---
905
- <Collapse
906
- variant="card"
907
- leading={<Text style={{ fontSize: 20 }}>📦</Text>}
908
- title="Leading icon"
909
- subtitle="Any ReactNode on the left"
910
- >…</Collapse>
911
-
912
- <Collapse
913
- variant="card"
914
- title="Trailing badge"
915
- trailing={<View style={{ backgroundColor: palettes.indigo[500], borderRadius: 10, paddingHorizontal: 8 }}><Text style={{ color: '#fff', fontWeight: '700' }}>NEW</Text></View>}
916
- >…</Collapse>
917
-
918
- // --- Active header tint ---
919
- <Collapse title="Tints when open" variant="bordered" activeHeader>…</Collapse>
920
-
921
- // --- Disabled ---
922
- <Collapse title="Premium feature" subtitle="Upgrade to unlock" variant="bordered" disabled>…</Collapse>
923
-
924
- // --- Default open (uncontrolled) ---
925
- <Collapse title="Starts expanded" variant="card" defaultCollapse>…</Collapse>
926
-
927
- // --- Controlled open state ---
928
- const [open, setOpen] = useState(false)
929
- <Collapse
930
- title="Externally driven"
931
- variant="bordered"
932
- collapse={open}
933
- onCollapse={setOpen}
934
- >…</Collapse>
935
-
936
- // --- Custom header renderer ---
937
- <Collapse
938
- variant="card"
939
- renderHeader={(open) => (
940
- <View style={{ padding: 14, backgroundColor: open ? '#eef2ff' : '#f2f2f7' }}>
941
- <Text style={{ fontWeight: '600' }}>{open ? '▲ Open' : '▼ Closed'}</Text>
942
- </View>
943
- )}
944
- >…</Collapse>
945
-
946
- // --- Custom header right (keep title, replace right side) ---
947
- <Collapse
948
- title="Custom right"
949
- variant="bordered"
950
- renderHeaderRight={(open, chevron) => (
951
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
952
- <Text style={{ color: open ? '#6366f1' : '#8e8e93' }}>{open ? 'Open' : 'Closed'}</Text>
953
- {chevron}
954
- </View>
955
- )}
956
- >…</Collapse>
957
-
958
- // --- Color token overrides ---
959
- <Collapse
960
- title="Dark slate theme"
961
- variant="card"
962
- colors={{
963
- background: theme.colors.blueGray[900],
964
- border: theme.colors.blueGray[700],
965
- titleColor: theme.colors.blueGray[100],
966
- subtitleColor: theme.colors.blueGray[400],
967
- iconColor: theme.colors.blueGray[400],
968
- activeHeaderBg: palettes.blueGray[800],
969
- }}
970
- >…</Collapse>
971
-
972
- <Collapse
973
- title="Warm amber theme"
974
- variant="bordered"
975
- colors={{
976
- background: palettes.amber[50],
977
- border: palettes.amber[300],
978
- titleColor: palettes.amber[900],
979
- iconColor: palettes.amber[500],
980
- activeHeaderBg: palettes.amber[100],
981
- }}
982
- >…</Collapse>
983
-
984
- // --- CollapseGroup: multi-open with defaultActiveKey array ---
985
- <CollapseGroup variant="bordered" defaultActiveKey={['shipping']}>
986
- <CollapseItem itemKey="shipping" title="Shipping" subtitle="2–5 business days">…</CollapseItem>
987
- <CollapseItem itemKey="returns" title="Returns" subtitle="30-day policy">…</CollapseItem>
988
- <CollapseItem itemKey="sizing" title="Size guide">…</CollapseItem>
989
- </CollapseGroup>
990
-
991
- // --- CollapseGroup: accordion FAQ with icons ---
992
- <CollapseGroup accordion variant="card" defaultActiveKey="q1" style={{ gap: 8 }}>
993
- <CollapseItem itemKey="q1" leading={<Text style={{ fontSize: 18 }}>💳</Text>} title="Accepted payment methods">…</CollapseItem>
994
- <CollapseItem itemKey="q2" leading={<Text style={{ fontSize: 18 }}>🔒</Text>} title="Is my data secure?">…</CollapseItem>
995
- <CollapseItem itemKey="q3" leading={<Text style={{ fontSize: 18 }}>📞</Text>} title="How do I contact support?">…</CollapseItem>
996
- </CollapseGroup>
997
- ```
998
-
999
- | Prop | Type | Default | Description |
1000
- |---|---|---|---|
1001
- | `title / subtitle` | `ReactNode` | — | Header content |
1002
- | `leading / trailing` | `ReactNode` | — | Header side slots |
1003
- | `variant` | `cell \| card \| bordered \| ghost` | `cell` | Visual style |
1004
- | `size` | `sm \| md \| lg` | `md` | Padding scale |
1005
- | `collapse / defaultCollapse` | `boolean` | — | Controlled / uncontrolled open state |
1006
- | `onCollapse` | `(open: boolean) => void` | — | Toggle callback |
1007
- | `activeHeader` | `boolean` | `false` | Tint header when open |
1008
- | `disabled` | `boolean` | `false` | Prevent interaction |
1009
- | `lazyRender` | `boolean` | `true` | Mount body on first expand |
1010
- | `destroyOnClose` | `boolean` | `false` | Unmount body when collapsed |
1011
- | `renderHeader` | `(open: boolean) => ReactNode` | — | Replace the entire header |
1012
- | `renderHeaderRight` | `(open, chevron) => ReactNode` | — | Replace only the right side of the header |
1013
- | `colors` | `Partial<CollapseColors>` | — | Token overrides |
1014
-
1015
- `CollapseGroup` additional props: `accordion` (single-open), `defaultActiveKey` (`string \| string[]`)
1016
-
1017
- ---
1018
-
1019
- ### TabBar
1020
-
1021
- A feature-rich animated tab bar with badge, icon, and indicator support.
1022
-
1023
- ```tsx
1024
- import { TabBar, TabItem, palettes } from 'fluent-styles'
1025
-
1026
- // --- Bottom nav with icons + dot badges ---
1027
- type Nav = 'home' | 'explore' | 'activity' | 'profile'
1028
- const NAV_TABS: TabItem<Nav>[] = [
1029
- { value: 'home', label: 'Home', iconRender: (c) => <HomeIcon color={c} /> },
1030
- { value: 'explore', label: 'Explore', iconRender: (c) => <SearchIcon color={c} />, badge: 3 },
1031
- { value: 'activity', label: 'Activity', iconRender: (c) => <BellIcon color={c} />, badge: '' }, // '' = dot badge
1032
- { value: 'profile', label: 'Profile', iconRender: (c) => <UserIcon color={c} /> },
1033
- ]
1034
- <TabBar options={NAV_TABS} value={nav} onChange={setNav} indicator="dot" showBorder />
1035
-
1036
- // --- Animated underline indicator ---
1037
- <TabBar options={SIMPLE_TABS} value={seg} onChange={setSeg} indicator="line" showBorder />
1038
-
1039
- // --- Sliding pill indicator ---
1040
- <TabBar
1041
- options={SIMPLE_TABS}
1042
- defaultValue="week"
1043
- indicator="pill"
1044
- colors={{ background: palettes.indigo[50], activeText: palettes.indigo[700], indicator: palettes.indigo[200], text: palettes.indigo[400] }}
1045
- />
1046
-
1047
- // --- Scrollable tabs (many items) ---
1048
- <TabBar options={MANY_TABS} value={cat} onChange={setCat} tabAlign="scroll" indicator="line" showBorder />
1049
-
1050
- // --- Disabled tabs ---
1051
- const TABS_WITH_DISABLED: TabItem<string>[] = [
1052
- { value: 'available', label: 'Available' },
1053
- { value: 'locked', label: 'Locked', disabled: true },
1054
- { value: 'open', label: 'Open' },
1055
- ]
1056
- <TabBar options={TABS_WITH_DISABLED} value={active} onChange={setActive} indicator="line" showBorder />
1057
-
1058
- // --- Solid / chip variant ---
1059
- <TabBar
1060
- options={SIMPLE_TABS}
1061
- defaultValue="week"
1062
- variant="solid"
1063
- indicator="pill"
1064
- colors={{
1065
- background: palettes.gray[100],
1066
- activeChipBg: '#ffffff',
1067
- activeChipText: palettes.gray[900],
1068
- indicator: palettes.coolGray[200],
1069
- text: palettes.gray[500],
1070
- }}
1071
- />
1072
-
1073
- // --- Numeric values (step wizard) ---
1074
- type Step = 1 | 2 | 3
1075
- const STEPS: TabItem<Step>[] = [
1076
- { value: 1, label: 'Step 1' },
1077
- { value: 2, label: 'Step 2' },
1078
- { value: 3, label: 'Step 3' },
1079
- ]
1080
- <TabBar
1081
- options={STEPS}
1082
- value={step}
1083
- onChange={setStep}
1084
- indicator="line"
1085
- colors={{ activeText: palettes.violet[600], indicator: palettes.violet[600], text: palettes.gray[400] }}
1086
- showBorder
1087
- />
1088
-
1089
- // --- Custom indicator sizing ---
1090
- <TabBar options={SIMPLE_TABS} defaultValue="month" indicator="line" indicatorWidth={24} indicatorHeight={3} indicatorRadius={3} showBorder />
1091
-
1092
- // --- Label scale on active tab ---
1093
- <TabBar options={SIMPLE_TABS} defaultValue="week" indicator="line" labelBulge={1.15} showBorder />
1094
-
1095
- // --- Color overrides: green theme ---
1096
- <TabBar
1097
- options={SIMPLE_TABS}
1098
- defaultValue="day"
1099
- indicator="line"
1100
- showBorder
1101
- colors={{ background: palettes.green[50], activeText: palettes.green[700], indicator: palettes.green[500], text: palettes.green[400], border: palettes.green[200] }}
1102
- />
1103
-
1104
- // --- Color overrides: dark slate ---
1105
- <TabBar
1106
- options={NAV_TABS}
1107
- defaultValue="home"
1108
- indicator="dot"
1109
- showBorder
1110
- colors={{ background: palettes.blueGray[900], activeText: palettes.indigo[400], indicator: palettes.indigo[400], text: palettes.blueGray[400], border: palettes.blueGray[700], badge: palettes.rose[400] }}
1111
- />
1112
-
1113
- // --- Large font / taller bar ---
1114
- <TabBar options={SIMPLE_TABS} defaultValue="week" indicator="line" fontSize={17} height={52} showBorder />
1115
- ```
1116
-
1117
- | Prop | Type | Default | Description |
1118
- |---|---|---|---|
1119
- | `options` | `TabItem<T>[]` | — | Tab definitions |
1120
- | `value / defaultValue` | `T` | — | Controlled / uncontrolled value |
1121
- | `onChange` | `(val: T) => void` | — | Change callback |
1122
- | `variant` | `default \| underline \| card \| solid` | `default` | Visual preset |
1123
- | `indicator` | `false \| line \| pill \| dot` | `false` | Animated indicator style |
1124
- | `indicatorColor` | `ColorValue` | — | Indicator colour override |
1125
- | `indicatorWidth` | `number` | auto | Fixed indicator width (0 = full tab width) |
1126
- | `indicatorHeight` | `number` | `2` | Indicator thickness |
1127
- | `indicatorRadius` | `number` | auto | Indicator border radius |
1128
- | `tabAlign` | `center \| scroll` | `center` | Equal-width or scrolling tabs |
1129
- | `labelBulge` | `number \| boolean` | `1` | Active label scale factor |
1130
- | `fontSize` | `number` | — | Label font size |
1131
- | `height` | `number` | — | Bar height override |
1132
- | `showBorder` | `boolean` | `false` | Persistent bottom border |
1133
- | `colors` | `Partial<TabBarColors>` | — | Token overrides |
1134
-
1135
- **TabItem:** `value`, `label`, `badge?` (`number` = count, `''` = dot), `iconRender?`, `disabled?`
1136
-
1137
- ---
1138
-
1139
- ### StyledDivider
1140
-
1141
- A simple horizontal rule.
1142
-
1143
- ```tsx
1144
- import { StyledDivider } from 'fluent-styles'
1145
-
1146
- <StyledDivider />
1147
- <StyledDivider borderBottomColor="#e4e4e7" marginVertical={8} />
1148
- ```
1149
-
1150
- ---
1151
-
1152
- ### StyledSeperator
1153
-
1154
- A horizontal row with left and optional right label — ideal for section headers.
1155
-
1156
- ```tsx
1157
- import { StyledSeperator } from 'fluent-styles'
1158
-
1159
- <StyledSeperator leftLabel="Recent" rightLabel="See all" marginVertical={8} />
1160
- ```
1161
-
1162
- Props: `leftLabel`, `leftLabelProps`, `rightLabel`, `rightLabelProps`, plus all `StackProps`.
1163
-
1164
- ---
1165
-
1166
- ### Stack
1167
-
1168
- A layout primitive for row and column arrangements.
1169
-
1170
- ```tsx
1171
- import { Stack } from 'fluent-styles'
1172
-
1173
- // Vertical (column)
1174
- <Stack gap={12}>
1175
- <StyledText>Item 1</StyledText>
1176
- <StyledText>Item 2</StyledText>
1177
- </Stack>
1178
-
1179
- // Horizontal (row)
1180
- <Stack horizontal alignItems="center" gap={8}>
1181
- <Avatar />
1182
- <StyledText>Jane Doe</StyledText>
1183
- </Stack>
1184
- ```
1185
-
1186
- ---
1187
-
1188
- ### StyledText
1189
-
1190
- A variant-aware Text component accepting `TextStyle` props directly.
1191
-
1192
- ```tsx
1193
- import { StyledText } from 'fluent-styles'
1194
-
1195
- <StyledText fontSize={18} fontWeight="700" color="#1c1c1e">Heading</StyledText>
1196
- <StyledText link>Click here</StyledText>
1197
- <StyledText textAlign="center" color="#6b7280">Muted caption</StyledText>
1198
- ```
1199
-
1200
- ---
1201
-
1202
- ### StyledPressable
1203
-
1204
- A styled `Pressable` accepting flat `ViewStyle` props.
1205
-
1206
- ```tsx
1207
- import { StyledPressable } from 'fluent-styles'
1208
-
1209
- <StyledPressable padding={12} borderRadius={8} backgroundColor="#f4f4f5" onPress={handlePress}>
1210
- <StyledText>Press me</StyledText>
1211
- </StyledPressable>
1212
- ```
1213
-
1214
- ---
1215
-
1216
- ### StyledPage / StyledScrollView
1217
-
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.
1219
-
1220
- ```tsx
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
- }
1245
-
1246
- // ── Custom background / padding ──────────────────────────────────────────────
1247
- <StyledPage flex={1} backgroundColor={theme.colors.gray[50]} paddingHorizontal={16}>
1248
- {/* content */}
1249
- </StyledPage>
1250
- ```
1251
-
1252
- Accepts all `SafeAreaViewProps` and flat `ViewStyle` props.
1253
-
1254
- ---
1255
-
1256
- ### StyledSafeAreaView
1257
-
1258
- A styled wrapper around `SafeAreaView`.
1259
-
1260
- ```tsx
1261
- import { StyledSafeAreaView } from 'fluent-styles'
1262
-
1263
- <StyledSafeAreaView flex={1} backgroundColor="#fff">
1264
- <App />
1265
- </StyledSafeAreaView>
1266
- ```
1267
-
1268
- ---
1269
-
1270
- ### Spacer / StyledSpacer
1271
-
1272
- Inserts fixed or flexible whitespace between elements.
1273
-
1274
- ```tsx
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} />
1282
-
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>
1291
- ```
1292
-
1293
- **Props:** `height`, `width`, `flex`, `margin`, `marginTop`, `marginBottom`, `marginHorizontal`, `marginVertical`, `backgroundColor`
1294
-
1295
- ---
1296
-
1297
- ### StyledShape
1298
-
1299
- A shaped container for icon chips, avatars, and dot indicators.
1300
-
1301
- ```tsx
1302
- import { StyledShape } from 'fluent-styles'
1303
-
1304
- <StyledShape size={40} borderRadius={20} backgroundColor="#6366f1">
1305
- <StyledText color="#fff">A</StyledText>
1306
- </StyledShape>
1307
- ```
1308
-
1309
- ---
1310
-
1311
- ### Loader
1312
-
1313
- A loading indicator with four animation variants, optional overlay, and label.
1314
-
1315
- ```tsx
1316
- import { Loader } from 'fluent-styles'
1317
-
1318
- <Loader variant="spinner" />
1319
- <Loader variant="dots" color="#6366f1" label="Saving…" />
1320
- <Loader variant="pulse" overlay />
1321
- <Loader variant="circular" label="Loading…" theme="dark" />
1322
- ```
1323
-
1324
- | Prop | Type | Default | Description |
1325
- |---|---|---|---|
1326
- | `variant` | `spinner \| pulse \| dots \| circular` | `spinner` | Animation style |
1327
- | `label` | `string` | — | Text below the indicator |
1328
- | `color` | `string` | — | Indicator tint colour |
1329
- | `overlay` | `boolean` | `false` | Full-screen dimmed backdrop |
1330
- | `theme` | `light \| dark \| system` | `system` | Colour scheme |
1331
- | `colors` | `Partial<LoaderColors>` | — | Fine-grained token overrides |
1332
-
1333
- ---
1334
-
1335
- ### StyledCircularProgress
1336
-
1337
- An animated SVG ring progress indicator with four visual variants, five preset sizes, centre label modes, gradient support, and full colour customisation.
1338
-
1339
- ```tsx
1340
- import { StyledCircularProgress } from 'fluent-styles'
1341
-
1342
- // Basic — shows percentage
1343
- <StyledCircularProgress value={72} />
1344
-
1345
- // Fraction display
1346
- <StyledCircularProgress value={18} total={25} display="fraction" />
1347
-
1348
- // With label and sublabel
1349
- <StyledCircularProgress
1350
- value={72}
1351
- display="percent"
1352
- label="Tasks"
1353
- sublabel="this week"
1354
- size="lg"
1355
- />
1356
-
1357
- // Gradient variant
1358
- <StyledCircularProgress
1359
- value={68}
1360
- variant="gradient"
1361
- colors={{
1362
- gradientFrom: '#6366f1',
1363
- gradientTo: '#22d3ee',
1364
- }}
1365
- />
1366
-
1367
- // Dashboard (half-circle gauge)
1368
- <StyledCircularProgress value={55} variant="dashboard" size="xl" />
1369
-
1370
- // Custom diameter and stroke
1371
- <StyledCircularProgress value={55} diameter={120} strokeWidth={24} display="percent" />
1372
-
1373
- // Colour overrides
1374
- <StyledCircularProgress
1375
- value={82}
1376
- display="percent"
1377
- label="Health"
1378
- size="md"
1379
- colors={{
1380
- arc: theme.colors.green[500],
1381
- track: theme.colors.green[100],
1382
- label: theme.colors.green[700],
1383
- sublabel: theme.colors.green[400],
1384
- }}
1385
- />
1386
-
1387
- // Custom centre content (overrides display/label)
1388
- <StyledCircularProgress value={55} display="none" size="lg">
1389
- <Stack alignItems="center" gap={2}>
1390
- <StyledText fontSize={14}>❤️</StyledText>
1391
- <StyledText fontSize={12} fontWeight={theme.fontWeight.bold} color="#f43f5e">
1392
- 55 bpm
1393
- </StyledText>
1394
- </Stack>
1395
- </StyledCircularProgress>
1396
-
1397
- // Controlled value with animation
1398
- <StyledCircularProgress
1399
- value={controlled}
1400
- display="percent"
1401
- label="Progress"
1402
- sublabel={`${controlled} / 100`}
1403
- size="xl"
1404
- variant="gradient"
1405
- duration={600}
1406
- />
1407
-
1408
- // No animation
1409
- <StyledCircularProgress value={90} animated={false} display="percent" />
1410
-
1411
- // On a dark surface
1412
- <StyledCircularProgress
1413
- value={72}
1414
- variant="gradient"
1415
- display="percent"
1416
- label="Progress"
1417
- colors={{
1418
- gradientFrom: '#818cf8',
1419
- gradientTo: '#22d3ee',
1420
- track: 'rgba(255,255,255,0.12)',
1421
- label: '#f4f4f5',
1422
- sublabel: 'rgba(255,255,255,0.5)',
1423
- }}
1424
- />
1425
- ```
1426
-
1427
- #### Props
1428
-
1429
- | Prop | Type | Default | Description |
1430
- |---|---|---|---|
1431
- | `value` | `number` | **required** | Current progress value |
1432
- | `total` | `number` | `100` | Maximum value |
1433
- | `display` | `'percent' \| 'fraction' \| 'value' \| 'label' \| 'none'` | `'percent'` | What to render in the centre |
1434
- | `label` | `string` | — | Primary label inside the ring |
1435
- | `sublabel` | `string` | — | Secondary line below the primary label |
1436
- | `variant` | `'default' \| 'ghost' \| 'gradient' \| 'dashboard'` | `'default'` | Visual style |
1437
- | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Preset diameter |
1438
- | `diameter` | `number` | — | Pixel diameter — overrides `size` |
1439
- | `strokeWidth` | `number` | — | Arc thickness in px — auto-scaled when omitted |
1440
- | `lineCap` | `'round' \| 'butt' \| 'square'` | `'round'` | Arc end cap style |
1441
- | `animated` | `boolean` | `true` | Animate from 0 to `value` on mount / value change |
1442
- | `duration` | `number` | `900` | Animation duration in ms |
1443
- | `colors` | `Partial<CircularProgressColors>` | — | Fine-grained colour overrides (see below) |
1444
- | `children` | `ReactNode` | — | Custom centre content — overrides `display`, `label`, `sublabel` |
1445
-
1446
- #### Size presets
1447
-
1448
- | Size | Diameter | Stroke |
1449
- |---|---|---|
1450
- | `xs` | 48 px | 4 px |
1451
- | `sm` | 64 px | 5 px |
1452
- | `md` | 80 px | 6 px |
1453
- | `lg` | 100 px | 7 px |
1454
- | `xl` | 128 px | 8 px |
1455
-
1456
- #### Variants
1457
-
1458
- | Variant | Description |
1459
- |---|---|
1460
- | `default` | Coloured arc on a light grey track |
1461
- | `ghost` | Arc only — no background track |
1462
- | `gradient` | Two-stop linear gradient arc (uses `gradientFrom` / `gradientTo`) |
1463
- | `dashboard` | Half-circle gauge — flat side at the bottom |
1464
-
1465
- #### `CircularProgressColors`
1466
-
1467
- | Token | Default | Controls |
1468
- |---|---|---|
1469
- | `arc` | `indigo[500]` | Progress arc fill |
1470
- | `track` | `gray[200]` | Background track ring |
1471
- | `label` | `gray[800]` | Primary centre text |
1472
- | `sublabel` | `gray[400]` | Secondary centre text |
1473
- | `gradientFrom` | `violet[500]` | Gradient start — `gradient` variant only |
1474
- | `gradientTo` | `cyan[400]` | Gradient end — `gradient` variant only |
1475
-
1476
- #### Real-world example — onboarding card
1477
-
1478
- ```tsx
1479
- <Stack
1480
- backgroundColor={theme.colors.indigo[600]}
1481
- borderRadius={16}
1482
- horizontal
1483
- gap={20}
1484
- paddingVertical={32}
1485
- paddingHorizontal={16}
1486
- alignItems="center"
1487
- >
1488
- <StyledCircularProgress
1489
- value={3}
1490
- total={5}
1491
- display="fraction"
1492
- label="done"
1493
- size="lg"
1494
- colors={{
1495
- arc: theme.colors.white,
1496
- track: 'rgba(255,255,255,0.2)',
1497
- label: theme.colors.white,
1498
- sublabel: 'rgba(255,255,255,0.65)',
1499
- }}
1500
- />
1501
- <Stack vertical flex={1} gap={4}>
1502
- <StyledText fontSize={16} fontWeight={theme.fontWeight.bold} color={theme.colors.white}>
1503
- Getting started
1504
- </StyledText>
1505
- <StyledText fontSize={13} color="rgba(255,255,255,0.75)">
1506
- Complete 2 more steps to unlock all features.
1507
- </StyledText>
1508
- </Stack>
1509
- </Stack>
1510
- ```
1511
-
1512
- ---
1513
-
1514
- ### StyledChip
1515
-
1516
- A multi-variant chip/tag component with controlled and uncontrolled selection, animated checkmarks, and six visual variants.
1517
-
1518
- ```tsx
1519
- import { StyledChip } from 'fluent-styles'
1520
- ```
1521
-
1522
- #### Variants
1523
-
1524
- | Variant | Description |
1525
- |---|---|
1526
- | `outlined` | Border + tinted bg when selected (default) |
1527
- | `filled` | Solid background, changes tone on select |
1528
- | `smooth` | Soft grey background, no border |
1529
- | `ingredient` | Dark-bg when selected (recipe/filter chips) |
1530
- | `likeable` | Pink heart chip — toggles like state |
1531
- | `icon` | Leading icon with accent styling |
1532
-
1533
- #### Size presets
1534
-
1535
- | Size | Padding H | Padding V | Font | Icon | Radius |
1536
- |---|---|---|---|---|---|
1537
- | `sm` | 10 | 5 | 11 | 12 | 20 |
1538
- | `md` | 14 | 8 | 13 | 14 | 24 |
1539
- | `lg` | 18 | 11 | 15 | 16 | 28 |
1540
-
1541
- #### Props
1542
-
1543
- | Prop | Type | Default | Description |
1544
- |---|---|---|---|
1545
- | `label` | `string` | — | Chip text |
1546
- | `variant` | `ChipVariant` | `'outlined'` | Visual style |
1547
- | `size` | `ChipSize` | `'md'` | Size preset |
1548
- | `selected` | `boolean` | — | Controlled selection state |
1549
- | `defaultSelected` | `boolean` | `false` | Uncontrolled initial state |
1550
- | `onPress` | `(selected: boolean) => void` | — | Fires with new selection value |
1551
- | `color` | `string` | theme default | Accent colour (border, text, icon) |
1552
- | `bgColor` | `string` | variant default | Fill background |
1553
- | `icon` | `React.ReactNode` | — | Leading icon node (used with `'icon'` variant) |
1554
- | `showCheck` | `boolean` | `true` | Show checkmark when selected |
1555
- | `disabled` | `boolean` | `false` | Reduces opacity, disables press |
1556
- | `borderRadius` | `number` | size preset | Override border radius |
1557
-
1558
- #### Usage
1559
-
1560
- ```tsx
1561
- import React, { useState } from 'react'
1562
- import { StyledChip, Stack } from 'fluent-styles'
1563
- import Icon from 'react-native-vector-icons/Feather'
1564
-
1565
- // ── Multi-select toggle helper ────────────────────────────────────────────────
1566
- // A common pattern: maintain a string[] of selected labels and toggle them.
1567
- const [selected, setSelected] = useState<string[]>(['Hacktoberfest'])
1568
-
1569
- const toggle = (label: string) =>
1570
- setSelected((prev) =>
1571
- prev.includes(label) ? prev.filter((l) => l !== label) : [...prev, label]
1572
- )
1573
-
1574
- // ── 1. Outlined — multi-select filter bar ─────────────────────────────────────
1575
- <Stack gap={10}>
1576
- <Stack horizontal gap={8} flexWrap="wrap">
1577
- {[
1578
- { label: 'Enhancement', color: '#9e9e9e' },
1579
- { label: 'Trends', color: '#ff9800' },
1580
- ].map(({ label, color }) => (
1581
- <StyledChip
1582
- key={label}
1583
- label={label}
1584
- variant="outlined"
1585
- color={color}
1586
- selected={selected.includes(label)}
1587
- onPress={() => toggle(label)}
1588
- />
1589
- ))}
1590
- </Stack>
1591
- <Stack horizontal gap={8} flexWrap="wrap">
1592
- {[
1593
- { label: 'Rubi Kapustu', color: '#2196f3' },
1594
- { label: 'Hacktoberfest', color: '#4caf50' },
1595
- ].map(({ label, color }) => (
1596
- <StyledChip
1597
- key={label}
1598
- label={label}
1599
- variant="outlined"
1600
- color={color}
1601
- selected={selected.includes(label)}
1602
- onPress={() => toggle(label)}
1603
- />
1604
- ))}
1605
- </Stack>
1606
- <Stack horizontal gap={8} flexWrap="wrap">
1607
- {[
1608
- { label: 'Limited', color: '#e65100' },
1609
- { label: 'Taken', color: '#e91e63' },
1610
- ].map(({ label, color }) => (
1611
- <StyledChip key={label} label={label} variant="outlined" color={color}
1612
- selected={selected.includes(label)} onPress={() => toggle(label)} />
1613
- ))}
1614
- </Stack>
1615
- </Stack>
1616
-
1617
- // ── 2. Ingredient — recipe / dietary filter ───────────────────────────────────
1618
- const [ingredients, setIngredients] = useState(['Cinnamon', 'Nut'])
1619
-
1620
- {[['Cheese', 'Vanilla', 'Chocolate', 'Egg'],
1621
- ['Honey', 'Milk', 'Banana', 'Nut'],
1622
- ['Cinnamon', 'Tomato', 'Yogurt'],
1623
- ].map((row, ri) => (
1624
- <Stack key={ri} horizontal gap={8} flexWrap="wrap">
1625
- {row.map((label) => (
1626
- <StyledChip
1627
- key={label}
1628
- label={label}
1629
- variant="ingredient"
1630
- selected={ingredients.includes(label)}
1631
- onPress={() => toggle(label)}
1632
- />
1633
- ))}
1634
- </Stack>
1635
- ))}
1636
-
1637
- // ── 3. Filled — label/status chips ────────────────────────────────────────────
1638
- <Stack horizontal gap={8} flexWrap="wrap">
1639
- <StyledChip label="Hacktoberfest" variant="filled" bgColor="#e8f5e9" color="#388e3c" />
1640
- <StyledChip label="Question" variant="filled" bgColor="#fff3e0" color="#f57c00" />
1641
- <StyledChip label="Enhancement" variant="filled" bgColor="#f3e5f5" color="#7b1fa2" />
1642
- {/* Selected filled chip with checkmark */}
1643
- <StyledChip
1644
- label="Taken"
1645
- variant="filled"
1646
- bgColor="#e91e8c"
1647
- color="#fff"
1648
- defaultSelected
1649
- showCheck
1650
- />
1651
- </Stack>
1652
-
1653
- // ── 4. Icon chips — dynamic icon colour based on selected state ───────────────
1654
- const [activeIcons, setActiveIcons] = useState<string[]>(['Social Media'])
1655
-
1656
- <Stack horizontal gap={8} flexWrap="wrap">
1657
- {/* Solid active: bgColor becomes fill when selected */}
1658
- <StyledChip
1659
- label="Social Media"
1660
- variant="icon"
1661
- icon={
1662
- <Icon
1663
- name="refresh-cw"
1664
- size={14}
1665
- color={activeIcons.includes('Social Media') ? '#fff' : '#2e7d32'}
1666
- />
1667
- }
1668
- color="#2e7d32"
1669
- bgColor="#2e7d32"
1670
- selected={activeIcons.includes('Social Media')}
1671
- onPress={() => toggle('Social Media')}
1672
- />
1673
- <StyledChip
1674
- label="Pin"
1675
- variant="icon"
1676
- icon={<Icon name="map-pin" size={14} color="#2e7d32" />}
1677
- color="#2e7d32"
1678
- bgColor="#e8f5e9"
1679
- selected={activeIcons.includes('Pin')}
1680
- onPress={() => toggle('Pin')}
1681
- />
1682
- <StyledChip
1683
- label="Activity"
1684
- variant="icon"
1685
- icon={<Icon name="activity" size={14} color="#2e7d32" />}
1686
- color="#2e7d32"
1687
- bgColor="#e8f5e9"
1688
- selected={activeIcons.includes('Activity')}
1689
- onPress={() => toggle('Activity')}
1690
- />
1691
- </Stack>
1692
-
1693
- // Icon chips — mixed accent colour palette
1694
- <Stack horizontal gap={8} flexWrap="wrap">
1695
- <StyledChip label="Annotation" variant="icon"
1696
- icon={<Icon name="edit-3" size={14} color="#5c6bc0" />} color="#5c6bc0" bgColor="#e8eaf6" />
1697
- <StyledChip label="Laboratory" variant="icon"
1698
- icon={<Icon name="zap" size={14} color="#5c6bc0" />} color="#5c6bc0" bgColor="#e8eaf6" />
1699
- <StyledChip label="History" variant="icon"
1700
- icon={<Icon name="clock" size={14} color="#5c6bc0" />} color="#5c6bc0" bgColor="#e8eaf6" />
1701
- {/* Solid active — pre-selected */}
1702
- <StyledChip
1703
- label="Globe"
1704
- variant="icon"
1705
- icon={<Icon name="globe" size={14} color="#fff" />}
1706
- color="#fff"
1707
- bgColor="#3f51b5"
1708
- selected
1709
- onPress={() => {}}
1710
- />
1711
- </Stack>
1712
-
1713
- // ── 5. Likeable — topic interest chips ───────────────────────────────────────
1714
- const [liked, setLiked] = useState(['Big Data', 'New Technology'])
1715
-
1716
- {[['Cryptocurrency', 'Big Data'],
1717
- ['Software Development'],
1718
- ['New Technology', 'Gadgets'],
1719
- ['Technology Startups'],
1720
- ].map((row, ri) => (
1721
- <Stack key={ri} horizontal gap={8} flexWrap="wrap">
1722
- {row.map((label) => (
1723
- <StyledChip
1724
- key={label}
1725
- label={label}
1726
- variant="likeable"
1727
- selected={liked.includes(label)}
1728
- onPress={() => toggle(label)}
1729
- />
1730
- ))}
1731
- </Stack>
1732
- ))}
1733
-
1734
- // ── 6. Size variants side-by-side ────────────────────────────────────────────
1735
- <Stack gap={10}>
1736
- <Stack horizontal gap={8} alignItems="center">
1737
- <StyledChip label="Small" variant="outlined" size="sm" color="#2196f3" />
1738
- <StyledChip label="Medium" variant="outlined" size="md" color="#2196f3" />
1739
- <StyledChip label="Large" variant="outlined" size="lg" color="#2196f3" />
1740
- </Stack>
1741
- <Stack horizontal gap={8} alignItems="center">
1742
- <StyledChip label="Small" variant="ingredient" size="sm" defaultSelected />
1743
- <StyledChip label="Medium" variant="ingredient" size="md" defaultSelected />
1744
- <StyledChip label="Large" variant="ingredient" size="lg" defaultSelected />
1745
- </Stack>
1746
- </Stack>
1747
-
1748
- // ── 7. Disabled state ────────────────────────────────────────────────────────
1749
- <Stack horizontal gap={8} flexWrap="wrap">
1750
- <StyledChip label="Outlined" variant="outlined" color="#2196f3" disabled />
1751
- <StyledChip label="Filled" variant="filled" bgColor="#e91e63" color="#fff" disabled />
1752
- <StyledChip label="Ingredient" variant="ingredient" disabled />
1753
- <StyledChip label="Likeable" variant="likeable" disabled />
1754
- </Stack>
1755
- ```
1756
-
1757
- ---
1758
-
1759
- ### StyledBar
1760
-
1761
- An animated SVG bar chart with gradient active bars, optional hatch texture on inactive bars, and a floating tooltip. Backed by `react-native-svg`.
1762
-
1763
- ```tsx
1764
- import { StyledBar } from 'fluent-styles'
1765
- ```
1766
-
1767
- #### `StyledBarDatum`
1768
-
1769
- | Field | Type | Description |
1770
- |---|---|---|
1771
- | `label` | `string` | X-axis label |
1772
- | `value` | `number \| null` | Bar height value; `null` renders a grey placeholder bar |
1773
- | `active` | `boolean` | Marks the active/highlighted bar (renders gradient + tooltip) |
1774
-
1775
- #### `StyledBarColors`
1776
-
1777
- | Field | Default | Description |
1778
- |---|---|---|
1779
- | `inactiveBar` | `gray[200]` | Inactive bar fill |
1780
- | `hatchLine` | `rgba(0,0,0,0.07)` | Hatch stripe colour on inactive bars |
1781
- | `activeTop` | `#d4f53c` | Active bar gradient top |
1782
- | `activeBottom` | `#a8c820` | Active bar gradient bottom |
1783
- | `tooltipBg` | `gray[900]` | Tooltip bubble background |
1784
- | `tooltipText` | `white` | Tooltip text colour |
1785
- | `activeLabelColor` | `gray[900]` | Label colour for active bar |
1786
- | `inactiveLabelColor` | `gray[400]` | Label colour for inactive bars |
1787
-
1788
- #### Props
1789
-
1790
- | Prop | Type | Default | Description |
1791
- |---|---|---|---|
1792
- | `data` | `StyledBarDatum[]` | — | Array of bar data |
1793
- | `maxValue` | `number` | max of values | Explicit Y-axis ceiling |
1794
- | `width` | `number` | screen width − padding | SVG canvas width in px |
1795
- | `containerPaddingHorizontal` | `number` | `80` | Horizontal padding to subtract from screen width |
1796
- | `height` | `number` | `180` | Plot area height in px |
1797
- | `barWidthRatio` | `number` | `0.62` | Bar width as fraction of slot width |
1798
- | `labelHeight` | `number` | `28` | Height reserved below bars for labels |
1799
- | `showHatch` | `boolean` | `true` | Render hatch texture on inactive bars |
1800
- | `hatchSpacing` | `number` | `8` | Pixel gap between hatch lines |
1801
- | `tooltipLabel` | `string` | auto from active value | Override tooltip text |
1802
- | `unit` | `string` | `''` | Unit suffix appended to auto tooltip (e.g. `'min'`) |
1803
- | `colors` | `StyledBarColors` | lime theme | Colour overrides |
1804
- | `animated` | `boolean` | `true` | Animate bars growing from zero on mount |
1805
- | `animationDuration` | `number` | `600` | Animation duration in ms |
1806
-
1807
- #### Usage
1808
-
1809
- ```tsx
1810
- import { StyledBar, StyledCard, palettes, theme } from 'fluent-styles'
1811
-
1812
- // ── Padding rule ──────────────────────────────────────────────────────────────
1813
- // The chart auto-sizes to: screenWidth − containerPaddingHorizontal
1814
- // When placed inside a card:
1815
- // screen paddingHorizontal = 20 → both sides = 40
1816
- // card padding = 20 → both sides = 40
1817
- // total containerPaddingHorizontal = 80 ← pass this value
1818
- const CONTAINER_PAD = 80
1819
-
1820
- // ── 1. Default lime — workout duration ───────────────────────────────────────
1821
- <StyledCard padding={20} shadow="light">
1822
- <StyledBar
1823
- data={[
1824
- { label: 'Sat', value: 45 },
1825
- { label: 'Sun', value: 60 },
1826
- { label: 'Mon', value: 35 },
1827
- { label: 'Tue', value: 70, active: true },
1828
- { label: 'Wed', value: 50 },
1829
- { label: 'Thu', value: 30 },
1830
- { label: 'Fri', value: 20 },
1831
- ]}
1832
- unit="min"
1833
- maxValue={100}
1834
- containerPaddingHorizontal={CONTAINER_PAD}
1835
- />
1836
- </StyledCard>
1837
-
1838
- // ── 2. Green theme — weight tracking with null placeholders ──────────────────
1839
- // null value renders a shorter grey placeholder bar (e.g. a rest/missing day)
1840
- <StyledBar
1841
- data={[
1842
- { label: '13', value: null }, // missing — shows placeholder
1843
- { label: '14', value: 60.0, active: true },
1844
- { label: '15', value: 58.2 },
1845
- { label: '16', value: 59.1 },
1846
- { label: '17', value: 57.4 },
1847
- { label: '18', value: 58.0 },
1848
- { label: '19', value: 56.8 },
1849
- ]}
1850
- unit="kg"
1851
- maxValue={80}
1852
- containerPaddingHorizontal={CONTAINER_PAD}
1853
- colors={{
1854
- activeTop: '#4ade80',
1855
- activeBottom: '#16a34a',
1856
- tooltipBg: '#15803d',
1857
- tooltipText: '#fff',
1858
- }}
1859
- />
1860
-
1861
- // ── 3. Orange — temperature, no hatch texture ─────────────────────────────────
1862
- <StyledBar
1863
- data={[
1864
- { label: '13', value: null },
1865
- { label: '14', value: 36.9, active: true },
1866
- { label: '15', value: 37.1 },
1867
- { label: '16', value: 36.8 },
1868
- { label: '17', value: 37.0 },
1869
- { label: '18', value: 37.2 },
1870
- { label: '19', value: 36.5 },
1871
- ]}
1872
- unit="°C"
1873
- maxValue={38}
1874
- showHatch={false}
1875
- containerPaddingHorizontal={CONTAINER_PAD}
1876
- colors={{
1877
- inactiveBar: '#fed7aa',
1878
- activeTop: '#fb923c',
1879
- activeBottom: '#ea580c',
1880
- tooltipBg: '#c2410c',
1881
- tooltipText: '#fff',
1882
- }}
1883
- />
1884
-
1885
- // ── 4. Blue — water intake, large values ──────────────────────────────────────
1886
- <StyledBar
1887
- data={[
1888
- { label: '13', value: null },
1889
- { label: '14', value: 1750, active: true },
1890
- { label: '15', value: 2100 },
1891
- { label: '16', value: 1600 },
1892
- { label: '17', value: 1900 },
1893
- { label: '18', value: 800 },
1894
- { label: '19', value: null },
1895
- ]}
1896
- unit="mL"
1897
- maxValue={2500}
1898
- containerPaddingHorizontal={CONTAINER_PAD}
1899
- colors={{
1900
- inactiveBar: '#bfdbfe',
1901
- hatchLine: 'rgba(59,130,246,0.15)',
1902
- activeTop: '#60a5fa',
1903
- activeBottom: '#2563eb',
1904
- tooltipBg: '#1e3a8a',
1905
- tooltipText: '#fff',
1906
- }}
1907
- />
1908
-
1909
- // ── 5. Rose — calories burned, overridden tooltip label ───────────────────────
1910
- // tooltipLabel lets you display a formatted string instead of the raw value
1911
- <StyledBar
1912
- data={caloriesData}
1913
- unit="kcal"
1914
- maxValue={2500}
1915
- tooltipLabel="2,200 kcal"
1916
- containerPaddingHorizontal={CONTAINER_PAD}
1917
- colors={{
1918
- inactiveBar: '#fce7f3',
1919
- hatchLine: 'rgba(236,72,153,0.12)',
1920
- activeTop: '#f472b6',
1921
- activeBottom: '#db2777',
1922
- tooltipBg: '#831843',
1923
- tooltipText: '#fff',
1924
- activeLabelColor: '#be185d', // active x-label gets accent colour too
1925
- }}
1926
- />
1927
-
1928
- // ── 6. Minimal — no animation, narrow bars, indigo ───────────────────────────
1929
- // Use animated=false for static/print-style charts.
1930
- // barWidthRatio controls how wide bars are relative to their slot.
1931
- <StyledBar
1932
- data={stepsData}
1933
- unit="k"
1934
- maxValue={100}
1935
- animated={false}
1936
- showHatch={false}
1937
- barWidthRatio={0.42}
1938
- containerPaddingHorizontal={CONTAINER_PAD}
1939
- colors={{
1940
- inactiveBar: '#e0e7ff',
1941
- activeTop: '#818cf8',
1942
- activeBottom: '#4338ca',
1943
- tooltipBg: '#312e81',
1944
- tooltipText: '#fff',
1945
- }}
1946
- />
1947
- ```
1948
-
1949
- ---
1950
-
1951
- ### StyledTimeline
1952
-
1953
- A data-driven vertical timeline with animated dots, three density variants, three dot shapes, custom renderers, and full colour overrides.
1954
-
1955
- ```tsx
1956
- import { StyledTimeline } from 'fluent-styles'
1957
- ```
1958
-
1959
- #### `TimelineItem`
1960
-
1961
- | Field | Type | Description |
1962
- |---|---|---|
1963
- | `id` | `string` | Unique identifier |
1964
- | `time` | `string` | Primary time label (e.g. `'09:00'`) |
1965
- | `endTime` | `string` | Optional end time shown smaller below |
1966
- | `title` | `string` | Bold title in default content renderer |
1967
- | `subtitle` | `string` | Secondary line (muted) |
1968
- | `description` | `string` | Tertiary detail line |
1969
- | `content` | `React.ReactNode` | Custom content — replaces default renderer for this item |
1970
- | `meta` | `Record<string, unknown>` | Arbitrary metadata for use in `renderItem` |
1971
-
1972
- #### Variants
1973
-
1974
- | Variant | Gap between rows |
1975
- |---|---|
1976
- | `compact` | 12 px |
1977
- | `default` | 20 px |
1978
- | `spacious` | 32 px |
1979
-
1980
- #### Dot shapes
1981
-
1982
- | Shape | Appearance |
1983
- |---|---|
1984
- | `circle` | Hollow ring (transparent fill, coloured border) |
1985
- | `ring` | White fill with coloured border |
1986
- | `filled` | Solid fill (default) |
1987
-
1988
- #### `StyledTimelineColors`
1989
-
1990
- | Field | Default | Description |
1991
- |---|---|---|
1992
- | `line` | `gray[200]` | Vertical connector line colour |
1993
- | `dot` | `#8bc34a` | Dot fill / border colour |
1994
- | `dotBorder` | `white` | Inner ring background (for `ring` shape) |
1995
- | `timeText` | `gray[900]` | Primary time label colour |
1996
- | `endTimeText` | `gray[400]` | End-time label colour |
1997
-
1998
- #### Props
1999
-
2000
- | Prop | Type | Default | Description |
2001
- |---|---|---|---|
2002
- | `items` | `TimelineItem[]` | `[]` | Data-driven item list |
2003
- | `renderItem` | `(item, index) => ReactNode` | — | Custom row content renderer |
2004
- | `children` | `ReactNode` | — | Extra rows appended after `items` |
2005
- | `variant` | `TimelineVariant` | `'default'` | Row density |
2006
- | `dotShape` | `TimelineDotShape` | `'filled'` | Dot style |
2007
- | `dotSize` | `number` | `10` | Dot diameter in px |
2008
- | `timeColumnWidth` | `number` | `56` | Width of left time column in px |
2009
- | `timeGap` | `number` | `16` | Horizontal gap between dot column and content |
2010
- | `animated` | `boolean` | `true` | Pop-in animation on each dot |
2011
- | `colors` | `StyledTimelineColors` | — | Colour overrides |
2012
- | `onItemPress` | `(item: TimelineItem) => void` | — | Press handler for rows |
2013
-
2014
- #### Usage
2015
-
2016
- ```tsx
2017
- import { StyledTimeline, type TimelineItem } from 'fluent-styles'
2018
-
2019
- // ── 1. Minimal JSON-driven ────────────────────────────────────────────────────
2020
- <StyledTimeline
2021
- items={[
2022
- { id: '1', time: '09:00', title: 'Morning Run', subtitle: 'Cardio · 5km' },
2023
- { id: '2', time: '11:30', title: 'Strength Class', subtitle: 'Upper body' },
2024
- { id: '3', time: '14:00', title: 'Yoga', subtitle: 'Recovery' },
2025
- ]}
2026
- />
2027
-
2028
- // ── 2. With end time ─────────────────────────────────────────────────────────
2029
- // endTime is shown smaller below the main time label
2030
- <StyledTimeline
2031
- items={[
2032
- { id: '1', time: '11:35', endTime: '13:05', title: 'Cardio', subtitle: '4 of 6 sessions' },
2033
- { id: '2', time: '14:45', endTime: '15:45', title: 'Muscle', subtitle: '5 of 8 sessions' },
2034
- { id: '3', time: '17:00', endTime: '18:00', title: 'Weight Training', subtitle: '4 of 9 sessions' },
2035
- ]}
2036
- colors={{ dot: '#8bc34a', timeText: '#1a1a1e', endTimeText: '#9ca3af' }}
2037
- />
2038
-
2039
- // ── 3. Custom renderItem — fitness planner card ───────────────────────────────
2040
- // Store arbitrary per-item data in the meta field; cast it inside renderItem.
2041
- interface WorkoutMeta {
2042
- [key: string]: unknown
2043
- iconName: string
2044
- calories: string
2045
- bpm: string
2046
- duration: string
2047
- }
2048
-
2049
- const workoutItems: TimelineItem[] = [
2050
- {
2051
- id: '1', time: '11:35', endTime: '13:05', title: 'Cardio',
2052
- meta: { iconName: 'heart', calories: '1200', bpm: '90', duration: '03:00' },
2053
- },
2054
- {
2055
- id: '2', time: '14:45', endTime: '15:45', title: 'Muscle',
2056
- meta: { iconName: 'zap', calories: '980', bpm: '102', duration: '01:00' },
2057
- },
2058
- ]
2059
-
2060
- const WorkoutCard: React.FC<{ item: TimelineItem }> = ({ item }) => {
2061
- const m = item.meta as unknown as WorkoutMeta
2062
- return (
2063
- <StyledCard padding={16} borderRadius={20} shadow="light" borderLeftWidth={4} borderLeftColor="#8bc34a">
2064
- <StyledText fontSize={16} fontWeight="800">{item.title}</StyledText>
2065
- <Stack horizontal gap={20} marginTop={8}>
2066
- <StyledText fontSize={13} color="#6b7280">{m.calories} kcal</StyledText>
2067
- <StyledText fontSize={13} color="#6b7280">{m.bpm} bpm</StyledText>
2068
- <StyledText fontSize={13} color="#6b7280">{m.duration} hr</StyledText>
2069
- </Stack>
2070
- </StyledCard>
2071
- )
2072
- }
2073
-
2074
- <StyledTimeline
2075
- items={workoutItems}
2076
- renderItem={(item) => <WorkoutCard item={item} />}
2077
- variant="default"
2078
- dotShape="filled"
2079
- dotSize={10}
2080
- timeColumnWidth={58}
2081
- timeGap={12}
2082
- animated
2083
- colors={{ dot: '#8bc34a', line: '#e5e7eb', timeText: '#1a1a1e', endTimeText: '#9ca3af' }}
2084
- />
2085
-
2086
- // ── 4. Mixed: data items + appended children ──────────────────────────────────
2087
- <StyledTimeline items={scheduleItems}>
2088
- {/* This node is appended as a final timeline row */}
2089
- <StyledCard padding={12}>
2090
- <StyledText>Don't forget to hydrate! 💚</StyledText>
2091
- </StyledCard>
2092
- </StyledTimeline>
2093
-
2094
- // ── 5. Density variants ───────────────────────────────────────────────────────
2095
- <StyledTimeline items={items} variant="compact" />
2096
- <StyledTimeline items={items} variant="default" />
2097
- <StyledTimeline items={items} variant="spacious" />
2098
-
2099
- // ── 6. Dot shapes ─────────────────────────────────────────────────────────────
2100
- <StyledTimeline items={items} dotShape="filled" /> // solid dot (default)
2101
- <StyledTimeline items={items} dotShape="ring" /> // white fill, coloured border
2102
- <StyledTimeline items={items} dotShape="circle" /> // hollow ring
2103
-
2104
- // ── 7. Blue theme with ring dots ─────────────────────────────────────────────
2105
- <StyledTimeline
2106
- items={items}
2107
- variant="spacious"
2108
- dotShape="ring"
2109
- colors={{ dot: '#2196f3', line: '#bbdefb', dotBorder: '#fff' }}
2110
- />
2111
-
2112
- // ── 8. Press handler — navigate on tap ───────────────────────────────────────
2113
- <StyledTimeline
2114
- items={items}
2115
- onItemPress={(item) => navigation.navigate('SessionDetail', { id: item.id })}
2116
- />
2117
-
2118
- // ── 9. Conditional rendering (rest day) ──────────────────────────────────────
2119
- // items is [] for rest days — render an empty state instead
2120
- {items.length > 0 ? (
2121
- <StyledTimeline items={items} renderItem={(item) => <WorkoutCard item={item} />} />
2122
- ) : (
2123
- <Stack alignItems="center" paddingVertical={48}>
2124
- <StyledText fontSize={18} fontWeight="800">Rest Day</StyledText>
2125
- <StyledText fontSize={14} color="#9ca3af">Recovery is part of the plan.</StyledText>
2126
- </Stack>
2127
- )}
2128
- ```
2129
-
2130
- ---
2131
-
2132
- ### StyledRadio / StyledRadioGroup
2133
-
2134
- Production-ready radio button system with three sub-components and three layout variants. Supports controlled and uncontrolled modes, generic value types, animated dot transitions, and full colour overrides.
2135
-
2136
- ```tsx
2137
- import { StyledRadio, StyledRadioGroup } from 'fluent-styles'
2138
- ```
2139
-
2140
- #### Sub-components
2141
-
2142
- | Component | Description |
2143
- |---|---|
2144
- | `StyledRadio` | Raw animated radio dot — for custom layouts |
2145
- | `StyledRadioGroup` | Full managed group with `list`, `card`, and `boxed` variants |
2146
-
2147
- #### Sizes (`RadioSize`)
2148
-
2149
- | Size | Outer | Inner dot | Border |
2150
- |---|---|---|---|
2151
- | `sm` | 16 | 7 | 1.5 |
2152
- | `md` | 20 | 9 | 2.0 |
2153
- | `lg` | 24 | 11 | 2.5 |
2154
-
2155
- #### Variants (`RadioVariant`)
2156
-
2157
- | Variant | Description |
2158
- |---|---|
2159
- | `list` | Full-width bordered rows — each option is a standalone pressable card |
2160
- | `card` | Horizontal grid (configurable columns) — compact cards for delivery/plan selection |
2161
- | `boxed` | Single card wrapper with a title + divider-separated rows inside |
2162
-
2163
- #### `RadioOption<T>`
2164
-
2165
- | Field | Type | Description |
2166
- |---|---|---|
2167
- | `value` | `T` | Unique option value (string or number) |
2168
- | `label` | `string` | Primary label text |
2169
- | `subtitle` | `string` | Secondary description line |
2170
- | `rightContent` | `ReactNode` | Content displayed on the right (price, tag, etc.) |
2171
- | `leadingContent` | `ReactNode` | Leading content (logo, icon, etc.) |
2172
- | `badge` | `ReactNode` | Inline badge after the label (e.g. `"SAVE 33%"`) |
2173
- | `disabled` | `boolean` | Disables this specific option |
2174
-
2175
- #### `StyledRadioColors`
2176
-
2177
- | Field | Default | Description |
2178
- |---|---|---|
2179
- | `active` | `gray[900]` | Active dot and border colour |
2180
- | `inactive` | `gray[300]` | Inactive ring colour |
2181
- | `selectedCardBg` | `active + 5% opacity` | Selected item background |
2182
- | `selectedCardBorder` | `active` | Selected item border |
2183
- | `unselectedCardBorder` | `gray[200]` | Unselected item border |
2184
- | `label` | `gray[900]` | Label text colour |
2185
- | `subtitle` | `gray[400]` | Subtitle text colour |
2186
-
2187
- #### `StyledRadioGroupProps<T>`
2188
-
2189
- | Prop | Type | Default | Description |
2190
- |---|---|---|---|
2191
- | `options` | `RadioOption<T>[]` | — | Options to render |
2192
- | `value` | `T` | — | Controlled selected value |
2193
- | `defaultValue` | `T` | — | Initial value for uncontrolled mode |
2194
- | `onChange` | `(value: T) => void` | — | Called when selection changes |
2195
- | `variant` | `RadioVariant` | `'list'` | Layout variant |
2196
- | `size` | `RadioSize` | `'md'` | Dot size preset |
2197
- | `title` | `string` | — | Section title (shown in `boxed` variant) |
2198
- | `colors` | `StyledRadioColors` | — | Colour overrides |
2199
- | `columns` | `number` | `3` | Columns for `card` variant |
2200
- | `gap` | `number` | `10` | Gap between cards in `card` variant |
2201
-
2202
- #### Usage
2203
-
2204
- ```tsx
2205
- import React, { useState } from 'react'
2206
- import { StyledRadioGroup, StyledRadio, StyledCard, Stack, StyledText, type RadioOption } from 'fluent-styles'
2207
-
2208
- // ── Shared colour theme ───────────────────────────────────────────────────────
2209
- const BLUE_COLORS = {
2210
- active: '#2563eb',
2211
- selectedCardBg: '#eff6ff',
2212
- selectedCardBorder: '#2563eb',
2213
- unselectedCardBorder: '#e5e7eb',
2214
- }
2215
-
2216
- // ── 1. Subscription plan — list variant with badge + price block ──────────────
2217
- // badge and rightContent let you embed arbitrary nodes beside each option.
2218
- const PriceBlock = ({ main, sub }: { main: string; sub: string }) => (
2219
- <Stack alignItems="flex-end" gap={2}>
2220
- <StyledText fontSize={14} fontWeight="600">{main}</StyledText>
2221
- <StyledText fontSize={12} color="#9ca3af">{sub}</StyledText>
2222
- </Stack>
2223
- )
2224
-
2225
- const SaveBadge = ({ label }: { label: string }) => (
2226
- <Stack paddingHorizontal={8} paddingVertical={3} borderRadius={6} backgroundColor="#dcfce7">
2227
- <StyledText fontSize={10} fontWeight="700" color="#16a34a">{label}</StyledText>
2228
- </Stack>
2229
- )
2230
-
2231
- const [plan, setPlan] = useState('yearly')
2232
-
2233
- <StyledRadioGroup
2234
- options={[
2235
- {
2236
- value: 'yearly',
2237
- label: 'Yearly',
2238
- badge: <SaveBadge label="SAVE 33%" />,
2239
- rightContent: <PriceBlock main="$19.99/month" sub="$240 billed yearly" />,
2240
- },
2241
- {
2242
- value: 'monthly',
2243
- label: 'Monthly',
2244
- rightContent: <PriceBlock main="$24/month" sub="$24 billed monthly" />,
2245
- },
2246
- ]}
2247
- value={plan}
2248
- onChange={setPlan}
2249
- variant="list"
2250
- colors={BLUE_COLORS}
2251
- />
2252
-
2253
- // ── 2. Billing period — boxed variant inside a card ─────────────────────────
2254
- // `title` renders a bold heading inside the card above the options.
2255
- <StyledRadioGroup
2256
- title="Billing Period"
2257
- options={[
2258
- { value: 'monthly', label: 'Monthly', rightContent: <StyledText>$9.99/month</StyledText> },
2259
- { value: 'yearly', label: 'Yearly', rightContent: <StyledText>$12.99/month</StyledText> },
2260
- ]}
2261
- defaultValue="monthly"
2262
- variant="boxed"
2263
- />
2264
-
2265
- // ── 3. Delivery method — card variant (3-column grid, blue accent) ────────────
2266
- // Each option becomes a compact card; columns controls the grid width.
2267
- <StyledRadioGroup
2268
- options={[
2269
- { value: 'standard', label: 'Standard', subtitle: '4–10 business days',
2270
- rightContent: <StyledText fontWeight="600">$5.00</StyledText> },
2271
- { value: 'express', label: 'Express', subtitle: '2–5 business days',
2272
- rightContent: <StyledText fontWeight="600" color="#2563eb">$16.00</StyledText> },
2273
- { value: 'superfast', label: 'Super Fast', subtitle: '1 business day',
2274
- rightContent: <StyledText fontWeight="600">$25.00</StyledText> },
2275
- ]}
2276
- defaultValue="express"
2277
- variant="card"
2278
- columns={3}
2279
- gap={10}
2280
- colors={{ ...BLUE_COLORS, subtitle: '#2563eb' }}
2281
- />
2282
-
2283
- // ── 4. Payment method — list variant with leading card logos ──────────────────
2284
- // leadingContent rows any node before the radio dot (logos, avatars, icons…).
2285
- const VisaLogo = () => (
2286
- <Stack width={40} height={24} borderRadius={4} backgroundColor="#1a1f71"
2287
- alignItems="center" justifyContent="center">
2288
- <StyledText fontSize={11} fontWeight="900" color="#fff">VISA</StyledText>
2289
- </Stack>
2290
- )
2291
-
2292
- const MastercardLogo = () => (
2293
- <Stack width={36} height={24} borderRadius={4} backgroundColor="#f4f4f4"
2294
- alignItems="center" justifyContent="center">
2295
- <Stack horizontal>
2296
- <Stack width={14} height={14} borderRadius={7} backgroundColor="#eb001b" />
2297
- <Stack width={14} height={14} borderRadius={7} backgroundColor="#f79e1b" style={{ marginLeft: -5 }} />
2298
- </Stack>
2299
- </Stack>
2300
- )
2301
-
2302
- <StyledRadioGroup
2303
- options={[
2304
- { value: 'mc8304', leadingContent: <MastercardLogo />, label: '**** 8304',
2305
- subtitle: 'Last used: Mar 26, 2022' },
2306
- { value: 'visa0123', leadingContent: <VisaLogo />, label: '**** 0123',
2307
- subtitle: 'Never used' },
2308
- ]}
2309
- defaultValue="visa0123"
2310
- variant="list"
2311
- colors={BLUE_COLORS}
2312
- />
2313
-
2314
- // ── 5. Size variants — sm / md / lg ─────────────────────────────────────────
2315
- // Size affects only the radio dot; overall row proportions stay the same.
2316
- <Stack gap={14}>
2317
- {(['sm', 'md', 'lg'] as const).map((size) => (
2318
- <Stack key={size}>
2319
- <StyledText fontSize={12} color="#9ca3af" marginBottom={8}>{size}</StyledText>
2320
- <StyledRadioGroup
2321
- options={[
2322
- { value: 'a', label: 'Option A', rightContent: <StyledText>$5.00</StyledText> },
2323
- { value: 'b', label: 'Option B', rightContent: <StyledText>$10.00</StyledText> },
2324
- ]}
2325
- defaultValue="a"
2326
- variant="list"
2327
- size={size}
2328
- colors={BLUE_COLORS}
2329
- />
2330
- </Stack>
2331
- ))}
2332
- </Stack>
2333
-
2334
- // ── 6. Disabled individual options ───────────────────────────────────────────
2335
- // Set disabled: true on any RadioOption to grey it out and block interaction.
2336
- <StyledRadioGroup
2337
- options={[
2338
- { value: 'active', label: 'Active option', rightContent: <StyledText>$9.99</StyledText> },
2339
- { value: 'disabled', label: 'Disabled option', rightContent: <StyledText>$19.99</StyledText>, disabled: true },
2340
- { value: 'other', label: 'Another option', rightContent: <StyledText>$5.99</StyledText> },
2341
- ]}
2342
- defaultValue="active"
2343
- variant="list"
2344
- colors={BLUE_COLORS}
2345
- />
2346
-
2347
- // ── 7. StyledRadio standalone — custom layout swatches ───────────────────────
2348
- // Use StyledRadio directly when you need the dot inside your own layout.
2349
- <StyledCard padding={16} borderRadius={14} shadow="light">
2350
- <Stack gap={16}>
2351
- {[
2352
- { label: 'Selected · dark', selected: true, color: '#111827' },
2353
- { label: 'Unselected', selected: false, color: '#111827' },
2354
- { label: 'Selected · blue', selected: true, color: '#2563eb' },
2355
- { label: 'Selected · green', selected: true, color: '#16a34a' },
2356
- { label: 'Selected · rose', selected: true, color: '#e11d48' },
2357
- ].map(({ label, selected, color }) => (
2358
- <Stack key={label} horizontal alignItems="center" gap={12}>
2359
- <StyledRadio selected={selected} color={color} size="md" />
2360
- <StyledText fontSize={14} color="#374151">{label}</StyledText>
2361
- </Stack>
2362
- ))}
2363
- </Stack>
2364
- </StyledCard>
2365
- ```
2366
-
2367
- ---
2368
-
2369
- ### StyledProgressBar
2370
-
2371
- Animated progress bar with 5 variants, 5 size presets, 3 shapes, 5 label positions, and full colour overrides. Backed by `react-native-svg` for gradient and striped fills.
2372
-
2373
- ```tsx
2374
- import { StyledProgressBar } from 'fluent-styles'
2375
- ```
2376
-
2377
- #### Variants
2378
-
2379
- | Variant | Description |
2380
- |---|---|
2381
- | `default` | Flat filled bar |
2382
- | `striped` | Diagonal animated stripe overlay |
2383
- | `gradient` | Left-to-right colour gradient (SVG) |
2384
- | `segmented` | Divided into N equal tick segments |
2385
- | `buffer` | Primary fill + secondary buffer track (media player style) |
2386
-
2387
- #### Sizes
2388
-
2389
- | Size | Height |
2390
- |---|---|
2391
- | `xs` | 3 px |
2392
- | `sm` | 6 px |
2393
- | `md` | 10 px (default) |
2394
- | `lg` | 16 px |
2395
- | `xl` | 24 px |
2396
-
2397
- #### Shapes
2398
-
2399
- | Shape | Border radius |
2400
- |---|---|
2401
- | `rounded` | `height / 2` (default) |
2402
- | `square` | `0` |
2403
- | `pill` | `999` |
2404
-
2405
- #### Label positions
2406
-
2407
- | Position | Placement |
2408
- |---|---|
2409
- | `none` | Hidden (default) |
2410
- | `above` | Right-aligned above the bar |
2411
- | `below` | Right-aligned below the bar |
2412
- | `right` | Inline to the right of the bar |
2413
- | `inside` | Centred inside the filled bar (requires `lg` or `xl`) |
2414
-
2415
- #### `StyledProgressColors`
2416
-
2417
- | Field | Default | Description |
2418
- |---|---|---|
2419
- | `fill` | `blue[500]` | Filled track colour |
2420
- | `track` | `gray[100]` | Background track colour |
2421
- | `buffer` | `gray[300]` | Buffer layer colour (`buffer` variant) |
2422
- | `stripe` | `rgba(255,255,255,0.25)` | Stripe overlay (`striped` variant) |
2423
- | `gradFrom` | `blue[400]` | Gradient start (`gradient` variant) |
2424
- | `gradTo` | `indigo[600]` | Gradient end (`gradient` variant) |
2425
- | `label` | `gray[700]` | External label text colour |
2426
- | `labelInside` | `white` | Inside label colour |
2427
-
2428
- #### Props
2429
-
2430
- | Prop | Type | Default | Description |
2431
- |---|---|---|---|
2432
- | `value` | `number` | required | Current progress (0–`total`) |
2433
- | `total` | `number` | `100` | Maximum value |
2434
- | `bufferValue` | `number` | = `value` | Buffer position (`buffer` variant) |
2435
- | `variant` | `ProgressVariant` | `'default'` | Visual style |
2436
- | `size` | `ProgressSize` | `'md'` | Height preset |
2437
- | `shape` | `ProgressShape` | `'rounded'` | Bar end shape |
2438
- | `labelPosition` | `LabelPosition` | `'none'` | Where to show the percentage |
2439
- | `label` | `string \| false` | auto `%` | Custom label; `false` hides it |
2440
- | `showSteps` | `boolean` | `false` | Show `value / total` instead of `%` |
2441
- | `segments` | `number` | `5` | Segment count (`segmented` variant) |
2442
- | `segmentGap` | `number` | `3` | Gap between segments in px |
2443
- | `width` | `number` | container width | Explicit pixel width |
2444
- | `animated` | `boolean` | `true` | Animate fill on mount / value change |
2445
- | `animationDuration` | `number` | `600` | Animation duration in ms |
2446
- | `colors` | `StyledProgressColors` | blue theme | Colour overrides |
2447
- | `onAnimationComplete` | `() => void` | — | Fires when animation finishes |
2448
-
2449
- #### Usage
2450
-
2451
- ```tsx
2452
- import { StyledProgressBar } from 'fluent-styles'
2453
-
2454
- // ── 1. Variants ───────────────────────────────────────────────────────────────
2455
- <StyledProgressBar value={65} labelPosition="right" />
2456
-
2457
- <StyledProgressBar value={45} variant="striped" size="lg" labelPosition="right" />
2458
-
2459
- <StyledProgressBar value={72} variant="gradient" labelPosition="right"
2460
- colors={{ gradFrom: '#6366f1', gradTo: '#22d3ee' }} />
2461
-
2462
- // Segmented — workout sets: 5 of 9 complete
2463
- <StyledProgressBar
2464
- value={5}
2465
- total={9}
2466
- variant="segmented"
2467
- segments={9}
2468
- showSteps
2469
- labelPosition="right"
2470
- colors={{ fill: '#8bc34a', track: '#e5e7eb' }}
2471
- />
2472
-
2473
- // Buffer — media player (loaded 60%, played 30%)
2474
- <StyledProgressBar
2475
- value={30}
2476
- bufferValue={60}
2477
- variant="buffer"
2478
- size="sm"
2479
- labelPosition="right"
2480
- colors={{ fill: '#2563eb', buffer: '#bfdbfe', track: '#e5e7eb' }}
2481
- />
2482
-
2483
- // ── 2. Sizes side-by-side ─────────────────────────────────────────────────────
2484
- {(['xs', 'sm', 'md', 'lg', 'xl'] as const).map((s) => (
2485
- <StyledProgressBar key={s} value={65} size={s} labelPosition="right"
2486
- colors={{ fill: '#3b82f6' }} />
2487
- ))}
2488
-
2489
- // ── 3. Label positions ────────────────────────────────────────────────────────
2490
- <StyledProgressBar value={60} labelPosition="above" />
2491
- <StyledProgressBar value={60} labelPosition="below" />
2492
- <StyledProgressBar value={60} labelPosition="right" />
2493
- // Inside label requires lg or xl
2494
- <StyledProgressBar value={60} size="lg" labelPosition="inside"
2495
- colors={{ fill: '#3b82f6', labelInside: '#fff' }} />
2496
- <StyledProgressBar value={55} size="xl" variant="striped" labelPosition="inside"
2497
- colors={{ fill: '#8bc34a', labelInside: '#1a1a1a' }} />
2498
-
2499
- // ── 4. Shapes ─────────────────────────────────────────────────────────────────
2500
- <StyledProgressBar value={65} size="lg" shape="rounded" labelPosition="right" />
2501
- <StyledProgressBar value={65} size="lg" shape="square" labelPosition="right" />
2502
- <StyledProgressBar value={65} size="lg" shape="pill" labelPosition="right" />
2503
-
2504
- // ── 5. Colour themes ─────────────────────────────────────────────────────────
2505
- <StyledProgressBar value={65} size="md" labelPosition="right" colors={{ fill: '#3b82f6', track: '#dbeafe' }} />
2506
- <StyledProgressBar value={65} size="md" labelPosition="right" colors={{ fill: '#8bc34a', track: '#ecfccb' }} />
2507
- <StyledProgressBar value={65} size="md" labelPosition="right" colors={{ fill: '#f43f5e', track: '#ffe4e6' }} />
2508
- <StyledProgressBar value={65} size="md" labelPosition="right" colors={{ fill: '#f59e0b', track: '#fef3c7' }} />
2509
-
2510
- // ── 6. Gradient themes ────────────────────────────────────────────────────────
2511
- {[
2512
- { label: 'Indigo → Cyan', from: '#6366f1', to: '#22d3ee' },
2513
- { label: 'Rose → Orange', from: '#f43f5e', to: '#fb923c' },
2514
- { label: 'Lime → Emerald', from: '#a3e635', to: '#10b981' },
2515
- { label: 'Violet → Pink', from: '#8b5cf6', to: '#ec4899' },
2516
- ].map(({ from, to }) => (
2517
- <StyledProgressBar value={70} variant="gradient" size="md" labelPosition="right"
2518
- colors={{ gradFrom: from, gradTo: to }} />
2519
- ))}
2520
-
2521
- // ── 7. Controlled with button strip ──────────────────────────────────────────
2522
- const [progress, setProgress] = useState(45)
2523
-
2524
- <StyledProgressBar value={progress} variant="gradient" size="lg" labelPosition="above"
2525
- animationDuration={400}
2526
- colors={{ gradFrom: '#6366f1', gradTo: '#22d3ee' }} />
2527
-
2528
- <Stack horizontal gap={10} justifyContent="center">
2529
- {[0, 25, 50, 75, 100].map((v) => (
2530
- <StyledPressable key={v} onPress={() => setProgress(v)}
2531
- paddingHorizontal={14} paddingVertical={8} borderRadius={20}
2532
- backgroundColor={progress === v ? '#6366f1' : '#f3f4f6'}>
2533
- <StyledText fontSize={13} fontWeight="600"
2534
- color={progress === v ? '#fff' : '#374151'}>{v}%</StyledText>
2535
- </StyledPressable>
2536
- ))}
2537
- </Stack>
2538
-
2539
- // ── 8. Real-world: workout card ───────────────────────────────────────────────
2540
- {[
2541
- { title: 'Cardio', progress: 65, label: '4 Of 6', color: '#dc2626', bg: '#fff0f0' },
2542
- { title: 'Muscle', progress: 62, label: '5 Of 8', color: '#9333ea', bg: '#fdf4ff' },
2543
- { title: 'Weight', progress: 44, label: '4 Of 9', color: '#ea580c', bg: '#fff7ed' },
2544
- ].map(({ title, progress, label, color, bg }) => (
2545
- <Stack key={title} marginBottom={16}>
2546
- <Stack horizontal alignItems="center" gap={10} marginBottom={8}>
2547
- <Stack width={32} height={32} borderRadius={16} backgroundColor={bg}
2548
- alignItems="center" justifyContent="center">
2549
- <Icon name="activity" size={14} color={color} />
2550
- </Stack>
2551
- <StyledText fontSize={14} fontWeight="700">{title}</StyledText>
2552
- </Stack>
2553
- <StyledProgressBar
2554
- value={progress}
2555
- size="sm"
2556
- labelPosition="right"
2557
- label={label}
2558
- colors={{ fill: color, track: '#f3f4f6' }}
2559
- />
2560
- </Stack>
2561
- ))}
2562
-
2563
- // ── 9. Real-world: file upload status ─────────────────────────────────────────
2564
- {[
2565
- { name: 'design-assets.zip', size: '24.5 MB', value: 100, done: true },
2566
- { name: 'report-final.pdf', size: '3.2 MB', value: 67, done: false },
2567
- { name: 'video-export.mp4', size: '128 MB', value: 23, done: false },
2568
- ].map(({ name, size, value, done }) => (
2569
- <Stack key={name} marginBottom={14}>
2570
- <Stack horizontal alignItems="center" justifyContent="space-between" marginBottom={6}>
2571
- <StyledText fontSize={13} fontWeight="600">{name}</StyledText>
2572
- <StyledText fontSize={11} color="#9ca3af">{size}</StyledText>
2573
- </Stack>
2574
- <StyledProgressBar
2575
- value={value}
2576
- size="xs"
2577
- shape="pill"
2578
- colors={{ fill: done ? '#10b981' : '#3b82f6', track: '#f3f4f6' }}
2579
- />
2580
- </Stack>
2581
- ))}
2582
-
2583
- // ── 10. Real-world: skills profile ────────────────────────────────────────────
2584
- {[
2585
- { skill: 'React Native', pct: 92 },
2586
- { skill: 'TypeScript', pct: 85 },
2587
- { skill: 'UI Design', pct: 74 },
2588
- ].map(({ skill, pct }) => (
2589
- <Stack key={skill} horizontal alignItems="center" gap={12} marginBottom={12}>
2590
- <StyledText fontSize={13} fontWeight="600" width={100}>{skill}</StyledText>
2591
- <Stack flex={1}>
2592
- <StyledProgressBar value={pct} size="md" variant="gradient" labelPosition="right"
2593
- colors={{ gradFrom: '#6366f1', gradTo: '#8b5cf6' }} />
2594
- </Stack>
2595
- </Stack>
2596
- ))}
2597
- ```
2598
-
2599
- ---
2600
-
2601
- ### StyledSlider
2602
-
2603
- Gesture-driven slider with PanResponder, animated thumb scale, tooltip, tick marks, and 5 variants. Range mode manages two independent thumbs. Backed by `react-native-svg` for gradient fills.
2604
-
2605
- ```tsx
2606
- import { StyledSlider } from 'fluent-styles'
2607
- ```
2608
-
2609
- #### Variants
2610
-
2611
- | Variant | Description |
2612
- |---|---|
2613
- | `default` | Single thumb, fill left of thumb |
2614
- | `range` | Two thumbs, fill between them |
2615
- | `stepped` | Snaps to discrete tick marks |
2616
- | `gradient` | Gradient-filled track |
2617
- | `buffer` | Primary thumb + secondary buffer fill (media player style) |
2618
-
2619
- #### Sizes
2620
-
2621
- | Size | Track height | Thumb diameter |
2622
- |---|---|---|
2623
- | `sm` | 4 px | 18 px |
2624
- | `md` | 6 px (default) | 24 px |
2625
- | `lg` | 10 px | 32 px |
2626
-
2627
- #### `StyledSliderColors`
2628
-
2629
- | Field | Default | Description |
2630
- |---|---|---|
2631
- | `fill` | `#3b82f6` | Filled track colour |
2632
- | `track` | `gray[200]` | Background track |
2633
- | `buffer` | `gray[300]` | Buffer fill (`buffer` variant) |
2634
- | `thumb` | `white` | Thumb fill |
2635
- | `thumbBorder` | = `fill` | Thumb border colour |
2636
- | `gradFrom` | `#60a5fa` | Gradient start (`gradient` variant) |
2637
- | `gradTo` | `#4f46e5` | Gradient end (`gradient` variant) |
2638
- | `tooltipBg` | `#111827` | Tooltip background |
2639
- | `tooltipText` | `white` | Tooltip text |
2640
- | `rangeLabel` | `gray[400]` | Min/max label colour |
2641
- | `tick` | `gray[300]` | Inactive tick colour |
2642
- | `tickActive` | = `fill` | Filled tick colour |
2643
-
2644
- #### Props
2645
-
2646
- | Prop | Type | Default | Description |
2647
- |---|---|---|---|
2648
- | `value` | `number` | required | Current thumb value (or low thumb for `range`) |
2649
- | `valueHigh` | `number` | — | High thumb value (`range` variant) |
2650
- | `bufferValue` | `number` | — | Buffer position (`buffer` variant) |
2651
- | `min` | `number` | `0` | Minimum value |
2652
- | `max` | `number` | `100` | Maximum value |
2653
- | `step` | `number` | `1` | Step increment |
2654
- | `variant` | `SliderVariant` | `'default'` | Visual style |
2655
- | `size` | `SliderSize` | `'md'` | Track/thumb size preset |
2656
- | `showTooltip` | `boolean` | `true` | Show tooltip while dragging |
2657
- | `alwaysShowTooltip` | `boolean` | `false` | Keep tooltip permanently visible |
2658
- | `showMinMax` | `boolean` | `false` | Display min/max labels at track ends |
2659
- | `steps` | `number` | `5` | Number of ticks (`stepped` variant) |
2660
- | `formatLabel` | `(v: number) => string` | `String(v)` | Custom tooltip/tick label formatter |
2661
- | `width` | `number` | container width | Explicit pixel width |
2662
- | `disabled` | `boolean` | `false` | Disables interaction, reduces opacity |
2663
- | `colors` | `StyledSliderColors` | blue theme | Colour overrides |
2664
- | `onValueChange` | `(value: number) => void` | — | Fires continuously while dragging |
2665
- | `onSlidingComplete` | `(value: number) => void` | — | Fires once on drag release |
2666
- | `onRangeChange` | `(low, high: number) => void` | — | Range drag callback |
2667
- | `onRangeComplete` | `(low, high: number) => void` | — | Range drag-release callback |
2668
-
2669
- #### Usage
2670
-
2671
- ```tsx
2672
- import React, { useState } from 'react'
2673
- import { StyledSlider, Stack, StyledText } from 'fluent-styles'
2674
- import Icon from 'react-native-vector-icons/Feather'
2675
-
2676
- // ── 1. Default — basic volume control ────────────────────────────────────────
2677
- const [vol, setVol] = useState(65)
2678
-
2679
- <StyledSlider value={vol} onValueChange={setVol} showMinMax />
2680
-
2681
- // ── 2. Range — price filter ───────────────────────────────────────────────────
2682
- // valueHigh is required for the range variant.
2683
- const [priceLow, setPriceLow] = useState(20)
2684
- const [priceHigh, setPriceHigh] = useState(75)
2685
-
2686
- <StyledSlider
2687
- variant="range"
2688
- value={priceLow}
2689
- valueHigh={priceHigh}
2690
- min={0}
2691
- max={200}
2692
- step={5}
2693
- onRangeChange={(lo, hi) => { setPriceLow(lo); setPriceHigh(hi) }}
2694
- showMinMax
2695
- formatLabel={(v) => `$${v}`}
2696
- colors={{ fill: '#6366f1', thumbBorder: '#6366f1', tooltipBg: '#6366f1' }}
2697
- />
2698
-
2699
- // ── 3. Stepped — star rating (snaps to 5 ticks) ───────────────────────────────
2700
- // steps controls both the number of ticks and the snap resolution.
2701
- const [rating, setRating] = useState(4)
2702
-
2703
- <StyledSlider
2704
- variant="stepped"
2705
- value={rating}
2706
- min={1} max={5} steps={5}
2707
- onValueChange={setRating}
2708
- alwaysShowTooltip
2709
- size="lg"
2710
- formatLabel={(v) => ['','★','★★','★★★','★★★★','★★★★★'][Math.round(v)] ?? ''}
2711
- colors={{
2712
- fill: '#f59e0b',
2713
- track: '#fef3c7',
2714
- thumbBorder: '#f59e0b',
2715
- tooltipBg: '#f59e0b',
2716
- tickActive: '#f59e0b',
2717
- }}
2718
- />
2719
-
2720
- // ── 4. Gradient — temperature control ────────────────────────────────────────
2721
- const [temp, setTemp] = useState(22)
2722
-
2723
- <StyledSlider
2724
- variant="gradient"
2725
- value={temp}
2726
- min={10} max={35}
2727
- onValueChange={setTemp}
2728
- formatLabel={(v) => `${v}°`}
2729
- alwaysShowTooltip
2730
- showMinMax
2731
- size="lg"
2732
- colors={{
2733
- gradFrom: '#60a5fa',
2734
- gradTo: '#ef4444',
2735
- tooltipBg: temp < 22 ? '#60a5fa' : '#ef4444',
2736
- }}
2737
- />
2738
-
2739
- // ── 5. Buffer — media player seek bar ────────────────────────────────────────
2740
- const formatTime = (s: number) =>
2741
- `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`
2742
-
2743
- const [played, setPlayed] = useState(95)
2744
- const DURATION = 243
2745
-
2746
- <StyledSlider
2747
- variant="buffer"
2748
- value={played}
2749
- bufferValue={Math.min(played + 60, DURATION)}
2750
- min={0}
2751
- max={DURATION}
2752
- size="sm"
2753
- onValueChange={setPlayed}
2754
- formatLabel={formatTime}
2755
- colors={{ fill: '#2563eb', buffer: '#bfdbfe' }}
2756
- />
2757
-
2758
- // ── 6. Sizes ──────────────────────────────────────────────────────────────────
2759
- <StyledSlider value={40} size="sm" onValueChange={() => {}} />
2760
- <StyledSlider value={60} size="md" onValueChange={() => {}} />
2761
- <StyledSlider value={80} size="lg" onValueChange={() => {}} />
2762
-
2763
- // ── 7. Colour themes ──────────────────────────────────────────────────────────
2764
- {[
2765
- { fill: '#8bc34a', track: '#ecfccb', label: 'Lime' },
2766
- { fill: '#f43f5e', track: '#ffe4e6', label: 'Rose' },
2767
- { fill: '#f59e0b', track: '#fef3c7', label: 'Amber' },
2768
- { fill: '#8b5cf6', track: '#ede9fe', label: 'Purple' },
2769
- { fill: '#14b8a6', track: '#ccfbf1', label: 'Teal' },
2770
- ].map(({ fill, track }) => (
2771
- <StyledSlider
2772
- value={65}
2773
- onValueChange={() => {}}
2774
- colors={{ fill, track, thumbBorder: fill, tooltipBg: fill }}
2775
- formatLabel={(v) => `${v}%`}
2776
- />
2777
- ))}
2778
-
2779
- // ── 8. Real-world: Audio / brightness controls with icons ─────────────────────
2780
- <Stack horizontal alignItems="center" gap={12}>
2781
- <Icon name="volume-x" size={18} color="#9ca3af" />
2782
- <Stack flex={1}>
2783
- <StyledSlider value={vol} onValueChange={setVol} showTooltip={false}
2784
- colors={{ fill: '#1a1a1a', track: '#e5e7eb', thumbBorder: '#1a1a1a' }} />
2785
- </Stack>
2786
- <Icon name="volume-2" size={18} color="#9ca3af" />
2787
- <StyledText fontSize={13} fontWeight="600" width={32}>{vol}%</StyledText>
2788
- </Stack>
2789
-
2790
- <Stack horizontal alignItems="center" gap={12}>
2791
- <Icon name="sun" size={18} color="#9ca3af" />
2792
- <Stack flex={1}>
2793
- <StyledSlider value={bright} onValueChange={setBright} showTooltip={false}
2794
- colors={{ fill: '#f59e0b', track: '#fef3c7', thumbBorder: '#f59e0b' }} />
2795
- </Stack>
2796
- <Icon name="sun" size={18} color="#f59e0b" />
2797
- <StyledText fontSize={13} fontWeight="600" width={32}>{bright}%</StyledText>
2798
- </Stack>
2799
-
2800
- // ── 9. Disabled state ─────────────────────────────────────────────────────────
2801
- <StyledSlider value={55} disabled showMinMax />
2802
- ```
2803
-
2804
- ---
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
-
3239
- ## Hooks
3240
-
3241
- All hooks require a `PortalManager` ancestor.
3242
-
3243
- ### useToast
3244
-
3245
- ```tsx
3246
- import { useToast } from 'fluent-styles'
3247
-
3248
- const toast = useToast()
3249
-
3250
- // --- Shortcut methods ---
3251
- toast.success('Profile saved')
3252
- toast.error('Upload failed', 'The selected file is larger than 5 MB.')
3253
- toast.warning('Unsaved changes', 'You have pending edits on this screen.')
3254
- toast.info('New update available', 'Restart the app to use the latest version.')
3255
-
3256
- // --- Full control with show() ---
3257
- const id = toast.show({
3258
- message: 'Settings updated',
3259
- description: 'Your preferences were saved successfully.',
3260
- variant: 'success', // 'success' | 'error' | 'warning' | 'info'
3261
- duration: 2500,
3262
- theme: 'light', // 'light' | 'dark' | 'system'
3263
- })
3264
-
3265
- // Dark-themed toast
3266
- toast.show({
3267
- message: 'Background sync started',
3268
- description: 'We will notify you when sync is complete.',
3269
- variant: 'info',
3270
- duration: 4000,
3271
- theme: 'dark',
3272
- })
3273
-
3274
- // --- Persistent toast (duration: 0 — never auto-dismisses) ---
3275
- const persistId = toast.show({
3276
- message: 'Uploading file…',
3277
- description: 'Please keep the app open until upload finishes.',
3278
- variant: 'info',
3279
- duration: 0,
3280
- theme: 'dark',
3281
- })
3282
- toast.dismiss(persistId) // dismiss manually later
3283
-
3284
- // --- Short / long durations ---
3285
- toast.show({ message: 'Quick message', variant: 'info', duration: 1200, theme: 'light' })
3286
- toast.show({ message: 'Read this carefully', variant: 'warning', duration: 6000, theme: 'light' })
3287
-
3288
- // --- Color token overrides ---
3289
- toast.show({
3290
- message: 'Custom success',
3291
- variant: 'success',
3292
- theme: 'light',
3293
- colors: {
3294
- successBg: '#ecfdf5',
3295
- successBorder: '#10b981',
3296
- successLabel: '#065f46',
3297
- description: '#047857',
3298
- closeIcon: '#065f46',
3299
- },
3300
- })
3301
-
3302
- toast.show({
3303
- message: 'Custom error',
3304
- variant: 'error',
3305
- theme: 'dark',
3306
- colors: {
3307
- errorBg: '#3b0a0a',
3308
- errorBorder: '#ef4444',
3309
- errorLabel: '#fecaca',
3310
- description: '#fca5a5',
3311
- closeIcon: '#fecaca',
3312
- },
3313
- })
3314
-
3315
- // --- Dismiss ---
3316
- toast.dismiss(id) // single
3317
- toast.dismissAll() // all active
3318
- ```
3319
-
3320
- | Method | Signature | Description |
3321
- |---|---|---|
3322
- | `show` | `(options) => number` | Show a toast, returns portal id |
3323
- | `success` | `(message, description?) => number` | Green success toast |
3324
- | `error` | `(message, description?) => number` | Red error toast |
3325
- | `warning` | `(message, description?) => number` | Amber warning toast |
3326
- | `info` | `(message, description?) => number` | Blue info toast |
3327
- | `dismiss` | `(id: number) => void` | Dismiss specific toast |
3328
- | `dismissAll` | `() => void` | Dismiss all active toasts |
3329
-
3330
- **`show` options:** `message`, `description?`, `variant`, `duration` (`0` = persistent), `theme`, `colors`
3331
-
3332
- ---
3333
-
3334
- ### useNotification
3335
-
3336
- ```tsx
3337
- import { useNotification } from 'fluent-styles'
3338
-
3339
- const notification = useNotification()
3340
-
3341
- // --- Basic notification ---
3342
- const id = notification.show({
3343
- title: 'New message from Alex',
3344
- body: 'Hey, are you free this afternoon?',
3345
- source: 'Messages',
3346
- initials: 'AK',
3347
- timestamp: 'now',
3348
- theme: 'dark',
3349
- })
3350
-
3351
- // --- With avatar image ---
3352
- notification.show({
3353
- title: 'Sarah Johnson',
3354
- body: 'Sent you 3 new design files.',
3355
- source: 'Drive',
3356
- avatar: { uri: 'https://example.com/avatar.jpg' },
3357
- timestamp: '2m',
3358
- theme: 'light',
3359
- })
3360
-
3361
- // --- With action button ---
3362
- notification.show({
3363
- title: 'Deployment finished',
3364
- body: 'Production build completed successfully.',
3365
- source: 'CI/CD',
3366
- initials: 'CI',
3367
- timestamp: 'now',
3368
- actionLabel: 'Open',
3369
- onAction: () => navigate('Dashboard'),
3370
- theme: 'dark',
3371
- })
3372
-
3373
- // --- Custom duration ---
3374
- notification.show({ title: 'Quick', body: 'Disappears fast', initials: 'Q', duration: 1500, theme: 'light' })
3375
- notification.show({ title: 'Long', body: 'Stays a while', initials: 'L', duration: 8000, theme: 'dark' })
3376
-
3377
- // --- Color token overrides ---
3378
- notification.show({
3379
- title: 'Custom brand notification',
3380
- body: 'Using token overrides on top of the active theme.',
3381
- source: 'Brand',
3382
- initials: 'BR',
3383
- timestamp: 'now',
3384
- theme: 'light',
3385
- actionLabel: 'View',
3386
- onAction: () => navigate('Brand'),
3387
- colors: {
3388
- background: '#eff6ff',
3389
- border: '#2563eb',
3390
- title: '#1e3a8a',
3391
- body: '#1d4ed8',
3392
- source: '#2563eb',
3393
- timestamp: '#3b82f6',
3394
- avatarBg: '#dbeafe',
3395
- avatarBorder: '#60a5fa',
3396
- avatarInitials: '#1d4ed8',
3397
- actionBg: '#dbeafe',
3398
- actionLabel: '#1d4ed8',
3399
- closeIcon: '#1d4ed8',
3400
- },
3401
- })
3402
-
3403
- // --- Real-world examples ---
3404
- notification.show({
3405
- title: 'New comment on your PR',
3406
- body: 'Chris left feedback on the latest changes.',
3407
- source: 'Git',
3408
- initials: 'CK',
3409
- timestamp: '1m',
3410
- actionLabel: 'Review',
3411
- onAction: () => navigate('PRReview'),
3412
- theme: 'dark',
3413
- })
3414
-
3415
- notification.show({
3416
- title: 'Meeting starts in 10 minutes',
3417
- body: 'Frontend sync with the product team.',
3418
- source: 'Calendar',
3419
- initials: 'CA',
3420
- timestamp: 'soon',
3421
- actionLabel: 'Join',
3422
- onAction: joinMeeting,
3423
- theme: 'light',
3424
- })
3425
-
3426
- notification.dismiss(id)
3427
- ```
3428
-
3429
- **Show options:** `title`, `body`, `avatar`, `initials`, `source`, `timestamp`, `actionLabel`, `onAction`, `duration` (`0` = persistent), `theme`, `colors`
3430
-
3431
- ---
3432
-
3433
- ### useDialogue
3434
-
3435
- ```tsx
3436
- import { useDialogue } from 'fluent-styles'
3437
-
3438
- const dialogue = useDialogue()
3439
-
3440
- // --- Alert (Promise<void>) ---
3441
- await dialogue.alert(
3442
- 'Session expired',
3443
- 'Please log in again to continue.',
3444
- '🔒',
3445
- 'light', // optional theme
3446
- )
3447
-
3448
- // --- Confirm (Promise<boolean>) ---
3449
- const confirmed = await dialogue.confirm({
3450
- title: 'Save changes?',
3451
- message: 'Your edits will be saved to this project.',
3452
- icon: '💾',
3453
- confirmLabel: 'Save',
3454
- cancelLabel: 'Cancel',
3455
- theme: 'light',
3456
- })
3457
- if (confirmed) save()
3458
-
3459
- // --- Destructive confirm ---
3460
- const ok = await dialogue.confirm({
3461
- title: 'Delete project?',
3462
- message: 'This action cannot be undone.',
3463
- icon: '⚠️',
3464
- confirmLabel: 'Delete',
3465
- cancelLabel: 'Keep it',
3466
- destructive: true,
3467
- })
3468
- if (ok) deleteProject()
3469
-
3470
- // --- Custom multi-action dialogue ---
3471
- dialogue.show({
3472
- title: 'Unsaved changes',
3473
- message: 'You have unsaved edits. What would you like to do?',
3474
- icon: '📝',
3475
- theme: 'light',
3476
- actions: [
3477
- { label: 'Discard', variant: 'destructive', onPress: () => discard() },
3478
- { label: 'Save draft', variant: 'secondary', onPress: () => saveDraft() },
3479
- { label: 'Keep editing', variant: 'primary', onPress: () => keepEditing() },
3480
- ],
3481
- })
3482
-
3483
- // --- Async chained flow (confirm then alert) ---
3484
- const publish = async () => {
3485
- const confirmed = await dialogue.confirm({
3486
- title: 'Publish update?',
3487
- message: 'This will make the latest version visible to users.',
3488
- icon: '🚀',
3489
- confirmLabel: 'Publish',
3490
- cancelLabel: 'Not now',
3491
- theme: 'light',
3492
- })
3493
- if (!confirmed) return
3494
-
3495
- await performPublish()
3496
-
3497
- await dialogue.alert('Published', 'Your update is now live.', '✅')
3498
- }
3499
-
3500
- // --- Programmatic dismiss by id ---
3501
- const id = dialogue.show({
3502
- title: 'Temporary dialogue',
3503
- message: 'This will close automatically in 2 seconds.',
3504
- icon: '⏳',
3505
- theme: 'light',
3506
- actions: [{ label: 'OK', variant: 'primary', onPress: () => {} }],
3507
- })
3508
- setTimeout(() => dialogue.dismiss(id), 2000)
3509
-
3510
- // --- Real-world: log out + rate app ---
3511
- const handleLogout = async () => {
3512
- const ok = await dialogue.confirm({ title: 'Log out?', message: 'You will need to sign in again.', icon: '👋', confirmLabel: 'Log out', destructive: true })
3513
- if (ok) logout()
3514
- }
3515
-
3516
- const handleRateApp = () => {
3517
- dialogue.show({
3518
- title: 'Enjoying the app?',
3519
- icon: '⭐',
3520
- actions: [
3521
- { label: '😠 1', variant: 'secondary', onPress: () => submitRating(1) },
3522
- { label: '😐 3', variant: 'secondary', onPress: () => submitRating(3) },
3523
- { label: '😁 5', variant: 'primary', onPress: () => submitRating(5) },
3524
- ],
3525
- })
3526
- }
3527
- ```
3528
-
3529
- **Action variants:** `primary` | `secondary` | `destructive`
3530
-
3531
- ---
3532
-
3533
- ### useActionSheet
3534
-
3535
- ```tsx
3536
- import { useActionSheet } from 'fluent-styles'
3537
-
3538
- const actionSheet = useActionSheet()
3539
-
3540
- // Items list
3541
- actionSheet.show({
3542
- title: 'Post options',
3543
- items: [
3544
- { icon: '✏️', label: 'Edit', onPress: onEdit },
3545
- { icon: '🔗', label: 'Copy link', onPress: onCopy },
3546
- { icon: '🚩', label: 'Report', variant: 'destructive', onPress: onReport },
3547
- { icon: '🔒', label: 'Premium', variant: 'disabled' },
3548
- ],
3549
- })
3550
-
3551
- // Custom content sheet
3552
- actionSheet.present(<MyDatePicker onChange={setDate} />, { title: 'Pick a date' })
3553
-
3554
- // Mixed: content + items
3555
- actionSheet.show({
3556
- title: 'Choose a colour',
3557
- children: <ColorSwatchRow onSelect={setColor} />,
3558
- items: [{ label: 'Reset to default', onPress: resetColor }],
3559
- })
3560
- ```
3561
-
3562
- **ActionSheetItem variants:** `default` | `destructive` | `disabled`
3563
-
3564
- ---
3565
-
3566
- ### useLoader
3567
-
3568
- ```tsx
3569
- import { useLoader } from 'fluent-styles'
3570
-
3571
- const loader = useLoader()
3572
-
3573
- // --- Manual show / hide ---
3574
- const id = loader.show({ label: 'Saving…', variant: 'spinner' })
3575
- await saveData()
3576
- loader.hide(id)
3577
-
3578
- // --- Variants ---
3579
- loader.show({ variant: 'spinner' })
3580
- loader.show({ variant: 'dots', label: 'Processing…' })
3581
- loader.show({ variant: 'pulse', overlay: true })
3582
- loader.show({ variant: 'circular', label: 'Loading…', theme: 'dark' })
3583
-
3584
- // --- Color overrides ---
3585
- loader.show({
3586
- label: 'Preparing analytics…',
3587
- variant: 'circular',
3588
- theme: 'dark',
3589
- colors: { indicator: '#60a5fa', label: '#dbeafe' },
3590
- })
3591
-
3592
- // --- Automatic wrap (always hides, even on error) ---
3593
- const report = await loader.wrap(
3594
- () => api.fetchReport(),
3595
- { label: 'Loading report…', variant: 'dots' },
3596
- )
3597
-
3598
- // --- Wrap example with status feedback ---
3599
- const runFakeTask = async (options, successMsg) => {
3600
- const result = await loader.wrap(
3601
- () => new Promise(resolve => setTimeout(resolve, 2000)),
3602
- options,
3603
- )
3604
- toast.success(successMsg)
3605
- }
3606
- await runFakeTask({ label: 'Saving profile…', variant: 'spinner' }, 'Profile saved')
3607
- await runFakeTask({ label: 'Uploading data…', variant: 'circular' }, 'Upload complete')
3608
- ```
3609
-
3610
- ---
3611
-
3612
- ## Imperative Services
3613
-
3614
- These services are callable from **anywhere** — Redux middleware, Axios interceptors, navigation helpers — because they use the global `portal` singleton. No `PortalManager` is required.
3615
-
3616
- ### toastService
3617
-
3618
- ```ts
3619
- import { toastService } from 'fluent-styles'
3620
-
3621
- toastService.success('Saved!')
3622
- toastService.error('Network error', 'Check your connection.')
3623
- toastService.warning('Session expiring')
3624
- toastService.info('New version available')
3625
-
3626
- const id = toastService.show({ message: 'Custom', variant: 'info', duration: 2000 })
3627
- toastService.dismiss(id)
3628
- ```
3629
-
3630
- ### notificationService
3631
-
3632
- ```ts
3633
- import { notificationService } from 'fluent-styles'
3634
-
3635
- const id = notificationService.show({
3636
- title: 'Payment received',
3637
- body: '$49.99 from John Smith',
3638
- initials: 'JS',
3639
- })
3640
- notificationService.dismiss(id)
3641
- ```
3642
-
3643
- ### dialogueService
3644
-
3645
- ```ts
3646
- import { dialogueService } from 'fluent-styles'
3647
-
3648
- const ok = await dialogueService.confirm({ title: 'Sign out?', destructive: true })
3649
- await dialogueService.alert('Welcome back!', 'You were away for 3 days.')
3650
-
3651
- dialogueService.show({ title: 'Custom', actions: [{ label: 'Got it', onPress: () => {} }] })
3652
- ```
3653
-
3654
- ### actionSheetService
3655
-
3656
- ```ts
3657
- import { actionSheetService } from 'fluent-styles'
3658
-
3659
- actionSheetService.show({
3660
- title: 'Share',
3661
- items: [
3662
- { icon: '📋', label: 'Copy link', onPress: copyLink },
3663
- { icon: '✉️', label: 'Email', onPress: shareEmail },
3664
- ],
3665
- })
3666
-
3667
- actionSheetService.present(<MyPicker />, { title: 'Choose' })
3668
- ```
3669
-
3670
- ### loaderService
3671
-
3672
- ```ts
3673
- import { loaderService } from 'fluent-styles'
3674
-
3675
- const id = loaderService.show({ label: 'Uploading…', variant: 'circular' })
3676
- await uploadFile()
3677
- loaderService.hide(id)
3678
-
3679
- // Wrapped — always hides, even on error
3680
- const result = await loaderService.wrap(() => api.submit(form), { label: 'Submitting…' })
3681
- ```
3682
-
3683
- ---
3684
-
3685
- ## Theme & Tokens
3686
-
3687
- The design token system is fully exported for use in your own components.
3688
-
3689
- ```ts
3690
- import { theme, palettes, lightColors, darkColors, fontStyles } from 'fluent-styles'
3691
-
3692
- // Colour scales (50–900)
3693
- theme.colors.indigo[500] // '#6366f1'
3694
- theme.colors.rose[600] // '#e11d48'
3695
- theme.colors.gray[100] // '#f4f4f5'
3696
-
3697
- // Base values
3698
- palettes.white // '#FFFFFF'
3699
- palettes.black // '#000000'
3700
-
3701
- // Typography scale
3702
- theme.fontSize.small
3703
- theme.fontSize.normal
3704
- theme.fontSize.medium
3705
- theme.fontSize.large
3706
-
3707
- theme.fontWeight.normal
3708
- theme.fontWeight.semiBold
3709
- theme.fontWeight.bold
3710
-
3711
- // Prebuilt font style objects
3712
- fontStyles.body
3713
- fontStyles.heading
3714
- ```
3715
-
3716
- ### Per-component colour token overrides
3717
-
3718
- Every complex component accepts a `colors` prop typed as `Partial<ComponentColors>`. Exported default token maps:
3719
-
3720
- | Export | Used by |
48
+ | Component | Category |
3721
49
  |---|---|
3722
- | `TAB_BAR_COLORS_LIGHT` / `TAB_BAR_COLORS_DARK` | `TabBar` |
3723
- | `POPUP_COLORS_LIGHT` / `POPUP_COLORS_DARK` | `Popup` |
3724
- | `DRAWER_COLORS_LIGHT` / `DRAWER_COLORS_DARK` | `Drawer` |
3725
- | `COLLAPSE_LIGHT` / `COLLAPSE_DARK` | `Collapse` |
3726
- | `LOADER_LIGHT` / `LOADER_DARK` | `Loader` |
3727
-
3728
- ### `theme` prop
3729
-
3730
- Overlay and feedback components accept a `theme` prop:
3731
-
3732
- ```tsx
3733
- <Popup theme="dark" visible={…}>…</Popup>
3734
- <Loader theme="system" /> {/* follows device dark mode */}
3735
- ```
3736
-
3737
- Values: `'light'` | `'dark'` | `'system'`
3738
-
3739
- ---
3740
-
3741
- ## Contributing
3742
-
3743
- Issues and pull requests are welcome. Please open an issue first to discuss any significant changes.
3744
-
3745
- - Repository: [github.com/aaghorighor/fluent-styles](https://github.com/aaghorighor/fluent-styles)
3746
- - Bug reports: [github.com/aaghorighor/fluent-styles/issues](https://github.com/aaghorighor/fluent-styles/issues)
50
+ | `Stack` / `XStack` / `YStack` | Layout |
51
+ | `StyledPage` | Layout |
52
+ | `StyledHeader` | Layout |
53
+ | `StyledCard` | Layout |
54
+ | `StyledSpacer` | Layout |
55
+ | `StyledDivider` | Layout |
56
+ | `StyledSeperator` | Layout |
57
+ | `StyledScrollView` | Layout |
58
+ | `StyledSafeAreaProvider` | Layout |
59
+ | `StyledSafeAreaView` | Layout |
60
+ | `StyledText` | Typography |
61
+ | `StyledButton` | Actions |
62
+ | `StyledPressable` | Actions |
63
+ | `StyledInput` | Form |
64
+ | `StyledForm` | Form |
65
+ | `StyledCheckBox` | Form |
66
+ | `Switch` | Form |
67
+ | `StyledDropdown` | Form |
68
+ | `StyledMultiSelectDropdown` | Form |
69
+ | `StyledRadio` | Form |
70
+ | `StyledSlider` | Form |
71
+ | `StyledDatePicker` | Form |
72
+ | `StyledSearchBar` | Form |
73
+ | `StyledImage` / `StyledImageBackground` | Media |
74
+ | `StyleShape` / `Cycle` | Shape |
75
+ | `StyledBadge` / `BadgeWithIcon` / `BadgeIcon` | Indicators |
76
+ | `StyledChips` | Indicators |
77
+ | `Spinner` / `Circular` / `Loader` | Loading |
78
+ | `StyledProgressBar` | Progress |
79
+ | `StyledCircularProgress` | Progress |
80
+ | `StyledBarChart` | Charts |
81
+ | `StyledDialog` / `StyledConfirmDialog` / `StyledOkDialog` | Overlays |
82
+ | `StyledDrawer` | Overlays |
83
+ | `StyledPopup` | Overlays |
84
+ | `StyledActionSheet` | Overlays |
85
+ | `StyledCollapsible` | Navigation |
86
+ | `StyledTabBar` | Navigation |
87
+ | `StyledTimeline` | Display |
88
+ | `StyledTable` | Display |
89
+ | `StyledSkeleton` | Display |
90
+ | `StyledEmptyState` | Display |
91
+ | `GlobalPortalProvider` | Portal |
92
+
93
+ ---
94
+
95
+ ## Hooks & Services
96
+
97
+ **Hooks**
98
+ - `useToast`
99
+ - `useNotification`
100
+ - `useLoader` / `useLoaderBinding`
101
+ - `useDialogue`
102
+ - `useActionSheet`
103
+ - `usePortal`
104
+ - `usePaginatedQuery`
105
+
106
+ **Imperative Services**
107
+ - `toastService`
108
+ - `notificationService`
109
+ - `loaderService`
110
+ - `dialogueService`
111
+ - `actionSheetService`
112
+
113
+ ---
114
+
115
+ ## Documentation
116
+
117
+ - **Full docs:** https://fluent-styles.io
118
+ - **Live examples:** https://github.com/suftnetrepo/fluent-styles-next/tree/main/example
3747
119
 
3748
120
  ---
3749
121