From 933dbe766e55a77dc36c2639d05df31ffe78f4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Sat, 8 Aug 2026 06:49:01 -0300 Subject: [PATCH 1/7] chore(UI): update components.json to reorganize menu properties and registries --- components.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components.json b/components.json index 6a7e364..bc5dfb5 100644 --- a/components.json +++ b/components.json @@ -12,6 +12,8 @@ }, "iconLibrary": "lucide", "rtl": true, + "menuColor": "default", + "menuAccent": "bold", "aliases": { "components": "~/components", "utils": "~/lib/utils", @@ -19,7 +21,7 @@ "lib": "~/lib", "hooks": "~/hooks" }, - "menuColor": "default", - "menuAccent": "bold", - "registries": {} + "registries": { + "@reui": "https://reui.io/r/{style}/{name}.json" + } } From 89761ffd93af82b62c93151dbfa2a102be82143e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Sat, 8 Aug 2026 06:49:08 -0300 Subject: [PATCH 2/7] feat(UI): add NumberField component with increment/decrement functionality --- app/components/reui/number-field.tsx | 230 +++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 app/components/reui/number-field.tsx diff --git a/app/components/reui/number-field.tsx b/app/components/reui/number-field.tsx new file mode 100644 index 0000000..d8ea0cd --- /dev/null +++ b/app/components/reui/number-field.tsx @@ -0,0 +1,230 @@ +import { NumberField as NumberFieldPrimitive } from '@base-ui/react/number-field'; +import type { VariantProps } from 'class-variance-authority'; +import { cva } from 'class-variance-authority'; +import type { ReactNode } from 'react'; +import { createContext, useContext, useId, useMemo } from 'react'; + +import { MinusIcon, PlusIcon } from 'lucide-react'; +import { Label } from '~/components/ui/label'; +import { cn } from '~/lib/utils'; + +const NumberFieldContext = createContext<{ + fieldId: string; + size: 'sm' | 'default' | 'lg'; +} | null>(null); + +const numberFieldGroupVariants = cva( + 'relative flex w-full justify-between rounded-md border border-input bg-transparent transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 focus-within:has-aria-invalid:border-destructive focus-within:has-aria-invalid:ring-destructive/20 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:focus-within:has-aria-invalid:ring-destructive/40 dark:aria-invalid:ring-destructive/40 data-disabled:pointer-events-none data-disabled:opacity-50', + { + variants: { + size: { + sm: 'h-7 text-sm', + default: 'h-8 text-sm', + lg: 'h-9 text-sm', + }, + }, + defaultVariants: { + size: 'default', + }, + }, +); + +const numberFieldButtonVariants = cva( + 'relative flex shrink-0 cursor-pointer items-center justify-center transition-colors hover:bg-accent pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11', + { + variants: { + size: { + sm: "([class*='size-'])]:size-3.5 ([class*='size-'])]:size-3.5 ([class*='size-'])]:size-3.5 ([class*='size-'])]:size-3 ([class*='size-'])]:size-3.5 ([class*='size-'])]:size-3.5 px-1.5 [&_svg:not([class*='size-'])]:size-3.5", + default: + "([class*='size-'])]:size-4 ([class*='size-'])]:size-4 ([class*='size-'])]:size-4 ([class*='size-'])]:size-3.5 ([class*='size-'])]:size-4 ([class*='size-'])]:size-3.5 px-2 [&_svg:not([class*='size-'])]:size-4", + lg: "([class*='size-'])]:size-4 ([class*='size-'])]:size-4 ([class*='size-'])]:size-4 ([class*='size-'])]:size-3.5 ([class*='size-'])]:size-4 ([class*='size-'])]:size-3.5 px-2.5 [&_svg:not([class*='size-'])]:size-4", + }, + }, + defaultVariants: { + size: 'default', + }, + }, +); + +const numberFieldInputVariants = cva('w-full min-w-0 flex-1 bg-transparent text-center tabular-nums outline-none', { + variants: { + size: { + sm: 'px-2 py-0.5', + default: 'px-2.5 py-1', + lg: 'px-2.5 py-1.5', + }, + }, + defaultVariants: { + size: 'default', + }, +}); + +function NumberField({ + id, + className, + size = 'default', + ...props +}: NumberFieldPrimitive.Root.Props & VariantProps) { + const generatedId = useId(); + const fieldId = id ?? generatedId; + const sizeValue = size ?? 'default'; + + const fieldContextValues = useMemo(() => ({ fieldId, size: sizeValue }), [fieldId, sizeValue]); + + return ( + + + + ); +} + +function NumberFieldGroup({ + className, + size: sizeProp, + ...props +}: NumberFieldPrimitive.Group.Props & Partial>) { + const context = useContext(NumberFieldContext); + if (!context) { + throw new Error('NumberFieldGroup must be used within a NumberField component.'); + } + const size = sizeProp ?? context.size; + + return ( + + ); +} + +function NumberFieldDecrement({ + className, + size: sizeProp, + children, + ...props +}: NumberFieldPrimitive.Decrement.Props & + Partial> & { + children?: React.ReactNode; + }) { + const context = useContext(NumberFieldContext); + if (!context) { + throw new Error('NumberFieldDecrement must be used within a NumberField component.'); + } + const size = sizeProp ?? context.size; + + return ( + + {children ?? } + + ); +} + +function NumberFieldIncrement({ + className, + size: sizeProp, + children, + ...props +}: NumberFieldPrimitive.Increment.Props & + Partial> & { + children?: ReactNode; + }) { + const context = useContext(NumberFieldContext); + if (!context) { + throw new Error('NumberFieldIncrement must be used within a NumberField component.'); + } + const size = sizeProp ?? context.size; + + return ( + + {children ?? } + + ); +} + +function NumberFieldInput({ + className, + size: sizeProp, + ...props +}: NumberFieldPrimitive.Input.Props & Partial>) { + const context = useContext(NumberFieldContext); + if (!context) { + throw new Error('NumberFieldInput must be used within a NumberField component.'); + } + const size = sizeProp ?? context.size; + + return ( + + ); +} + +function NumberFieldScrubArea({ + className, + label, + ...props +}: NumberFieldPrimitive.ScrubArea.Props & { + label: string; +}) { + const context = useContext(NumberFieldContext); + if (!context) { + throw new Error('NumberFieldScrubArea must be used within a NumberField component for accessibility.'); + } + + return ( + + + + + + + ); +} + +function CursorGrowIcon(props: Readonly>) { + return ( + + + + ); +} + +export { + NumberField, + NumberFieldDecrement, + NumberFieldGroup, + NumberFieldIncrement, + NumberFieldInput, + NumberFieldScrubArea, +}; From ccf55fd10065096154c2062b6ac4d265a31a52bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Sat, 8 Aug 2026 06:55:25 -0300 Subject: [PATCH 3/7] feat(dice-roller): integrate NumberField component for difficulty input in VtmDiceRoller --- .../utilities/dice-roller/vtm-dice-roller.tsx | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx index 091a271..91f6a16 100644 --- a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx +++ b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx @@ -1,10 +1,27 @@ +import { Checkbox } from '@base-ui/react/checkbox'; import { zodResolver } from '@hookform/resolvers/zod'; +import { XIcon } from 'lucide-react'; import { floor } from 'mathjs'; import { type BaseSyntheticEvent, Fragment, useCallback, useState, useTransition } from 'react'; import { Controller, type Resolver, useForm } from 'react-hook-form'; import { Link } from 'react-router'; import { z } from 'zod/v4'; import VtmAlert from '~/components/pages/utilities/dice-roller/vtm-alert'; +import { + NumberField, + NumberFieldDecrement, + NumberFieldGroup, + NumberFieldIncrement, + NumberFieldInput, +} from '~/components/reui/number-field'; +import { + ActionBar, + ActionBarClose, + ActionBarGroup, + ActionBarItem, + ActionBarSelection, + ActionBarSeparator, +} from '~/components/ui/action-bar'; import { Button } from '~/components/ui/button'; import { ButtonGroup } from '~/components/ui/button-group'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '~/components/ui/card'; @@ -12,10 +29,9 @@ import { Field, FieldError, FieldGroup, FieldLabel } from '~/components/ui/field import { Input } from '~/components/ui/input'; import useIsMobile from '~/hooks/use-mobile'; import { rollDie } from '~/lib/dice-roller.utils'; +import { cn } from '~/lib/utils'; // Dice import -import { Checkbox } from '@base-ui/react/checkbox'; -import { XIcon } from 'lucide-react'; import hungerDieBestialFailure from '~/assets/dice/vtm/Dice_Hunger_BestialFailure.png'; import hungerDieFailure from '~/assets/dice/vtm/Dice_Hunger_Failure.png'; import hungerDieMessyCritical from '~/assets/dice/vtm/Dice_Hunger_MessyCritical.png'; @@ -23,15 +39,6 @@ import hungerDieSuccess from '~/assets/dice/vtm/Dice_Hunger_Success.png'; import regularDieCritical from '~/assets/dice/vtm/Dice_Regular_Critical.png'; import regularDieFailure from '~/assets/dice/vtm/Dice_Regular_Failure.png'; import regularDieSuccess from '~/assets/dice/vtm/Dice_Regular_Success.png'; -import { - ActionBar, - ActionBarClose, - ActionBarGroup, - ActionBarItem, - ActionBarSelection, - ActionBarSeparator, -} from '~/components/ui/action-bar'; -import { cn } from '~/lib/utils'; type DieResult = { id: string; @@ -382,18 +389,20 @@ export default function VtmDiceRoller() { render={({ field, fieldState }) => ( Difficulty - + value={field.value} + onValueChange={(value) => field.onChange(value)} + > + + + + + + {fieldState.invalid && ( Date: Sat, 8 Aug 2026 06:57:13 -0300 Subject: [PATCH 4/7] feat(dice-roller): replace Hunger Dice input with NumberField component for improved UX --- .../utilities/dice-roller/vtm-dice-roller.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx index 91f6a16..39b807e 100644 --- a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx +++ b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx @@ -362,17 +362,21 @@ export default function VtmDiceRoller() { render={({ field, fieldState }) => ( Hunger Dice - + value={field.value} + onValueChange={(value) => field.onChange(value)} + > + + + + + + {fieldState.invalid && ( Difficulty Date: Sat, 8 Aug 2026 06:58:23 -0300 Subject: [PATCH 5/7] feat(dice-roller): replace Regular Dice input with NumberField component for enhanced user interaction --- .../utilities/dice-roller/vtm-dice-roller.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx index 39b807e..2b2cd16 100644 --- a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx +++ b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx @@ -26,7 +26,6 @@ import { Button } from '~/components/ui/button'; import { ButtonGroup } from '~/components/ui/button-group'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '~/components/ui/card'; import { Field, FieldError, FieldGroup, FieldLabel } from '~/components/ui/field'; -import { Input } from '~/components/ui/input'; import useIsMobile from '~/hooks/use-mobile'; import { rollDie } from '~/lib/dice-roller.utils'; import { cn } from '~/lib/utils'; @@ -335,17 +334,21 @@ export default function VtmDiceRoller() { render={({ field, fieldState }) => ( Regular Dice - + value={field.value} + onValueChange={(value) => field.onChange(value)} + > + + + + + + {fieldState.invalid && ( Date: Sat, 8 Aug 2026 07:27:48 -0300 Subject: [PATCH 6/7] feat(UI): add labelClassName prop to NumberFieldScrubArea for customizable label styling --- app/components/reui/number-field.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/components/reui/number-field.tsx b/app/components/reui/number-field.tsx index d8ea0cd..9a676c9 100644 --- a/app/components/reui/number-field.tsx +++ b/app/components/reui/number-field.tsx @@ -179,9 +179,11 @@ function NumberFieldInput({ function NumberFieldScrubArea({ className, label, + labelClassName, ...props }: NumberFieldPrimitive.ScrubArea.Props & { label: string; + labelClassName?: string; }) { const context = useContext(NumberFieldContext); if (!context) { @@ -194,7 +196,7 @@ function NumberFieldScrubArea({ data-slot="number-field-scrub-area" {...props} > -