@cdx-ui/components 0.0.1-beta.121 → 0.0.1-beta.123

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 (43) hide show
  1. package/lib/commonjs/components/ScrollShadow/index.js +82 -0
  2. package/lib/commonjs/components/ScrollShadow/index.js.map +1 -0
  3. package/lib/commonjs/components/ScrollShadow/styles.js +54 -0
  4. package/lib/commonjs/components/ScrollShadow/styles.js.map +1 -0
  5. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js +83 -0
  6. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js.map +1 -0
  7. package/lib/commonjs/components/Tabs/index.js +525 -0
  8. package/lib/commonjs/components/Tabs/index.js.map +1 -0
  9. package/lib/commonjs/components/Tabs/styles.js +333 -0
  10. package/lib/commonjs/components/Tabs/styles.js.map +1 -0
  11. package/lib/commonjs/components/index.js +24 -0
  12. package/lib/commonjs/components/index.js.map +1 -1
  13. package/lib/module/components/ScrollShadow/index.js +78 -0
  14. package/lib/module/components/ScrollShadow/index.js.map +1 -0
  15. package/lib/module/components/ScrollShadow/styles.js +50 -0
  16. package/lib/module/components/ScrollShadow/styles.js.map +1 -0
  17. package/lib/module/components/ScrollShadow/useScrollShadow.js +79 -0
  18. package/lib/module/components/ScrollShadow/useScrollShadow.js.map +1 -0
  19. package/lib/module/components/Tabs/index.js +520 -0
  20. package/lib/module/components/Tabs/index.js.map +1 -0
  21. package/lib/module/components/Tabs/styles.js +330 -0
  22. package/lib/module/components/Tabs/styles.js.map +1 -0
  23. package/lib/module/components/index.js +2 -0
  24. package/lib/module/components/index.js.map +1 -1
  25. package/lib/typescript/components/ScrollShadow/index.d.ts +29 -0
  26. package/lib/typescript/components/ScrollShadow/index.d.ts.map +1 -0
  27. package/lib/typescript/components/ScrollShadow/styles.d.ts +12 -0
  28. package/lib/typescript/components/ScrollShadow/styles.d.ts.map +1 -0
  29. package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts +17 -0
  30. package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts.map +1 -0
  31. package/lib/typescript/components/Tabs/index.d.ts +48 -0
  32. package/lib/typescript/components/Tabs/index.d.ts.map +1 -0
  33. package/lib/typescript/components/Tabs/styles.d.ts +107 -0
  34. package/lib/typescript/components/Tabs/styles.d.ts.map +1 -0
  35. package/lib/typescript/components/index.d.ts +2 -0
  36. package/lib/typescript/components/index.d.ts.map +1 -1
  37. package/package.json +5 -5
  38. package/src/components/ScrollShadow/index.tsx +119 -0
  39. package/src/components/ScrollShadow/styles.ts +57 -0
  40. package/src/components/ScrollShadow/useScrollShadow.ts +110 -0
  41. package/src/components/Tabs/index.tsx +694 -0
  42. package/src/components/Tabs/styles.ts +340 -0
  43. package/src/components/index.ts +2 -0
