This commit is contained in:
oneao committed 2025-11-10 22:37:18 +08:00
1 parent dee2eab404
commit 098a061308
10 files changed
+869 -155

No files matched your search

@@ -0,0 +1,283 @@
import React, {
useState,
createContext,
useContext,
useMemo,
forwardRef,
} from 'react';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { View, Dimensions, Platform, ViewProps } from 'react-native';
import { gridStyle, gridItemStyle } from './styles';
import { cssInterop } from 'nativewind';
import {
useBreakpointValue,
getBreakPointValue,
} from '@gluestack-ui/utils/hooks';
const { width: DEVICE_WIDTH } = Dimensions.get('window');
const GridContext = createContext<any>({});
function arrangeChildrenIntoRows({
childrenArray,
colSpanArr,
numColumns,
}: {
childrenArray: React.ReactNode[];
colSpanArr: number[];
numColumns: number;
}) {
let currentRow = 1;
let currentRowTotalColSpan = 0;
// store how many items in each row
const rowItemsCount: {
[key: number]: number[];
} = {};
for (let i = 0; i < childrenArray.length; i++) {
const colSpan = colSpanArr[i];
// if current row is full, go to next row
if (currentRowTotalColSpan + colSpan > numColumns) {
currentRow++;
currentRowTotalColSpan = colSpan;
} else {
// if current row is not full, add colSpan to current row
currentRowTotalColSpan += colSpan;
}
rowItemsCount[currentRow] = rowItemsCount[currentRow]
? [...rowItemsCount[currentRow], i]
: [i];
}
return rowItemsCount;
}
function generateResponsiveNumColumns({ gridClass }: { gridClass: string }) {
const gridClassNamePattern = /\b(?:\w+:)?grid-cols-?\d+\b/g;
const numColumns = gridClass?.match(gridClassNamePattern);
if (!numColumns) {
return 12;
}
const regex = /^(?:(\w+):)?grid-cols-?(\d+)$/;
const result: any = {};
numColumns.forEach((classname) => {
const match = classname.match(regex);
if (match) {
const prefix = match[1] || 'default';
const value = parseInt(match[2], 10);
result[prefix] = value;
}
});
return result;
}
function generateResponsiveColSpans({
gridItemClassName,
}: {
gridItemClassName: string;
}) {
const gridClassNamePattern = /\b(?:\w+:)?col-span-?\d+\b/g;
const colSpan: any = gridItemClassName?.match(gridClassNamePattern);
if (!colSpan) {
return 1;
}
const regex = /^(?:(\w+):)?col-span-?(\d+)$/;
const result: any = {};
colSpan.forEach((classname: any) => {
const match = classname.match(regex);
if (match) {
const prefix = match[1] || 'default';
const value = parseInt(match[2], 10);
result[prefix] = value;
}
});
return result;
}
type IGridProps = ViewProps &
VariantProps<typeof gridStyle> & {
gap?: number;
rowGap?: number;
columnGap?: number;
flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
padding?: number;
paddingLeft?: number;
paddingRight?: number;
paddingStart?: number;
paddingEnd?: number;
borderWidth?: number;
borderLeftWidth?: number;
borderRightWidth?: number;
_extra: {
className: string;
};
};
const Grid = forwardRef<React.ComponentRef<typeof View>, IGridProps>(
function Grid({ className, _extra, children, ...props }, ref) {
const [calculatedWidth, setCalculatedWidth] = useState<number | null>(null);
const gridClass = _extra?.className;
const obj = generateResponsiveNumColumns({ gridClass });
const responsiveNumColumns: any = useBreakpointValue(obj);
const itemsPerRow = useMemo(() => {
// get the colSpan of each child
const colSpanArr = React.Children.map(children, (child: any) => {
const gridItemClassName = child?.props?._extra?.className;
const colSpan2 = getBreakPointValue(
generateResponsiveColSpans({ gridItemClassName }),
DEVICE_WIDTH
);
const colSpan = colSpan2 ? colSpan2 : 1;
if (colSpan > responsiveNumColumns) {
return responsiveNumColumns;
}
return colSpan;
});
const childrenArray = React.Children.toArray(children);
const rowItemsCount = arrangeChildrenIntoRows({
childrenArray,
colSpanArr,
numColumns: responsiveNumColumns,
});
return rowItemsCount;
}, [responsiveNumColumns, children]);
const childrenWithProps = React.Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { key: index, index: index } as any);
}
return child;
});
const gridClassMerged = `${Platform.select({
web: gridClass ?? '',
})}`;
const contextValue = useMemo(() => {
return {
calculatedWidth,
numColumns: responsiveNumColumns,
itemsPerRow,
flexDirection: props?.flexDirection || 'row',
gap: props?.gap || 0,
columnGap: props?.columnGap || 0,
};
}, [calculatedWidth, itemsPerRow, responsiveNumColumns, props]);
const borderLeftWidth = props?.borderLeftWidth || props?.borderWidth || 0;
const borderRightWidth = props?.borderRightWidth || props?.borderWidth || 0;
const borderWidthToSubtract = borderLeftWidth + borderRightWidth;
return (
<GridContext.Provider value={contextValue}>
<View
ref={ref}
className={gridStyle({
class: className + ' ' + gridClassMerged,
})}
onLayout={(event) => {
const paddingLeftToSubtract =
props?.paddingStart || props?.paddingLeft || props?.padding || 0;
const paddingRightToSubtract =
props?.paddingEnd || props?.paddingRight || props?.padding || 0;
const gridWidth =
Math.floor(event.nativeEvent.layout.width) -
paddingLeftToSubtract -
paddingRightToSubtract -
borderWidthToSubtract;
setCalculatedWidth(gridWidth);
}}
{...props}
>
{calculatedWidth && childrenWithProps}
</View>
</GridContext.Provider>
);
}
);
cssInterop(Grid, {
className: {
target: 'style',
nativeStyleToProp: {
gap: 'gap',
rowGap: 'rowGap',
columnGap: 'columnGap',
flexDirection: 'flexDirection',
padding: 'padding',
paddingLeft: 'paddingLeft',
paddingRight: 'paddingRight',
paddingStart: 'paddingStart',
paddingEnd: 'paddingEnd',
borderWidth: 'borderWidth',
borderLeftWidth: 'borderLeftWidth',
borderRightWidth: 'borderRightWidth',
},
},
});
type IGridItemProps = ViewProps &
VariantProps<typeof gridItemStyle> & {
index?: number;
_extra: {
className: string;
};
};
const GridItem = forwardRef<React.ComponentRef<typeof View>, IGridItemProps>(
function GridItem({ className, _extra, ...props }, ref) {
const {
calculatedWidth,
numColumns,
itemsPerRow,
flexDirection,
gap,
columnGap,
} = useContext(GridContext);
const gridItemClass = _extra?.className;
const responsiveColSpan = (useBreakpointValue(
generateResponsiveColSpans({ gridItemClassName: gridItemClass })
) ?? 1) as number;
const flexBasisValue = useMemo(() => {
if (!calculatedWidth || !numColumns || responsiveColSpan <= 0) {
return 'auto';
}
if (flexDirection?.includes('column')) {
return 'auto';
}
// Find which row this item is in
const row = Object.keys(itemsPerRow).find((key) => {
return itemsPerRow[key].includes(props?.index);
});
if (!row) {
return 'auto';
}
const rowColsCount = itemsPerRow[row]?.length || 1;
const space = columnGap || gap || 0;
// Calculate available width accounting for gaps
const totalGapWidth = space * (rowColsCount - 1);
const availableWidth = calculatedWidth - totalGapWidth;
// Calculate the width for this item based on its column span
const itemWidth = (availableWidth * responsiveColSpan) / numColumns;
// Return the width directly instead of percentage for better native compatibility
return Math.max(0, Math.floor(itemWidth));
}, [
calculatedWidth,
responsiveColSpan,
numColumns,
columnGap,
gap,
flexDirection,
itemsPerRow,
props?.index,
]);
return (
<View
ref={ref}
// @ts-expect-error : internal implementation for r-19/react-native-web
gridItemClass={gridItemClass}
className={gridItemStyle({
class: className,
})}
{...props}
style={[
{
width:
typeof flexBasisValue === 'number' ? flexBasisValue : undefined,
flexBasis:
typeof flexBasisValue === 'string' ? flexBasisValue : undefined,
flexShrink: 0,
flexGrow: 0,
},
props.style,
]}
/>
);
}
);
Grid.displayName = 'Grid';
GridItem.displayName = 'GridItem';
export { Grid, GridItem };
@@ -0,0 +1,66 @@
import React from 'react';
import { gridStyle, gridItemStyle } from './styles';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
type IGridProps = React.ComponentPropsWithoutRef<'div'> &
VariantProps<typeof gridStyle> & {
gap?: number;
rowGap?: number;
columnGap?: number;
flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
padding?: number;
paddingLeft?: number;
paddingRight?: number;
paddingStart?: number;
paddingEnd?: number;
_extra: {
className: string;
};
};
const Grid = React.forwardRef<HTMLDivElement, IGridProps>(function Grid(
{ className, _extra, ...props },
ref
) {
const gridClass = _extra?.className;
const finalGridClass = gridClass ?? '';
return (
<div
ref={ref}
className={gridStyle({
class: className + ' ' + finalGridClass,
})}
{...props}
/>
);
});
type IGridItemProps = React.ComponentPropsWithoutRef<'div'> &
VariantProps<typeof gridItemStyle> & {
index?: number;
_extra: {
className: string;
};
};
const GridItem = React.forwardRef<HTMLDivElement, IGridItemProps>(
function GridItem({ className, _extra, ...props }, ref) {
const gridItemClass = _extra?.className;
const finalGridItemClass = gridItemClass ?? '';
return (
<div
ref={ref}
className={gridItemStyle({
class: className + ' ' + finalGridItemClass,
})}
{...props}
/>
);
}
);
Grid.displayName = 'Grid';
GridItem.displayName = 'GridItem';
export { Grid, GridItem };
@@ -0,0 +1,15 @@
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { isWeb } from '@gluestack-ui/utils/nativewind-utils';
const gridBaseStyle = isWeb
? 'grid grid-cols-12'
: 'box-border flex-row flex-wrap justify-start';
const gridItemBaseStyle = isWeb ? 'w-auto col-span-1' : '';
export const gridStyle = tva({
base: `w-full ${gridBaseStyle}`,
});
export const gridItemStyle = tva({
base: `w-full ${gridItemBaseStyle}`,
});
@@ -0,0 +1,169 @@
'use client';
import React from 'react';
import { createProgress } from '@gluestack-ui/core/progress/creator';
import { View } from 'react-native';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/utils/nativewind-utils';
import { cssInterop } from 'nativewind';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
const SCOPE = 'PROGRESS';
export const UIProgress = createProgress({
Root: withStyleContext(View, SCOPE),
FilledTrack: View,
});
cssInterop(UIProgress, { className: 'style' });
cssInterop(UIProgress.FilledTrack, { className: 'style' });
const progressStyle = tva({
base: 'bg-background-300 rounded-full w-full',
variants: {
orientation: {
horizontal: 'w-full',
vertical: 'h-full',
},
size: {
'xs': 'h-1',
'sm': 'h-2',
'md': 'h-3',
'lg': 'h-4',
'xl': 'h-5',
'2xl': 'h-6',
},
},
compoundVariants: [
{
orientation: 'vertical',
size: 'xs',
class: 'h-full w-1 justify-end',
},
{
orientation: 'vertical',
size: 'sm',
class: 'h-full w-2 justify-end',
},
{
orientation: 'vertical',
size: 'md',
class: 'h-full w-3 justify-end',
},
{
orientation: 'vertical',
size: 'lg',
class: 'h-full w-4 justify-end',
},
{
orientation: 'vertical',
size: 'xl',
class: 'h-full w-5 justify-end',
},
{
orientation: 'vertical',
size: '2xl',
class: 'h-full w-6 justify-end',
},
],
});
const progressFilledTrackStyle = tva({
base: 'bg-primary-500 rounded-full',
parentVariants: {
orientation: {
horizontal: 'w-full',
vertical: 'h-full',
},
size: {
'xs': 'h-1',
'sm': 'h-2',
'md': 'h-3',
'lg': 'h-4',
'xl': 'h-5',
'2xl': 'h-6',
},
},
parentCompoundVariants: [
{
orientation: 'vertical',
size: 'xs',
class: 'h-full w-1',
},
{
orientation: 'vertical',
size: 'sm',
class: 'h-full w-2',
},
{
orientation: 'vertical',
size: 'md',
class: 'h-full w-3',
},
{
orientation: 'vertical',
size: 'lg',
class: 'h-full w-4',
},
{
orientation: 'vertical',
size: 'xl',
class: 'h-full w-5',
},
{
orientation: 'vertical',
size: '2xl',
class: 'h-full w-6',
},
],
});
type IProgressProps = VariantProps<typeof progressStyle> &
React.ComponentProps<typeof UIProgress>;
type IProgressFilledTrackProps = VariantProps<typeof progressFilledTrackStyle> &
React.ComponentProps<typeof UIProgress.FilledTrack>;
const Progress = React.forwardRef<
React.ComponentRef<typeof UIProgress>,
IProgressProps
>(function Progress(
{ className, size = 'md', orientation = 'horizontal', ...props },
ref
) {
return (
<UIProgress
ref={ref}
{...props}
className={progressStyle({ size, orientation, class: className })}
context={{ size, orientation }}
orientation={orientation}
/>
);
});
const ProgressFilledTrack = React.forwardRef<
React.ComponentRef<typeof UIProgress.FilledTrack>,
IProgressFilledTrackProps
>(function ProgressFilledTrack({ className, ...props }, ref) {
const { size: parentSize, orientation: parentOrientation } =
useStyleContext(SCOPE);
return (
<UIProgress.FilledTrack
ref={ref}
className={progressFilledTrackStyle({
parentVariants: {
size: parentSize,
orientation: parentOrientation,
},
class: className,
})}
{...props}
/>
);
});
export { Progress, ProgressFilledTrack };