🆙 Init V3

This commit is contained in:
DuckieTM
2026-01-31 09:10:52 +01:00
commit 7feb10ab15
1733 changed files with 53405 additions and 0 deletions
+185
View File
@@ -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
});
+44
View File
@@ -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';
+124
View File
@@ -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
});
+32
View File
@@ -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';
+33
View File
@@ -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';
+1
View File
@@ -0,0 +1 @@
export const classNames = (...classes: string[]) => classes.filter(Boolean).join(' ');
+8
View File
@@ -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 } />
) }
</>
);
};
+1
View File
@@ -0,0 +1 @@
export * from './NitroLimitedEditionStyledNumberView';
+8
View File
@@ -0,0 +1,8 @@
export const styleNames = (...styles: object[]) =>
{
let mergedStyle = {};
styles.filter(Boolean).forEach(style => mergedStyle = { ...mergedStyle, ...style });
return mergedStyle;
};