@@ -0,0 +1,340 @@
1
+ import { Platform, StyleSheet } from 'react-native';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { DISABLED_CURSOR, TRANSITION_COLORS } from '../../styles/constants';
4
+
5
+ // Kept local (rather than imported from `./index`) to avoid a circular import — `index.tsx`
6
+ // imports styles from this file.
7
+ export type TabsVariant = 'outline' | 'solid';
8
+
9
+ // `rounded-full` is used for the indicator + individual tabs, which are small enough that a
10
+ // full capsule radius looks correct in both orientations.
11
+ const SOLID_RADIUS = 'rounded-full';
12
+
13
+ // `rounded-full` (`border-radius/round`, 9999px) for the indicator, tab capsules, and the
14
+ // horizontal solid track/shell — matches Figma SegmentedControl (always much wider than tall,
15
+ // so full rounding reads as a capsule). Applied via StyleSheet on the track/shell because
16
+ // arbitrary-value radius classes don't reliably resolve through Uniwind's scanner.
17
+ const SOLID_TRACK_RADIUS = 9999;
18
+
19
+ /**
20
+ * Vertical solid track radius — deliberately *not* `SOLID_TRACK_RADIUS` (9999px). A vertical
21
+ * track's box can end up nearly square (few short tab labels stacked in a column), and a
22
+ * fully-round radius on a near-square box renders as a circle/blob instead of a rounded rect.
23
+ * A fixed, modest radius avoids that regardless of how many tabs are stacked or how wide their
24
+ * labels are.
25
+ */
26
+ const SOLID_VERTICAL_TRACK_RADIUS = 20;
27
+
28
+ /** Thickness of the 1px gray rail the outline indicator rides beside. */
29
+ const OUTLINE_TRACK_THICKNESS = 1;
30
+ /** Thickness of the 2px action-colored outline selection segment (animated size, not a class). */
31
+ export const OUTLINE_INDICATOR_THICKNESS = 2;
32
+
33
+ /**
34
+ * Shared duration (ms) for the indicator's `withTiming` glide (`index.tsx`) and the tab's
35
+ * `duration-300` transition class (below) — kept in sync so the focus ring doesn't snap in
36
+ * ahead of a still-sliding solid/outline indicator. Change both together.
37
+ */
38
+ export const TABS_INDICATOR_TRANSITION_MS = 300;
39
+
40
+ /** Outline horizontal scroll — narrow fade reads cleanly against the page background. */
41
+ export const TABS_SCROLL_FADE_SIZE = 32;
42
+
43
+ /**
44
+ * Solid horizontal scroll — wider fade so tabs dissolve smoothly into the track (and under the
45
+ * overlay chevron) instead of ending in a hard clip at the scroll viewport edge.
46
+ */
47
+ export const TABS_SOLID_SCROLL_FADE_SIZE = 40;
48
+
49
+ /**
50
+ * Room reserved on the left/right sides inside the horizontal-scroll clip rect for ink that
51
+ * paints past a tab's own box: `shadow-sm` (`0px 1px 3px`, solid only) and the web focus ring
52
+ * (`ring-2` + `ring-offset-2`, ~4px, both variants). `overflow-x: auto` + `overflow-y: visible`
53
+ * does *not* achieve this — per the CSS Overflow spec, a non-`visible` value on one axis forces
54
+ * `visible` on the other axis to compute as `auto`, so it still clips. Applies to both variants —
55
+ * outline's ring bleeds past its box exactly the same way solid's does.
56
+ */
57
+ /** Reserved ink room (focus ring, shadow) inside the horizontal-scroll clip rect — 4px (`p-1`). */
58
+ const INDICATOR_BLEED = 4;
59
+
60
+ /**
61
+ * Same bleed room as `INDICATOR_BLEED` on the top/bottom sides — kept equal to the solid shell's
62
+ * own `py-1` (below) so the shell's `overflow: hidden` clip boundary sits exactly where the
63
+ * reserved ink stops, with no gap and no clipping. Solid indicator = 32px, whole tablist = 40px
64
+ * per Figma SegmentedControl — a wider value here would inflate the shell past 40px.
65
+ */
66
+ const INDICATOR_BLEED_VERTICAL = INDICATOR_BLEED;
67
+
68
+ /**
69
+ * Numeric styles that can't be expressed as static Uniwind utility classes — solid track radius
70
+ * and outline indicator offset (`bottom-px` / `left-px` don't apply on Reanimated host views).
71
+ */
72
+ export const tabsStyles = StyleSheet.create({
73
+ solidTrack: {
74
+ borderRadius: SOLID_TRACK_RADIUS,
75
+ },
76
+ /** See `SOLID_VERTICAL_TRACK_RADIUS` — used instead of `solidTrack` for vertical solid tracks. */
77
+ solidTrackVertical: {
78
+ borderRadius: SOLID_VERTICAL_TRACK_RADIUS,
79
+ },
80
+ /** Clips scroll ink + edge fades to the solid shell's rounded rect (className radius alone won't). */
81
+ solidShell: {
82
+ borderRadius: SOLID_TRACK_RADIUS,
83
+ overflow: 'hidden',
84
+ },
85
+ horizontalScrollContent: {
86
+ paddingHorizontal: INDICATOR_BLEED,
87
+ paddingVertical: INDICATOR_BLEED_VERTICAL,
88
+ },
89
+ horizontalScrollViewBleed: {
90
+ marginVertical: -INDICATOR_BLEED_VERTICAL,
91
+ },
92
+ outlineIndicator: {
93
+ zIndex: 1,
94
+ },
95
+ outlineIndicatorHorizontal: {
96
+ bottom: OUTLINE_TRACK_THICKNESS,
97
+ },
98
+ outlineIndicatorVertical: {
99
+ left: OUTLINE_TRACK_THICKNESS,
100
+ },
101
+ solidIndicator: {
102
+ zIndex: 0,
103
+ },
104
+ /** Clips press/hover feedback to the solid tab capsule (`rounded-full` alone is not enough on Pressable). */
105
+ solidTabPressable: {
106
+ borderRadius: 9999,
107
+ overflow: 'hidden',
108
+ },
109
+ });
110
+
111
+ export const tabsRootVariants = cva([], {
112
+ variants: {
113
+ orientation: {
114
+ horizontal: 'flex-col',
115
+ vertical: 'flex-row items-start gap-4',
116
+ },
117
+ },
118
+ defaultVariants: { orientation: 'horizontal' },
119
+ });
120
+
121
+ export const tabsListVariants = cva(['relative min-w-0']);
122
+
123
+ /** Flex track that hosts the rail, indicator, and tab triggers. */
124
+ export const tabsListTrackVariants = cva(['relative items-stretch'], {
125
+ variants: {
126
+ orientation: {
127
+ horizontal: 'flex-row',
128
+ vertical: 'flex-col',
129
+ },
130
+ variant: {
131
+ outline: '',
132
+ solid: 'gap-0.5',
133
+ },
134
+ /**
135
+ * When horizontal tabs scroll, the solid track background + radius live on
136
+ * `tabsListSolidShellVariants` (viewport-sized) so rounded corners aren't clipped
137
+ * by the ScrollView.
138
+ */
139
+ scrollable: {
140
+ true: '',
141
+ false: '',
142
+ },
143
+ },
144
+ compoundVariants: [
145
+ { orientation: 'vertical', variant: 'outline', className: 'gap-2 pl-1' },
146
+ { orientation: 'horizontal', variant: 'outline', className: 'pb-1' },
147
+ { variant: 'solid', scrollable: false, className: 'bg-surface-secondary p-1' },
148
+ // Whole tablist = 40px per Figma SegmentedControl (`p-1` + 32px tab, above). Height-only —
149
+ // vertical solid lists size to their stacked tab content instead.
150
+ {
151
+ orientation: 'horizontal',
152
+ variant: 'solid',
153
+ scrollable: false,
154
+ className: 'h-10 max-h-10 min-h-10',
155
+ },
156
+ ],
157
+ defaultVariants: { orientation: 'horizontal', variant: 'solid', scrollable: false },
158
+ });
159
+
160
+ /**
161
+ * Viewport-sized solid shell for horizontal scroll — bg is a separate layer so overflow can stay
162
+ * visible. `py-1` (4px) matches the non-scrolling track's `p-1` exactly, so both the scrollable
163
+ * and non-scrollable solid paths land on the same 40px whole-tablist height (32px tab + 4px top
164
+ * + 4px bottom). `h-10`/`max-h-10`/`min-h-10` pin it explicitly so a taller-than-expected label
165
+ * line-height can't inflate the shell. `INDICATOR_BLEED_VERTICAL` (the ScrollView's own top/bottom
166
+ * reserved ink room, above) is kept equal to this `py-1` so the shell's `overflow: hidden` clip
167
+ * boundary lines up with where that reserved ink stops.
168
+ */
169
+ export const tabsListSolidShellVariants = cva([
170
+ 'relative min-w-0 w-full py-1 h-10 max-h-10 min-h-10',
171
+ ]);
172
+
173
+ export const tabsListSolidShellBgVariants = cva(['absolute inset-0 bg-surface-secondary']);
174
+
175
+ export const tabsListScrollViewVariants = cva(['w-full max-w-full']);
176
+
177
+ export const tabsListScrollContainerVariants = cva(['relative min-w-0 w-full']);
178
+
179
+ // Outline: flex sibling beside the scroll viewport (outline scroll looks good this way).
180
+ export const tabsListScrollButtonVariants = cva([
181
+ 'z-10 flex h-6 w-6 shrink-0 items-center justify-center self-center rounded-full',
182
+ TRANSITION_COLORS,
183
+ Platform.select({
184
+ default: 'data-[active=true]:bg-surface-secondary-active',
185
+ web: 'data-[hover=true]:bg-surface-secondary-hover data-[active=true]:data-[hover=true]:bg-surface-secondary-active',
186
+ }),
187
+ ]);
188
+
189
+ // Solid: tiny overlay on the edge fade — the scroll viewport stays full-width inside the shell so
190
+ // the track radius + gradient read as one continuous surface (no hard clip before the chevron).
191
+ export const tabsListSolidScrollButtonVariants = cva([
192
+ 'h-5 w-3 items-center justify-center rounded-full',
193
+ TRANSITION_COLORS,
194
+ Platform.select({
195
+ default: 'data-[active=true]:bg-surface-secondary-active',
196
+ web: 'data-[hover=true]:bg-surface-secondary-hover data-[active=true]:data-[hover=true]:bg-surface-secondary-active',
197
+ }),
198
+ ]);
199
+
200
+ export const tabsListSolidScrollButtonSlotVariants = cva(
201
+ ['pointer-events-none absolute inset-y-0 z-20 flex w-3 justify-center'],
202
+ {
203
+ variants: {
204
+ position: {
205
+ start: 'left-1',
206
+ end: 'right-1',
207
+ },
208
+ },
209
+ },
210
+ );
211
+
212
+ // Horizontal: `h-px border-b` (1px). Vertical: Divider-style `w-px bg-*` (no border — `border`
213
+ // on `w-px` doubles thickness to 2px). Gutter padding on the list keeps tabs off the rail.
214
+ export const tabsOutlineTrackVariants = cva(['absolute z-0'], {
215
+ variants: {
216
+ orientation: {
217
+ horizontal: 'bottom-0 left-0 right-0 h-px border-b border-stroke-secondary',
218
+ vertical: 'top-0 bottom-0 left-0 w-px h-full bg-stroke-secondary',
219
+ },
220
+ },
221
+ defaultVariants: { orientation: 'horizontal' },
222
+ });
223
+
224
+ export const tabsIndicatorVariants = cva(['absolute'], {
225
+ variants: {
226
+ orientation: {
227
+ horizontal: '',
228
+ vertical: '',
229
+ },
230
+ variant: {
231
+ outline: 'bg-surface-action-strong',
232
+ solid: `${SOLID_RADIUS} bg-surface-primary shadow-sm`,
233
+ },
234
+ },
235
+ compoundVariants: [
236
+ // Rail offset is in `tabsStyles.outlineIndicator*` (Reanimated host).
237
+ {
238
+ orientation: 'horizontal',
239
+ variant: 'outline',
240
+ className: 'left-0',
241
+ },
242
+ {
243
+ orientation: 'vertical',
244
+ variant: 'outline',
245
+ className: 'top-0',
246
+ },
247
+ { variant: 'solid', className: 'top-0 left-0' },
248
+ ],
249
+ defaultVariants: { orientation: 'horizontal', variant: 'solid' },
250
+ });
251
+
252
+ export const tabsTabVariants = cva(
253
+ [
254
+ 'relative z-10 items-center justify-center',
255
+ // `transition` (not `transition-colors`) — its default property list includes `box-shadow`,
256
+ // so the focus ring eases in over `TABS_INDICATOR_TRANSITION_MS` instead of snapping in
257
+ // ahead of the indicator, which glides over the same duration (see `index.tsx`).
258
+ 'transition duration-300',
259
+ 'data-[disabled=true]:opacity-[var(--opacity-disabled)]',
260
+ DISABLED_CURSOR,
261
+ Platform.select({
262
+ web: [
263
+ 'outline-none',
264
+ 'web:data-[focus-visible=true]:ring-2 web:data-[focus-visible=true]:ring-stroke-focus web:data-[focus-visible=true]:ring-offset-2',
265
+ ].join(' '),
266
+ default: '',
267
+ }),
268
+ ],
269
+ {
270
+ variants: {
271
+ orientation: {
272
+ horizontal: '',
273
+ vertical: '',
274
+ },
275
+ variant: {
276
+ outline: '',
277
+ solid: `${SOLID_RADIUS} overflow-hidden`,
278
+ },
279
+ },
280
+ compoundVariants: [
281
+ // `py-2` keeps horizontal outline tabs closer to the solid variant's compact 32px trigger
282
+ // height. The rail gap comes from the List's own `pb-1` compound variant (in
283
+ // `tabsListVariants`), not from this padding.
284
+ { orientation: 'horizontal', variant: 'outline', className: 'px-4 py-2' },
285
+ {
286
+ orientation: 'vertical',
287
+ variant: 'outline',
288
+ // Leave room for 1px rail + 2px indicator on the left.
289
+ className: 'px-4 py-1 pl-4',
290
+ },
291
+ // Ring-offset color defaults to the page background, which doesn't match the solid
292
+ // track's `bg-surface-secondary` — override it so the offset gap blends with the track
293
+ // instead of showing as a mismatched patch next to the ring. Explicit `h-8` (32px, per
294
+ // Figma SegmentedControl) instead of vertical padding — a fixed height, combined with the
295
+ // base class's `items-center`, centers the label regardless of its line-height, and can't
296
+ // drift the way padding-derived sizing could.
297
+ {
298
+ variant: 'solid',
299
+ className: [
300
+ 'h-8 max-h-8 min-h-8 px-4',
301
+ 'web:data-[focus-visible=true]:ring-offset-surface-secondary',
302
+ ].join(' '),
303
+ },
304
+ ],
305
+ defaultVariants: { orientation: 'horizontal', variant: 'solid' },
306
+ },
307
+ );
308
+
309
+ export const tabsPanelVariants = cva(['data-[state=inactive]:hidden'], {
310
+ variants: {
311
+ orientation: {
312
+ horizontal: '',
313
+ vertical: 'flex-1',
314
+ },
315
+ },
316
+ defaultVariants: { orientation: 'horizontal' },
317
+ });
318
+
319
+ export const tabsTabLabelVariants = cva('body-md', {
320
+ variants: {
321
+ state: {
322
+ // Selection state is a CVA input (not a `data-[]` selector) so inactive/active
323
+ // typography reconciles on native — `data-[state=active]:` on `Text` is unreliable
324
+ // there, and `font-sans-medium` / `font-sans-semibold` are the same family on iOS/Android
325
+ // so weight must come from `font-medium` / `font-semibold` instead.
326
+ inactive: Platform.select({
327
+ web: 'font-sans-medium text-content-secondary',
328
+ default: 'font-medium text-content-secondary',
329
+ }),
330
+ active: Platform.select({
331
+ web: 'font-sans-semibold text-content-primary',
332
+ default: 'font-semibold text-content-primary',
333
+ }),
334
+ },
335
+ },
336
+ defaultVariants: { state: 'inactive' },
337
+ });
338
+
339
+ export type TabsTabVariantProps = VariantProps<typeof tabsTabVariants>;
340
+ export type TabsListTrackVariantProps = VariantProps<typeof tabsListTrackVariants>;
@@ -25,10 +25,12 @@ export * from './Skeleton';
25
25
  export * from './ProgressSegmented';
26
26
  export * from './Radio';
27
27
  export * from './ScopedTheme';
28
+ export * from './ScrollShadow';
28
29
  export * from './Select';
29
30
  export * from './Slider';
30
31
  export * from './VirtualizedList';
31
32
  export * from './Switch';
33
+ export * from './Tabs';
32
34
  export * from './Textarea';
33
35
  export * from './Tile';
34
36
  export * from './Toast';