From 349a17a71f846b6e3c1f060880605225cee46737 Mon Sep 17 00:00:00 2001 From: indaviande Date: Wed, 11 Dec 2024 12:50:18 +0100 Subject: [PATCH 1/3] working on table row + new hero image --- src/components/extenders/Modal.tsx | 2 +- src/components/primitives/Box.tsx | 27 ++++++- src/components/primitives/Icon.tsx | 37 ++++++--- src/components/primitives/Table.tsx | 91 +++++++++++++++++----- src/components/primitives/Tabs.tsx | 33 ++++++++ src/components/primitives/Text.tsx | 116 ++++++++++++++++------------ src/components/primitives/Title.tsx | 52 +++++++++++-- src/utils/css.ts | 30 ++++++- 8 files changed, 297 insertions(+), 91 deletions(-) diff --git a/src/components/extenders/Modal.tsx b/src/components/extenders/Modal.tsx index 2b44e36c6..e591d53f0 100644 --- a/src/components/extenders/Modal.tsx +++ b/src/components/extenders/Modal.tsx @@ -28,7 +28,7 @@ export default function Modal({ state, title, description, modal, children, clas - + {title && ( {typeof title === "string" ? {title} : title} diff --git a/src/components/primitives/Box.tsx b/src/components/primitives/Box.tsx index f1361fc7f..bbea75a65 100644 --- a/src/components/primitives/Box.tsx +++ b/src/components/primitives/Box.tsx @@ -1,3 +1,27 @@ +/** + * Box Component + * + * A flexible container component that serves as a fundamental building block for layouts. + * It provides various visual styles and sizing options through variants. + * + * @variants + * content: + * - xs to xl: Content size variants + * + * container: + * - true/false: Controls container behavior + * + * @compoundVariants + * The component uses compound variants to determine border radius (rounded corners): + * - When container=true: Uses both size and content values (rounded-${size}+${content}) + * - When container=false: Uses only size value (rounded-${size}) + * + * @example + * + * Content goes here + * + */ + import { tv } from "tailwind-variants"; import useThemedVariables from "../../hooks/theming/useThemedVariables"; import { mergeClass } from "../../utils/css"; @@ -57,7 +81,6 @@ export const boxStyles = tv( ]), ), }, - { twMerge: false }, ); export type BoxProps = Component & Themable>; @@ -78,7 +101,7 @@ export default function Box({ return (
); diff --git a/src/components/primitives/Icon.tsx b/src/components/primitives/Icon.tsx index fe28ef0e4..33d65f6bb 100644 --- a/src/components/primitives/Icon.tsx +++ b/src/components/primitives/Icon.tsx @@ -2,11 +2,12 @@ import * as RemixIcon from "@remixicon/react"; import { type ReactElement, useMemo } from "react"; import { tv } from "tailwind-variants"; import { mergeClass } from "../.."; -import type { Component, Styled } from "../.."; +import type { Component, Styled, Themable } from "../.."; +import useThemableProps from "../../hooks/theming/useThemableProps"; import Image from "./Image"; export const iconStyles = tv({ - base: "flex flex-col border-0 overflow-hidden self-center rounded-sm w-[1em] h-[1em]", + base: "flex flex-col border-0 overflow-hidden self-center rounded-sm w-[1em] h-[1em]", variants: { size: { xs: "", @@ -26,20 +27,38 @@ export const iconStyles = tv({ }); export type IconProps = Component< - Styled & { - src?: string; - remix?: keyof typeof RemixIcon; - }, + Styled & + Themable & { + src?: string; + remix?: keyof typeof RemixIcon; + }, HTMLImageElement >; -export default function Icon({ rounded, remix, size, src, alt, className, ...props }: IconProps) { +export default function Icon({ + rounded, + remix, + size, + src, + alt, + className, + ...props +}: IconProps) { + const themeVars = useThemableProps(props); const styles = useMemo(() => iconStyles({ rounded, size }), [rounded, size]); const Component = useMemo(() => { if (remix) return RemixIcon[remix] as () => ReactElement; - return (imageProps: Component) => {alt}; + return (imageProps: Component) => ( + {alt} + ); }, [remix, alt, src]); - return ; + return ( + + ); } diff --git a/src/components/primitives/Table.tsx b/src/components/primitives/Table.tsx index a98e74021..9d6cb7646 100644 --- a/src/components/primitives/Table.tsx +++ b/src/components/primitives/Table.tsx @@ -1,4 +1,9 @@ -import { type PropsWithChildren, type ReactNode, useMemo, useState } from "react"; +import { + type PropsWithChildren, + type ReactNode, + useMemo, + useState, +} from "react"; import { useMediaQuery } from "react-responsive"; import { tv } from "tailwind-variants"; import { mergeClass } from "../../utils/css"; @@ -72,7 +77,12 @@ export type RowProps = Component< > >; -export function Row({ columns, exclude, children, ...props }: RowProps) { +export function Row({ + columns, + exclude, + children, + ...props +}: RowProps) { const isScreenSmall = useMediaQuery({ maxWidth: 640 }); const [ids, grid, compact] = useMemo(() => { const cols = Object.keys(columns ?? {}) as (keyof T)[]; @@ -84,7 +94,7 @@ export function Row({ columns, exclude, children, ...props }: display: "grid", rowGap: "0px", gridTemplateColumns: cols - .map(id => { + .map((id) => { if (exclude?.includes(id)) return; return columns?.[id]?.size ?? "1fr"; }) @@ -93,8 +103,8 @@ export function Row({ columns, exclude, children, ...props }: const compactStyle: { display: "grid"; gridTemplateColumns: string } = { display: "grid", gridTemplateColumns: cols - .filter(id => !columns?.[id]?.main) - .map(id => columns?.[id]?.compactSize ?? "1fr") + .filter((id) => !columns?.[id]?.main) + .map((id) => columns?.[id]?.compactSize ?? "1fr") .join(" "), }; @@ -112,8 +122,10 @@ export function Row({ columns, exclude, children, ...props }: return ( - {ids?.map(id => { - const element = props[`${String(id)}Column` as keyof typeof props] as ReactNode; + {ids?.map((id) => { + const element = props[ + `${String(id)}Column` as keyof typeof props + ] as ReactNode; const { className, main } = columns[id]; if (exclude?.includes(id)) return; @@ -123,17 +135,22 @@ export function Row({ columns, exclude, children, ...props }: style={ main && isScreenSmall ? { - gridColumn: `span ${ids?.length - 1} / span ${ids?.length - 1}`, + gridColumn: `span ${ids?.length - 1} / span ${ + ids?.length - 1 + }`, } : {} } key={String(id)} - className={[className, "inline-flex items-center"].join(" ")}> + className={[className, "inline-flex items-center"].join(" ")} + > {element}
); })} - {children && {children}} + {children && ( + {children} + )}
); } @@ -161,7 +178,7 @@ export function useHeaders( onHeaderClick?: (id: keyof T) => void, sortBy?: keyof T, order?: Order, - props?: TableHeaders, + props?: TableHeaders ) { //TODO: assess if props needs to be updated for columns and how to memo all columns // biome-ignore lint/correctness/useExhaustiveDependencies: props in dependency would render the memo useless @@ -172,15 +189,26 @@ export function useHeaders( for (const id of ids) { const { name: title, className: _className } = columns[id]; const isSortable = sortable?.includes(id); - const handler = title && isSortable ? () => onHeaderClick?.(id) : undefined; + const handler = + title && isSortable ? () => onHeaderClick?.(id) : undefined; head[`${id}Column` as keyof TableColumns] = ( - + {props?.[`${id}Header` as keyof TableHeaders] ?? title} {sortable && id === sortBy && - (order === "desc" ? : )} + (order === "desc" ? ( + + ) : ( + + ))} ); @@ -209,7 +237,8 @@ export function Table({ const [sortBy, setSortBy] = useState(sortable?.[0]); function onHeaderClick(id: keyof T) { - const currentOrder = id !== sortBy ? "desc" : _order === "desc" ? "asc" : "desc"; + const currentOrder = + id !== sortBy ? "desc" : _order === "desc" ? "asc" : "desc"; setOrder(currentOrder); setSortBy(id); @@ -217,13 +246,27 @@ export function Table({ } // biome-ignore lint/suspicious/noExplicitAny: please forgive this any - const headers = useHeaders(columns, sortable, onHeaderClick, sort ?? sortBy, order ?? _order, props as any); + const headers = useHeaders( + columns, + sortable, + onHeaderClick, + sort ?? sortBy, + order ?? _order, + props as any + ); return ( - + {!!header ? {header} : undefined} {/* biome-ignore lint/suspicious/noExplicitAny: please forgive this one as well */} - {!hideLabels ? : undefined} + {!hideLabels ? ( + + ) : undefined} {children} {!!footer ? {footer} : undefined} @@ -233,11 +276,17 @@ export function Table({ export function createTable(columns: T) { const TemplateTable = (props: Omit, "columns"> & ListProps) => ( // biome-ignore lint/suspicious/noExplicitAny: no reasons for it to have type errors - +
); // biome-ignore lint/suspicious/noExplicitAny: no reasons for it to have type errors - const TemplateRow = (props: Omit, "columns">) => ; + const TemplateRow = (props: Omit, "columns">) => ( + + ); - return [TemplateTable, TemplateRow, Object.keys(columns)] as [typeof TemplateTable, typeof TemplateRow, (keyof T)[]]; + return [TemplateTable, TemplateRow, Object.keys(columns)] as [ + typeof TemplateTable, + typeof TemplateRow, + (keyof T)[] + ]; } diff --git a/src/components/primitives/Tabs.tsx b/src/components/primitives/Tabs.tsx index 6c10178d3..1a1fd80bd 100644 --- a/src/components/primitives/Tabs.tsx +++ b/src/components/primitives/Tabs.tsx @@ -1,3 +1,36 @@ +/** + * @component Tabs + * @description A flexible and customizable tabs component that supports different visual styles and navigation. + * + * @features + * - Multiple visual styles (soft, base, bold, tint, hype) + * - Responsive design with Tailwind CSS + * - Built-in navigation support using Remix's Link component + * - Customizable themes + * - Accessibility features including keyboard navigation + * + * @usage + * ```tsx + * + * ``` + * + * @props + * - tabs: Array of tab items with label, link, and key + * - look: Visual style variant (soft, base, bold, tint, hype) + * - size: Size variant + * - theme: Custom theme override + * - className: Additional CSS classes + * - disabled: Whether the tabs are disabled + * - to: Default navigation path + */ + import { Link, useLocation } from "@remix-run/react"; import type { ReactNode } from "react"; diff --git a/src/components/primitives/Text.tsx b/src/components/primitives/Text.tsx index 1134fa8b7..b82f7281f 100644 --- a/src/components/primitives/Text.tsx +++ b/src/components/primitives/Text.tsx @@ -2,59 +2,77 @@ import { tv } from "tailwind-variants"; import { mergeClass } from "../../utils/css"; import type { Component, Styled } from "../../utils/types"; -export const textStyles = tv( - { - base: "text-main-11 font-text text-[clamp(15px,0.4167vw+0.78125rem,20px)]", - variants: { - look: { - base: "text-main-11", - soft: "text-main-11", - bold: "text-main-12", - tint: "text-accent-12", - hype: "text-accent-11", - }, - size: { - xs: "text-xs", - sm: "text-sm", - md: "text-base", - lg: "text-lg", - xl: "text-xl", - display1: "font-title font-bold leading-tight italic uppercase !text-[clamp(44px,5vw+0.875rem,104px)]", - 1: "font-title !text-3xl", - 2: "font-title font-bold leading-none italic !text-[clamp(38px,0.667vw+2.125rem,46px)]", - 3: "font-title font-bold leading-none italic !text-[clamp(26px,0.667vw+1.375rem,34px)]", - 4: "font-title font-bold leading-[1.18] !text-[clamp(18px,0.667vw+0.875rem,26px)]", - 5: "font-title font-bold leading-normal !text-[clamp(15px,0.25vw+0.84375rem,18px)] uppercase tracking-[1.6px] ", - 6: "font-title !text-sm", - }, - interactable: { - true: "cursor-pointer select-none", - false: "", - }, +export const textStyles = tv({ + base: "font-text text-[clamp(15px,0.4167vw+0.78125rem,20px)]", + variants: { + look: { + base: "text-main-11", + soft: "text-main-11", + bold: "text-main-12", + tint: "text-accent-12", + hype: "text-accent-11", }, - defaultVariants: { - size: "md", - look: "base", - interactable: false, + size: { + xs: "text-xs", + sm: "text-sm", + md: "text-base", + lg: "text-lg", + xl: "text-xl", + display1: + "font-title font-bold leading-tight italic uppercase !text-[clamp(44px,5vw+0.875rem,104px)]", + 1: "font-title !text-3xl", + 2: "font-title font-bold leading-none italic !text-[clamp(38px,0.667vw+2.125rem,46px)]", + 3: "font-title font-bold leading-none italic !text-[clamp(26px,0.667vw+1.375rem,34px)]", + 4: "font-title font-bold leading-[1.18] !text-[clamp(18px,0.667vw+0.875rem,26px)]", + 5: "font-title font-bold leading-none !text-[clamp(15px,0.25vw+0.84375rem,18px)] uppercase tracking-[0.8px] ", + 6: "font-title !text-sm", + }, + interactable: { + true: "cursor-pointer select-none", + false: "", }, - compoundVariants: [ - { look: "soft", interactable: true, class: "hover:text-main-12" }, - { - look: "base", - interactable: true, - class: "hover:text-main-12 active:text-main-11", - }, - { look: "bold", interactable: true, class: "hover:text-main-12" }, - { look: "tint", interactable: true, class: "hover:text-main-12" }, - { look: "hype", interactable: true, class: "hover:text-main-12" }, - ], }, - { twMerge: false }, -); + defaultVariants: { + size: "md", + look: "base", + interactable: false, + }, + compoundVariants: [ + { look: "soft", interactable: true, class: "hover:text-main-12" }, + { + look: "base", + interactable: true, + class: "hover:text-main-12 active:text-main-11", + }, + { look: "bold", interactable: true, class: "hover:text-main-12" }, + { look: "tint", interactable: true, class: "hover:text-main-12" }, + { look: "hype", interactable: true, class: "hover:text-main-12" }, + ], +}); -export type TextProps = Component & { bold?: boolean }, HTMLParagraphElement>; +export type TextProps = Component< + Styled & { bold?: boolean }, + HTMLParagraphElement +>; -export default function Text({ look, size, style, bold, interactable, className, ...props }: TextProps) { +export default function Text({ + look, + size, + style, + bold, + interactable, + className, + ...props +}: TextProps) { const styleBold = bold ? "font-bold" : ""; - return

; + return ( +

+ ); } diff --git a/src/components/primitives/Title.tsx b/src/components/primitives/Title.tsx index 2880c70d1..ebd6a792b 100644 --- a/src/components/primitives/Title.tsx +++ b/src/components/primitives/Title.tsx @@ -6,7 +6,7 @@ import { textStyles } from "./Text"; export const titleStyles = tv( { extend: textStyles, - base: "text-main-12 font-title font-bold", + base: "font-title font-bold", variants: { look: { base: "text-main-12", @@ -20,7 +20,7 @@ export const titleStyles = tv( look: "base", }, }, - { twMerge: false }, + { twMerge: false } ); export type TitleProps = Component< @@ -31,19 +31,55 @@ export type TitleProps = Component< HTMLHeadingElement >; -export default function Title({ look, h, size: _size, className, ...props }: TitleProps) { +export default function Title({ + look, + h, + size: _size, + className, + ...props +}: TitleProps) { const size = _size ?? h; switch (h) { case 1: - return

; + return ( +

+ ); case 2: - return

; + return ( +

+ ); case 3: - return

; + return ( +

+ ); case 4: - return

; + return ( +

+ ); case 5: - return

; + return ( +
+ ); default: break; } diff --git a/src/utils/css.ts b/src/utils/css.ts index 396ab454d..6b4ed489a 100644 --- a/src/utils/css.ts +++ b/src/utils/css.ts @@ -1,5 +1,33 @@ import clsx, { type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; +import { extendTailwindMerge } from "tailwind-merge"; + +const twMerge = extendTailwindMerge({ + extend: { + classGroups: { + p: ['p-sm/2', 'p-sm/4', 'p-sm*2', 'p-sm*4', + 'p-md/2', 'p-md/4', 'p-md*2', 'p-md*4', + 'p-lg/2', 'p-lg/4', 'p-lg*2', 'p-lg*4'], + px: ['px-sm/2', 'px-sm/4', 'px-sm*2', 'px-sm*4', + 'px-md/2', 'px-md/4', 'px-md*2', 'px-md*4', + 'px-lg/2', 'px-lg/4', 'px-lg*2', 'px-lg*4'], + py: ['py-sm/2', 'py-sm/4', 'py-sm*2', 'py-sm*4', + 'py-md/2', 'py-md/4', 'py-md*2', 'py-md*4', + 'py-lg/2', 'py-lg/4', 'py-lg*2', 'py-lg*4'], + m: ['m-sm/2', 'm-sm/4', 'm-sm*2', 'm-sm*4', + 'm-md/2', 'm-md/4', 'm-md*2', 'm-md*4', + 'm-lg/2', 'm-lg/4', 'm-lg*2', 'm-lg*4'], + mx: ['mx-sm/2', 'mx-sm/4', 'mx-sm*2', 'mx-sm*4', + 'mx-md/2', 'mx-md/4', 'mx-md*2', 'mx-md*4', + 'mx-lg/2', 'mx-lg/4', 'mx-lg*2', 'mx-lg*4'], + my: ['my-sm/2', 'my-sm/4', 'my-sm*2', 'my-sm*4', + 'my-md/2', 'my-md/4', 'my-md*2', 'my-md*4', + 'my-lg/2', 'my-lg/4', 'my-lg*2', 'my-lg*4'], + gap: ['gap-sm/2', 'gap-sm/4', 'gap-sm*2', 'gap-sm*4', + 'gap-md/2', 'gap-md/4', 'gap-md*2', 'gap-md*4', + 'gap-lg/2', 'gap-lg/4', 'gap-lg*2', 'gap-lg*4'], + }, + }, +}); /** * Merges classes together with the most compatibility possible From aaf64c4f485e2d37c71d685e45d05e1074d22914 Mon Sep 17 00:00:00 2001 From: indaviande Date: Wed, 11 Dec 2024 13:10:37 +0100 Subject: [PATCH 2/3] add fitlers --- src/components/extenders/Select.tsx | 101 ++++++++++++++++++---------- src/components/primitives/Input.tsx | 68 +++++++++++++------ 2 files changed, 115 insertions(+), 54 deletions(-) diff --git a/src/components/extenders/Select.tsx b/src/components/extenders/Select.tsx index 8c0b0fdd3..8eecc5c73 100644 --- a/src/components/extenders/Select.tsx +++ b/src/components/extenders/Select.tsx @@ -17,7 +17,8 @@ export const selectStyles = tv({ "text-main-11 rounded-sm flex items-center dim focus-visible:outline-main-12 !leading-none justify-between text-nowrap font-text font-semibold", ], slots: { - dropdown: "outline-0 z-50 origin-top animate-drop animate-stretch mt-sm min-w-[var(--popover-anchor-width)]", + dropdown: + "outline-0 z-50 origin-top animate-drop animate-stretch mt-sm min-w-[var(--popover-anchor-width)]", item: "rounded-sm flex justify-between items-center gap-lg cursor-pointer select-none p-sm outline-offset-0 outline-0 text-nowrap focus-visible:outline-main-12", icon: "flex items-center", value: "flex gap-sm items-center", @@ -137,12 +138,14 @@ export type SelectProps = Component<{ }> & RadixSelect.SelectProps; -type MaybeArray = IsArray extends true ? T[] : T; +type MaybeArray = IsArray extends true + ? T[] + : T; export default function Select< T extends string | number, Multiple extends undefined | boolean, - Value extends MaybeArray, + Value extends MaybeArray >({ look, size, @@ -174,35 +177,45 @@ export default function Select< }); const value = useMemo(() => getter ?? internal, [getter, internal]); - const setValue = useCallback((v: Value) => setter?.(v) ?? setInternal(v), [setter]); + const setValue = useCallback( + (v: Value) => setter?.(v) ?? setInternal(v), + [setter] + ); const [searchInput, setSearch] = useState(); const matches = useMemo(() => { if (!search) return Object.keys(options ?? {}); // const textToMatch = Object.keys(options ?? {}).map(option => `${option}_${options[option]?.props?.children?.filter(a => typeof a !== "object").join(" ")}`) - const textToMatch = Object.keys(options ?? {}).reduce( - (matches, option) => { - const opt = options?.[option]; - const key = - typeof opt === "string" - ? opt - : ( - options?.[option] as Exclude> - )?.props?.children - ?.filter?.((a: unknown) => typeof a !== "object") - ?.join(" "); + const textToMatch = Object.keys(options ?? {}).reduce((matches, option) => { + const opt = options?.[option]; + const key = + typeof opt === "string" + ? opt + : ( + options?.[option] as Exclude< + ReactNode, + string | number | boolean | Iterable + > + )?.props?.children + ?.filter?.((a: unknown) => typeof a !== "object") + ?.join(" "); - return Object.assign(matches, { [`${option}`]: option }, { [`${key}`]: option }); - }, - {} as { [key: string]: keyof typeof options }, - ); - const searchMatches = matchSorter(Object.keys(textToMatch), searchInput ?? "").map(key => textToMatch[key]); + return Object.assign( + matches, + { [`${option}`]: option }, + { [`${key}`]: option } + ); + }, {} as { [key: string]: keyof typeof options }); + const searchMatches = matchSorter( + Object.keys(textToMatch), + searchInput ?? "" + ).map((key) => textToMatch[key]); const uniqueOptionMatches = Array.from( searchMatches.reduce((set, option) => { set.add(option); return set; - }, new Set()), + }, new Set()) ) as (typeof value)[]; return uniqueOptionMatches; @@ -211,7 +224,9 @@ export default function Select< const label = useMemo(() => { if ( value && - (typeof value === "number" || typeof value === "string" || typeof value === "symbol") && + (typeof value === "number" || + typeof value === "string" || + typeof value === "symbol") && options?.[value] ) return options?.[value]; @@ -221,8 +236,9 @@ export default function Select< + "w-[1.2em] h-[1.2em] flex items-center justify-center rounded-full bg-main-6 text-main-12" + )} + > {value.length} {" "} {placeholder} @@ -234,17 +250,23 @@ export default function Select< return ( { + setValue={(value) => { setSearch(value); - }}> + }} + > setValue(v as Value)} + setValue={(v) => setValue(v as Value)} value={value as string} - defaultValue={multiple ? [] : undefined}> + defaultValue={multiple ? [] : undefined} + >
{label}
- {loading ? : } + {loading ? ( + + ) : ( + + )}
@@ -254,7 +276,11 @@ export default function Select< )} @@ -277,7 +303,11 @@ export default function Select< key="select" className={mergeClass( check(), - !((typeof value === "object" && value?.length > 0) || value === undefined) && "opacity-0", + !( + (typeof value === "object" && + value?.length > 0) || + value === undefined + ) && "opacity-0" )} size="sm" remix="RiCheckFill" @@ -287,7 +317,7 @@ export default function Select< } /> )} - {matches?.map(_value => ( + {matches?.map((_value) => ( = Component< @@ -44,9 +51,16 @@ export type InputProps = Component< function Input({ look, size, state, className, ...props }: InputProps) { const { header, footer, prefix, suffix, label, hint, ...rest } = props; - if (extensions.some(extension => !!props?.[extension])) + if (extensions.some((extension) => !!props?.[extension])) return ( -