mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Init V3
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { DetailedHTMLProps, Fragment, HTMLAttributes, ReactElement, forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import { classNames } from './classNames';
|
||||
import { NitroLimitedEditionStyledNumberView } from './limited-edition';
|
||||
import { styleNames } from './styleNames';
|
||||
|
||||
type Props<T> = {
|
||||
items: T[];
|
||||
columnCount: number;
|
||||
overscan?: number;
|
||||
estimateSize?: number;
|
||||
itemRender?: (item: T, index?: number) => ReactElement;
|
||||
}
|
||||
|
||||
const InfiniteGridRoot = <T,>(props: Props<T>) =>
|
||||
{
|
||||
const { items = [], columnCount = 4, overscan = 5, estimateSize = 45, itemRender = null } = props;
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: Math.ceil(items.length / columnCount),
|
||||
overscan,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => estimateSize
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const element = parentRef.current;
|
||||
|
||||
if(!element || !items) return;
|
||||
|
||||
const checkAndApplyPadding = () =>
|
||||
{
|
||||
if(!element) return;
|
||||
|
||||
element.style.paddingRight = (element.scrollHeight > element.clientHeight) ? '0.25rem' : '0';
|
||||
};
|
||||
|
||||
checkAndApplyPadding();
|
||||
|
||||
window.addEventListener('resize', checkAndApplyPadding);
|
||||
|
||||
return () =>
|
||||
{
|
||||
window.removeEventListener('resize', checkAndApplyPadding);
|
||||
};
|
||||
}, [ items ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!items || !items.length) return;
|
||||
|
||||
virtualizer.scrollToIndex(0);
|
||||
}, [ items, virtualizer ]);
|
||||
|
||||
const virtualItems = virtualizer.getVirtualItems();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ parentRef }
|
||||
className="overflow-y-auto size-full">
|
||||
<div
|
||||
className="flex flex-col w-full *:pb-1 relative"
|
||||
style={ {
|
||||
height: virtualizer.getTotalSize()
|
||||
} }>
|
||||
{ virtualItems.map(virtualRow => (
|
||||
<div
|
||||
key={ virtualRow.key + 'a' }
|
||||
ref={ virtualizer.measureElement }
|
||||
className={ `grid grid-cols-${ columnCount } gap-1 absolute top-0 left-0 last:pb-0 w-full` }
|
||||
data-index={ virtualRow.index }
|
||||
style={ {
|
||||
height: virtualRow.size,
|
||||
transform: `translateY(${ virtualRow.start }px)`
|
||||
} }>
|
||||
{ Array.from(Array(columnCount)).map((e, i) =>
|
||||
{
|
||||
const item = items[i + (virtualRow.index * columnCount)];
|
||||
|
||||
if(!item) return <Fragment
|
||||
key={ virtualRow.index + i + 'b' } />;
|
||||
|
||||
return (
|
||||
<Fragment key={ i }>
|
||||
{ itemRender(item, i) }
|
||||
</Fragment>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InfiniteGridItem = forwardRef<HTMLDivElement, {
|
||||
itemImage?: string;
|
||||
itemColor?: string;
|
||||
itemActive?: boolean;
|
||||
itemCount?: number;
|
||||
itemCountMinimum?: number;
|
||||
itemUniqueSoldout?: boolean;
|
||||
itemUniqueNumber?: number;
|
||||
itemUnseen?: boolean;
|
||||
itemHighlight?: boolean;
|
||||
disabled?: boolean;
|
||||
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>>((props, ref) =>
|
||||
{
|
||||
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, itemHighlight = false, disabled = false, className = null, style = {}, children = null, ...rest } = props;
|
||||
const [ backgroundImageUrl, setBackgroundImageUrl ] = useState<string>(null);
|
||||
const disposed = useRef<boolean>(false);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!itemImage || !itemImage.length) return;
|
||||
|
||||
const image = new Image();
|
||||
|
||||
image.onload = () =>
|
||||
{
|
||||
if(disposed.current) return;
|
||||
|
||||
setBackgroundImageUrl(image.src);
|
||||
};
|
||||
|
||||
image.src = itemImage;
|
||||
}, [ itemImage ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
disposed.current = false;
|
||||
|
||||
return () =>
|
||||
{
|
||||
disposed.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ ref }
|
||||
className={ classNames(
|
||||
'flex flex-col items-center justify-center cursor-pointer overflow-hidden relative bg-center bg-no-repeat w-full rounded-md border-2',
|
||||
(itemImage && (!backgroundImageUrl || !backgroundImageUrl.length)) && 'nitro-icon icon-loading',
|
||||
itemActive ? 'border-card-grid-item-active bg-card-grid-item-active' : 'border-card-grid-item-border bg-card-grid-item',
|
||||
(itemUniqueSoldout || (itemUniqueNumber > 0)) && 'unique-item',
|
||||
itemUniqueSoldout && 'sold-out',
|
||||
itemUnseen && ' bg-green-500 bg-opacity-40',
|
||||
className
|
||||
) }
|
||||
style={ styleNames(
|
||||
backgroundImageUrl && backgroundImageUrl.length && !(itemUniqueSoldout || (itemUniqueNumber > 0)) && {
|
||||
backgroundImage: `url(${ backgroundImageUrl })`
|
||||
},
|
||||
itemColor && {
|
||||
backgroundColor: itemColor
|
||||
},
|
||||
style
|
||||
) }
|
||||
{ ...rest }>
|
||||
{ (itemCount > itemCountMinimum) &&
|
||||
<div className="absolute align-middle rounded bg-red-700 bg-opacity-80 text-white border-black border top-[2px] right-[2px] text-[9.5px] p-[2px] z-[1] leading-[8px]">{ itemCount }</div> }
|
||||
{ (itemUniqueNumber > 0) &&
|
||||
<>
|
||||
<div
|
||||
className="size-full unique-bg-override"
|
||||
style={ {
|
||||
backgroundImage: `url(${ backgroundImageUrl })`
|
||||
} } />
|
||||
<div className="absolute bottom-0 unique-item-counter">
|
||||
<NitroLimitedEditionStyledNumberView value={ itemUniqueNumber } />
|
||||
</div>
|
||||
</> }
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
InfiniteGridItem.displayName = 'InfiniteGridItem';
|
||||
|
||||
export const InfiniteGrid = Object.assign(InfiniteGridRoot, {
|
||||
Item: InfiniteGridItem
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef, PropsWithChildren } from 'react';
|
||||
import { classNames } from './classNames';
|
||||
|
||||
const classes = {
|
||||
base: 'inline-flex justify-center items-center gap-2 transition-[background-color] duration-300 transform tracking-wide rounded-md',
|
||||
disabled: '',
|
||||
size: {
|
||||
default: 'px-2 py-0.5 font-medium',
|
||||
lg: 'px-5 py-3 text-base font-medium',
|
||||
xl: 'px-6 py-3.5 text-base font-medium',
|
||||
},
|
||||
outline: {
|
||||
default: 'text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:hover:bg-blue-600 dark:focus:ring-blue-800'
|
||||
},
|
||||
color: {
|
||||
default: 'bg-button-gradient-gray border border-gray-500',
|
||||
}
|
||||
};
|
||||
|
||||
export const NitroButton = forwardRef<HTMLButtonElement, PropsWithChildren<{
|
||||
color?: 'default' | 'dark' | 'ghost';
|
||||
size?: 'default' | 'lg' | 'xl';
|
||||
outline?: boolean;
|
||||
}> & DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>>((props, ref) =>
|
||||
{
|
||||
const { color = 'default', size = 'default', outline = false, disabled = false, type = 'button', className = null, ...rest } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ ref }
|
||||
className={ classNames(
|
||||
classes.base,
|
||||
classes.size[size],
|
||||
outline ? classes.outline[color] : classes.color[color],
|
||||
disabled && classes.disabled,
|
||||
className
|
||||
) }
|
||||
disabled={ disabled }
|
||||
type={ type }
|
||||
{ ...rest } />
|
||||
);
|
||||
});
|
||||
|
||||
NitroButton.displayName = 'NitroButton';
|
||||
@@ -0,0 +1,124 @@
|
||||
import { DetailedHTMLProps, forwardRef, HTMLAttributes, MouseEvent, PropsWithChildren } from 'react';
|
||||
import { DraggableWindow, DraggableWindowPosition, DraggableWindowProps } from '../common';
|
||||
import { classNames } from './classNames';
|
||||
import { NitroItemCountBadge } from './NitroItemCountBadge';
|
||||
|
||||
const NitroCardRoot = forwardRef<HTMLDivElement, PropsWithChildren<{
|
||||
} & DraggableWindowProps> & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>>((props, ref) =>
|
||||
{
|
||||
const { uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, className = null, ...rest } = props;
|
||||
|
||||
return (
|
||||
<DraggableWindow disableDrag={ disableDrag } handleSelector={ handleSelector } uniqueKey={ uniqueKey } windowPosition={ windowPosition }>
|
||||
<div
|
||||
ref={ ref }
|
||||
className={ classNames(
|
||||
'flex flex-col rounded-md shadow border-2 border-card-border overflow-hidden min-w-full min-h-full max-w-full max-h-full',
|
||||
className
|
||||
) }
|
||||
{ ...rest } />
|
||||
</DraggableWindow>
|
||||
);
|
||||
});
|
||||
|
||||
NitroCardRoot.displayName = 'NitroCardRoot';
|
||||
|
||||
const NitroCardHeader = forwardRef<HTMLDivElement, {
|
||||
headerText: string;
|
||||
onCloseClick?: (event: MouseEvent) => void;
|
||||
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>>((props, ref) =>
|
||||
{
|
||||
const { headerText = '', onCloseClick = null, className = null, ...rest } = props;
|
||||
|
||||
const onMouseDown = (event: MouseEvent<HTMLDivElement>) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ ref } className={ classNames('relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header bg-card-header', className) }>
|
||||
<div className="flex items-center justify-center w-full ">
|
||||
<span className="text-xl text-white drop-shadow-lg">{ headerText }</span>
|
||||
<div className="absolute flex items-center justify-center cursor-pointer right-2 p-[2px] ubuntu-close-button" onClick={ onCloseClick } onMouseDownCapture={ onMouseDown } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
NitroCardHeader.displayName = 'NitroCardHeader';
|
||||
|
||||
const NitroCardContent = forwardRef<HTMLDivElement, {
|
||||
isLoading?: boolean;
|
||||
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>>((props, ref) =>
|
||||
{
|
||||
const { isLoading = false, className = null, children = null, ...rest } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ ref }
|
||||
className={ classNames(
|
||||
'flex flex-col overflow-auto bg-card-content-area p-2 h-full',
|
||||
className
|
||||
) }
|
||||
{ ...rest }>
|
||||
{ isLoading &&
|
||||
<div className="absolute top-0 left-0 z-10 opacity-50 size-full bg-muted" /> }
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
NitroCardContent.displayName = 'NitroCardContent';
|
||||
|
||||
const NitroCardTabs = forwardRef<HTMLDivElement, {
|
||||
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>>((props, ref) =>
|
||||
{
|
||||
const { className = null, ...rest } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ ref }
|
||||
className={ classNames(
|
||||
'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-[1px]',
|
||||
className)
|
||||
}
|
||||
{ ...rest } />
|
||||
);
|
||||
});
|
||||
|
||||
NitroCardTabs.displayName = 'NitroCardTabs';
|
||||
|
||||
const NitroCardTabItem = forwardRef<HTMLDivElement, {
|
||||
isActive?: boolean;
|
||||
count?: number;
|
||||
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>>((props, ref) =>
|
||||
{
|
||||
const { isActive = false, count = 0, className = null, children = null, ...rest } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ ref }
|
||||
className={ classNames(
|
||||
'overflow-hidden relative cursor-pointer rounded-t-md flex bg-card-tab-item px-3 py-1 z-[1] border-card-border border-t border-l border-r before:absolute before:w-[93%] before:h-[3px] before:rounded-md before:top-[1.5px] before:left-0 before:right-0 before:m-auto before:z-[1] before:bg-[#C2C9D1]',
|
||||
isActive && 'bg-card-tab-item-active -mb-[1px] before:bg-white',
|
||||
className)
|
||||
}
|
||||
{ ...rest }>
|
||||
<div className="flex items-center justify-center shrink-0">
|
||||
{ children }
|
||||
</div>
|
||||
{ (count > 0) &&
|
||||
<NitroItemCountBadge count={ count } /> }
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
NitroCardTabItem.displayName = 'NitroCardTabItem';
|
||||
|
||||
export const NitroCard = Object.assign(NitroCardRoot, {
|
||||
Header: NitroCardHeader,
|
||||
Content: NitroCardContent,
|
||||
Tabs: NitroCardTabs,
|
||||
TabItem: NitroCardTabItem
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { DetailedHTMLProps, forwardRef, InputHTMLAttributes, PropsWithChildren } from 'react';
|
||||
import { classNames } from './classNames';
|
||||
|
||||
const classes = {
|
||||
base: 'block w-full placeholder-gray-400 border border-gray-300 shadow-sm appearance-none',
|
||||
disabled: '',
|
||||
size: {
|
||||
default: 'px-2 py-2 font-medium',
|
||||
},
|
||||
rounded: 'rounded-md',
|
||||
color: {
|
||||
default: 'focus:outline-none focus:ring-indigo-500 focus:border-indigo-500',
|
||||
}
|
||||
};
|
||||
|
||||
export const NitroInput = forwardRef<HTMLInputElement, PropsWithChildren<{
|
||||
color?: 'default' | 'dark' | 'ghost';
|
||||
inputSize?: 'xs' | 'sm' | 'default' | 'lg' | 'xl';
|
||||
rounded?: boolean;
|
||||
}> & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>>((props, ref) =>
|
||||
{
|
||||
const { color = 'default', inputSize = 'default', rounded = true, disabled = false, type = 'text', autoComplete = 'off', className = null, ...rest } = props;
|
||||
|
||||
return (
|
||||
<input ref={ ref } autoComplete={ autoComplete } className={ classNames( classes.base, classes.size[inputSize], rounded && classes.rounded, classes.color[color], disabled && classes.disabled, className ) }
|
||||
disabled={ disabled }
|
||||
type={ type }
|
||||
{ ...rest } />
|
||||
);
|
||||
});
|
||||
|
||||
NitroInput.displayName = 'NitroInput';
|
||||
@@ -0,0 +1,33 @@
|
||||
import { DetailedHTMLProps, forwardRef, HTMLAttributes, PropsWithChildren } from 'react';
|
||||
import { classNames } from './classNames';
|
||||
|
||||
const classes = {
|
||||
base: 'text-[white] font-bold leading-none text-[9.5px] absolute right-0 top-0 py-0.5 px-[3px] z-[1] rounded border',
|
||||
themes: {
|
||||
'primary': 'border-black bg-red-700'
|
||||
}
|
||||
};
|
||||
|
||||
export const NitroItemCountBadge = forwardRef<HTMLDivElement, PropsWithChildren<{
|
||||
theme?: 'primary';
|
||||
count: number;
|
||||
}> & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>>((props, ref) =>
|
||||
{
|
||||
const { theme = 'primary', count = 0, className = null, children = null, ...rest } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ ref }
|
||||
className={ classNames(
|
||||
classes.base,
|
||||
classes.themes[theme],
|
||||
className
|
||||
) }
|
||||
{ ...rest }>
|
||||
{ count }
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
NitroItemCountBadge.displayName = 'NitroItemCountBadge';
|
||||
@@ -0,0 +1 @@
|
||||
export const classNames = (...classes: string[]) => classes.filter(Boolean).join(' ');
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './InfiniteGrid';
|
||||
export * from './NitroButton';
|
||||
export * from './NitroCard';
|
||||
export * from './NitroInput';
|
||||
export * from './NitroItemCountBadge';
|
||||
export * from './classNames';
|
||||
export * from './limited-edition';
|
||||
export * from './styleNames';
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
export const NitroLimitedEditionStyledNumberView: FC<{
|
||||
value: number;
|
||||
}> = props =>
|
||||
{
|
||||
const { value = 0 } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ value.toString().split('').map((number, index) =>
|
||||
<i
|
||||
key={ index }
|
||||
className={ 'limited-edition-number n-' + number } />
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './NitroLimitedEditionStyledNumberView';
|
||||
@@ -0,0 +1,8 @@
|
||||
export const styleNames = (...styles: object[]) =>
|
||||
{
|
||||
let mergedStyle = {};
|
||||
|
||||
styles.filter(Boolean).forEach(style => mergedStyle = { ...mergedStyle, ...style });
|
||||
|
||||
return mergedStyle;
|
||||
};
|
||||
Reference in New Issue
Block a user