@tinybigui/react 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -2624,25 +2624,25 @@ interface RadioHeadlessProps extends AriaRadioProps {
2624
2624
  * Material Design 3 Radio Component (Layer 3: Styled)
2625
2625
  *
2626
2626
  * Built on React Aria for world-class accessibility.
2627
- * Uses CVA for type-safe variant management.
2628
- * Styled with Tailwind CSS using MD3 design tokens.
2629
- * Must be used within a RadioGroup for proper functionality.
2627
+ * Implements the Variants-vs-States architecture: all interaction/selection/error
2628
+ * states are expressed as data-* attributes on the root and consumed by each
2629
+ * slot via group-data-[x]/radio Tailwind selectors no state variants in CVA.
2630
2630
  *
2631
2631
  * Features:
2632
- * - ✅ 2 states: unselected, selected
2632
+ * - ✅ Unselected / selected states
2633
+ * - ✅ Error/invalid state (via RadioGroup isInvalid)
2633
2634
  * - ✅ Ripple effect (Material Design)
2634
2635
  * - ✅ Full keyboard accessibility (via React Aria)
2635
2636
  * - ✅ Screen reader support (via React Aria)
2636
- * - ✅ Focus management (via React Aria)
2637
+ * - ✅ Focus management with MD3 focus ring (no animate-pulse)
2637
2638
  * - ✅ Form integration (name, value props from RadioGroup)
2638
2639
  *
2639
2640
  * MD3 Specifications:
2640
- * - Radio icon: 20x20dp (within 40x40dp touch target)
2641
- * - Outer circle: 20px
2642
- * - Inner dot: 10px (selected state)
2643
- * - Outline width: 2dp
2644
- * - State layers: 8% hover, 12% focus/pressed
2645
- * - Disabled: 38% opacity
2641
+ * - Radio icon: 20×20dp (within 40×40dp touch target)
2642
+ * - Outer circle border: 2dp
2643
+ * - Inner dot: 10dp (selected state, scale 0→1)
2644
+ * - State layers: 8% hover, 10% focus/pressed
2645
+ * - Disabled: 38% opacity (on root)
2646
2646
  * - Label spacing: 16px (ml-4)
2647
2647
  *
2648
2648
  * @example
@@ -2663,11 +2663,6 @@ interface RadioHeadlessProps extends AriaRadioProps {
2663
2663
  * <Radio value="a">Enabled</Radio>
2664
2664
  * <Radio value="b" isDisabled>Disabled</Radio>
2665
2665
  * </RadioGroup>
2666
- *
2667
- * // Custom styling
2668
- * <Radio value="custom" className="my-custom-class">
2669
- * Custom
2670
- * </Radio>
2671
2666
  * ```
2672
2667
  */
2673
2668
  declare const Radio: React__default.ForwardRefExoticComponent<RadioProps & React__default.RefAttributes<HTMLInputElement>>;
package/dist/index.d.ts CHANGED
@@ -2624,25 +2624,25 @@ interface RadioHeadlessProps extends AriaRadioProps {
2624
2624
  * Material Design 3 Radio Component (Layer 3: Styled)
2625
2625
  *
2626
2626
  * Built on React Aria for world-class accessibility.
2627
- * Uses CVA for type-safe variant management.
2628
- * Styled with Tailwind CSS using MD3 design tokens.
2629
- * Must be used within a RadioGroup for proper functionality.
2627
+ * Implements the Variants-vs-States architecture: all interaction/selection/error
2628
+ * states are expressed as data-* attributes on the root and consumed by each
2629
+ * slot via group-data-[x]/radio Tailwind selectors no state variants in CVA.
2630
2630
  *
2631
2631
  * Features:
2632
- * - ✅ 2 states: unselected, selected
2632
+ * - ✅ Unselected / selected states
2633
+ * - ✅ Error/invalid state (via RadioGroup isInvalid)
2633
2634
  * - ✅ Ripple effect (Material Design)
2634
2635
  * - ✅ Full keyboard accessibility (via React Aria)
2635
2636
  * - ✅ Screen reader support (via React Aria)
2636
- * - ✅ Focus management (via React Aria)
2637
+ * - ✅ Focus management with MD3 focus ring (no animate-pulse)
2637
2638
  * - ✅ Form integration (name, value props from RadioGroup)
2638
2639
  *
2639
2640
  * MD3 Specifications:
2640
- * - Radio icon: 20x20dp (within 40x40dp touch target)
2641
- * - Outer circle: 20px
2642
- * - Inner dot: 10px (selected state)
2643
- * - Outline width: 2dp
2644
- * - State layers: 8% hover, 12% focus/pressed
2645
- * - Disabled: 38% opacity
2641
+ * - Radio icon: 20×20dp (within 40×40dp touch target)
2642
+ * - Outer circle border: 2dp
2643
+ * - Inner dot: 10dp (selected state, scale 0→1)
2644
+ * - State layers: 8% hover, 10% focus/pressed
2645
+ * - Disabled: 38% opacity (on root)
2646
2646
  * - Label spacing: 16px (ml-4)
2647
2647
  *
2648
2648
  * @example
@@ -2663,11 +2663,6 @@ interface RadioHeadlessProps extends AriaRadioProps {
2663
2663
  * <Radio value="a">Enabled</Radio>
2664
2664
  * <Radio value="b" isDisabled>Disabled</Radio>
2665
2665
  * </RadioGroup>
2666
- *
2667
- * // Custom styling
2668
- * <Radio value="custom" className="my-custom-class">
2669
- * Custom
2670
- * </Radio>
2671
2666
  * ```
2672
2667
  */
2673
2668
  declare const Radio: React__default.ForwardRefExoticComponent<RadioProps & React__default.RefAttributes<HTMLInputElement>>;
package/dist/index.js CHANGED
@@ -3139,224 +3139,102 @@ var RadioGroupHeadless = forwardRef(
3139
3139
  }
3140
3140
  );
3141
3141
  RadioGroupHeadless.displayName = "RadioGroupHeadless";
3142
+ var radioRootVariants = cva([
3143
+ "relative inline-flex items-center cursor-pointer select-none",
3144
+ // Disabled state — self-targeting so children inherit via group
3145
+ "data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none",
3146
+ "data-[disabled]:opacity-38"
3147
+ ]);
3148
+ var radioControlVariants = cva([
3149
+ "relative flex items-center justify-center flex-shrink-0",
3150
+ "size-10"
3151
+ // 40dp touch target (MD3 spec)
3152
+ ]);
3153
+ var radioFocusRingVariants = cva([
3154
+ "pointer-events-none absolute inset-0 rounded-full",
3155
+ "outline outline-2 outline-offset-2 outline-secondary",
3156
+ // Effects transition for opacity
3157
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
3158
+ "opacity-0",
3159
+ "group-data-[focus-visible]/radio:opacity-100"
3160
+ ]);
3161
+ var radioTargetVariants = cva([
3162
+ "absolute inset-0 rounded-full overflow-hidden",
3163
+ "flex items-center justify-center"
3164
+ ]);
3165
+ var radioStateLayerVariants = cva([
3166
+ "absolute inset-0 rounded-full pointer-events-none opacity-0",
3167
+ // Base state-layer color (unselected)
3168
+ "bg-on-surface",
3169
+ // Effects transition for opacity + color
3170
+ "transition-[opacity,background-color] duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
3171
+ // Selected state-layer color
3172
+ "group-data-[selected]/radio:bg-primary",
3173
+ // Error state-layer color (overrides selected via cascade position)
3174
+ "group-data-[invalid]/radio:bg-error",
3175
+ // Interaction opacities (MD3: hover 8%, focus/pressed 10%)
3176
+ "group-data-[hovered]/radio:opacity-8",
3177
+ "group-data-[focus-visible]/radio:opacity-10",
3178
+ "group-data-[pressed]/radio:opacity-10",
3179
+ // No state layer when disabled
3180
+ "group-data-[disabled]/radio:hidden"
3181
+ ]);
3182
+ var radioRingVariants = cva([
3183
+ "relative z-10 size-5 rounded-full border-2 flex items-center justify-center flex-shrink-0",
3184
+ // Base (unselected, enabled)
3185
+ "border-on-surface-variant",
3186
+ // Effects transition for border-color
3187
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
3188
+ // Selected — border becomes primary
3189
+ "group-data-[selected]/radio:border-primary",
3190
+ // Error — placed after selected so it overrides by cascade order
3191
+ "group-data-[invalid]/radio:border-error",
3192
+ // Disabled — on-surface/38 opacity value
3193
+ "group-data-[disabled]/radio:border-on-surface/38"
3194
+ ]);
3195
+ var radioDotVariants = cva([
3196
+ "size-2.5 rounded-full origin-center",
3197
+ // Spatial transition for scale — springs may overshoot (intentional)
3198
+ "transition-transform duration-spring-standard-fast-spatial ease-spring-standard-fast-spatial",
3199
+ // Base fill color (shown when selected)
3200
+ "bg-primary",
3201
+ // Hidden when unselected
3202
+ "scale-0",
3203
+ // Visible when selected
3204
+ "group-data-[selected]/radio:scale-100",
3205
+ // Error — error fill color (placed after selected by cascade)
3206
+ "group-data-[invalid]/radio:bg-error",
3207
+ // Disabled — use on-surface/38 opacity
3208
+ "group-data-[disabled]/radio:bg-on-surface/38"
3209
+ ]);
3210
+ var radioLabelVariants = cva(["text-body-medium text-on-surface select-none ml-4"]);
3142
3211
  var radioGroupVariants = cva(
3143
- [
3144
- // Base classes (always applied to group wrapper)
3145
- "flex",
3146
- "gap-4"
3147
- // 16px spacing between radios (MD3 standard)
3148
- ],
3212
+ ["flex", "gap-1"],
3213
+ // 4dp gap padding around each 40dp control handles visual spacing
3149
3214
  {
3150
3215
  variants: {
3151
3216
  /**
3152
3217
  * Layout orientation
3218
+ * @default "vertical"
3153
3219
  */
3154
3220
  orientation: {
3155
3221
  vertical: "flex-col",
3156
3222
  horizontal: "flex-row flex-wrap"
3157
- },
3158
- /**
3159
- * Disabled state
3160
- */
3161
- disabled: {
3162
- true: "",
3163
- false: ""
3164
- }
3165
- },
3166
- defaultVariants: {
3167
- orientation: "vertical",
3168
- disabled: false
3169
- }
3170
- }
3171
- );
3172
- var radioGroupLabelVariants = cva(
3173
- [
3174
- "text-sm font-medium",
3175
- // MD3: Body Medium
3176
- "text-on-surface",
3177
- "mb-3"
3178
- // Spacing below label (12px)
3179
- ],
3180
- {
3181
- variants: {
3182
- disabled: {
3183
- true: "opacity-38",
3184
- false: ""
3185
- }
3186
- },
3187
- defaultVariants: {
3188
- disabled: false
3189
- }
3190
- }
3191
- );
3192
- var radioVariants = cva(
3193
- [
3194
- // Base classes (always applied to label wrapper)
3195
- "relative inline-flex items-center cursor-pointer select-none",
3196
- "transition-opacity duration-200"
3197
- ],
3198
- {
3199
- variants: {
3200
- /**
3201
- * Disabled state
3202
- */
3203
- disabled: {
3204
- true: "opacity-38 cursor-not-allowed pointer-events-none",
3205
- false: ""
3206
- }
3207
- },
3208
- defaultVariants: {
3209
- disabled: false
3210
- }
3211
- }
3212
- );
3213
- var radioContainerVariants = cva(
3214
- [
3215
- // Base classes for radio visual container
3216
- "relative inline-flex items-center justify-center",
3217
- "w-10 h-10",
3218
- // 40x40dp touch target (MD3 spec)
3219
- "flex-shrink-0",
3220
- "transition-all duration-200",
3221
- "m-1",
3222
- // 4px margin around radio for spacing (total 8px gap between radios)
3223
- // State layer (hover, focus, active) - MD3 spec: 8%/12%/12% opacity
3224
- "before:absolute before:inset-0 before:rounded-full before:transition-opacity before:duration-200",
3225
- "before:bg-current before:opacity-0",
3226
- "hover:before:opacity-8",
3227
- "active:before:opacity-12"
3228
- ],
3229
- {
3230
- variants: {
3231
- /**
3232
- * Radio state (determines visual appearance)
3233
- */
3234
- state: {
3235
- unselected: "text-on-surface-variant",
3236
- selected: "text-primary"
3237
- },
3238
- /**
3239
- * Error/invalid state
3240
- */
3241
- isInvalid: {
3242
- true: "text-error",
3243
- false: ""
3244
- },
3245
- /**
3246
- * Disabled state
3247
- */
3248
- disabled: {
3249
- true: "text-on-surface pointer-events-none",
3250
- false: ""
3251
- }
3252
- },
3253
- compoundVariants: [
3254
- // Error state overrides normal colors for all states
3255
- {
3256
- state: "unselected",
3257
- isInvalid: true,
3258
- disabled: false,
3259
- className: "text-error"
3260
- },
3261
- {
3262
- state: "selected",
3263
- isInvalid: true,
3264
- disabled: false,
3265
- className: "text-error"
3266
- }
3267
- ],
3268
- defaultVariants: {
3269
- state: "unselected",
3270
- isInvalid: false,
3271
- disabled: false
3272
- }
3273
- }
3274
- );
3275
- var radioIconOuterVariants = cva(
3276
- [
3277
- // Base classes for the radio outer circle
3278
- "transition-all duration-200 stroke-current stroke-2 fill-transparent"
3279
- ],
3280
- {
3281
- variants: {
3282
- /**
3283
- * Radio state
3284
- */
3285
- state: {
3286
- unselected: [],
3287
- selected: []
3288
- },
3289
- /**
3290
- * Disabled state
3291
- */
3292
- disabled: {
3293
- true: ["fill-transparent", "stroke-current", "stroke-2"],
3294
- false: ""
3295
- }
3296
- },
3297
- compoundVariants: [
3298
- // Disabled + selected state overrides fill
3299
- {
3300
- state: "selected",
3301
- disabled: true,
3302
- className: "fill-current stroke-none"
3303
- }
3304
- ],
3305
- defaultVariants: {
3306
- state: "unselected",
3307
- disabled: false
3308
- }
3309
- }
3310
- );
3311
- var radioIconInnerVariants = cva(["transition-all origin-center duration-200"], {
3312
- variants: {
3313
- /**
3314
- * Visibility based on state
3315
- */
3316
- state: {
3317
- selected: ["fill-current"],
3318
- unselected: ["fill-transparent"]
3319
- },
3320
- visible: {
3321
- true: "opacity-100 scale-100 fill-current",
3322
- false: "opacity-0 scale-0"
3323
- }
3324
- },
3325
- defaultVariants: {
3326
- visible: false
3327
- }
3328
- });
3329
- var radioLabelVariants = cva(
3330
- [
3331
- "text-sm",
3332
- // MD3: Body Medium (14px)
3333
- "text-on-surface",
3334
- "select-none"
3335
- ],
3336
- {
3337
- variants: {
3338
- disabled: {
3339
- true: "",
3340
- false: ""
3341
3223
  }
3342
3224
  },
3343
3225
  defaultVariants: {
3344
- disabled: false
3226
+ orientation: "vertical"
3345
3227
  }
3346
3228
  }
3347
3229
  );
3230
+ var radioGroupLabelVariants = cva([
3231
+ "text-body-medium font-medium",
3232
+ "text-on-surface",
3233
+ "mb-3",
3234
+ "data-[disabled]:opacity-38"
3235
+ ]);
3348
3236
  var Radio = forwardRef(
3349
- ({
3350
- // Content props
3351
- children,
3352
- // State props
3353
- disableRipple = false,
3354
- isDisabled = false,
3355
- // Styling
3356
- className,
3357
- // Other props
3358
- ...props
3359
- }, forwardedRef) => {
3237
+ ({ children, disableRipple = false, isDisabled = false, className, ...props }, forwardedRef) => {
3360
3238
  const state = useContext(RadioGroupContext);
3361
3239
  if (!state) {
3362
3240
  throw new Error("Radio must be used within a RadioGroup");
@@ -3371,127 +3249,57 @@ var Radio = forwardRef(
3371
3249
  "data-testid": _dataTestId,
3372
3250
  id: _htmlId,
3373
3251
  title: _htmlTitle,
3374
- ...restPropsWithoutHtmlAttrs
3252
+ ...ariaProps
3375
3253
  } = props;
3376
3254
  const {
3377
3255
  inputProps,
3378
3256
  isSelected,
3379
- isDisabled: radioIsDisabled
3257
+ isDisabled: radioIsDisabled,
3258
+ isPressed
3380
3259
  } = useRadio(
3381
- {
3382
- ...restPropsWithoutHtmlAttrs,
3383
- value: props.value
3384
- },
3260
+ { ...ariaProps, value: props.value },
3385
3261
  state,
3386
3262
  ref
3387
3263
  );
3388
3264
  const { isFocusVisible, focusProps } = useFocusRing();
3389
3265
  const finalIsDisabled = isDisabled || radioIsDisabled;
3390
- const visualState = isSelected ? "selected" : "unselected";
3266
+ const { isHovered, hoverProps } = useHover({ isDisabled: finalIsDisabled });
3267
+ const isInvalid = state.validationState === "invalid";
3391
3268
  const { onMouseDown: handleRipple, ripples } = useRipple({
3392
3269
  disabled: finalIsDisabled || disableRipple
3393
3270
  });
3394
3271
  if (process.env.NODE_ENV === "development") {
3395
- const ariaProps = restPropsWithoutHtmlAttrs;
3396
- if (!children && !ariaProps["aria-label"] && !ariaProps["aria-labelledby"]) {
3397
- console.warn(
3398
- "[Radio] Radio should have a label (children) or aria-label for accessibility."
3399
- );
3272
+ const a = ariaProps;
3273
+ if (!children && !a["aria-label"] && !a["aria-labelledby"]) {
3274
+ console.warn("[Radio] Provide a label via children or aria-label for accessibility.");
3400
3275
  }
3401
3276
  }
3402
- const isInvalid = state.validationState === "invalid";
3403
3277
  return /* @__PURE__ */ jsxs(
3404
3278
  "label",
3405
3279
  {
3406
- className: cn(
3407
- radioVariants({
3408
- disabled: finalIsDisabled
3409
- }),
3410
- className
3411
- ),
3280
+ ...mergeProps$1(hoverProps),
3281
+ className: cn(radioRootVariants(), "group/radio", className),
3412
3282
  "data-testid": dataTestId,
3413
3283
  title: htmlTitle,
3284
+ ...getInteractionDataAttributes({
3285
+ isHovered,
3286
+ isFocusVisible,
3287
+ isPressed,
3288
+ isSelected,
3289
+ isDisabled: finalIsDisabled,
3290
+ isInvalid
3291
+ }),
3414
3292
  children: [
3415
3293
  /* @__PURE__ */ jsx(VisuallyHidden, { children: /* @__PURE__ */ jsx("input", { ...mergeProps$1(inputProps, focusProps), ref, id: htmlId }) }),
3416
- /* @__PURE__ */ jsxs(
3417
- "div",
3418
- {
3419
- role: "presentation",
3420
- className: cn(
3421
- radioContainerVariants({
3422
- state: visualState,
3423
- isInvalid,
3424
- disabled: finalIsDisabled
3425
- })
3426
- ),
3427
- onMouseDown: handleRipple,
3428
- children: [
3429
- ripples,
3430
- /* @__PURE__ */ jsxs(
3431
- "svg",
3432
- {
3433
- width: "20",
3434
- height: "20",
3435
- viewBox: "0 0 20 20",
3436
- "aria-hidden": "true",
3437
- className: "relative z-10",
3438
- children: [
3439
- /* @__PURE__ */ jsx(
3440
- "circle",
3441
- {
3442
- cx: "10",
3443
- cy: "10",
3444
- r: "9",
3445
- className: cn(
3446
- radioIconOuterVariants({
3447
- state: visualState,
3448
- disabled: finalIsDisabled
3449
- })
3450
- )
3451
- }
3452
- ),
3453
- /* @__PURE__ */ jsx(
3454
- "circle",
3455
- {
3456
- cx: "10",
3457
- cy: "10",
3458
- r: "5",
3459
- className: cn(
3460
- radioIconInnerVariants({
3461
- visible: isSelected
3462
- })
3463
- )
3464
- }
3465
- ),
3466
- isFocusVisible && /* @__PURE__ */ jsx(
3467
- "circle",
3468
- {
3469
- cx: "10",
3470
- cy: "10",
3471
- r: "13",
3472
- fill: "none",
3473
- stroke: "currentColor",
3474
- strokeWidth: "2",
3475
- className: "animate-pulse"
3476
- }
3477
- )
3478
- ]
3479
- }
3480
- )
3481
- ]
3482
- }
3483
- ),
3484
- children && /* @__PURE__ */ jsx(
3485
- "span",
3486
- {
3487
- className: cn(
3488
- radioLabelVariants({
3489
- disabled: finalIsDisabled
3490
- })
3491
- ),
3492
- children
3493
- }
3494
- )
3294
+ /* @__PURE__ */ jsxs("div", { role: "presentation", className: cn(radioControlVariants()), children: [
3295
+ /* @__PURE__ */ jsx("div", { className: cn(radioFocusRingVariants()), "aria-hidden": "true" }),
3296
+ /* @__PURE__ */ jsxs("div", { role: "presentation", className: cn(radioTargetVariants()), onMouseDown: handleRipple, children: [
3297
+ ripples,
3298
+ /* @__PURE__ */ jsx("span", { className: cn(radioStateLayerVariants()), "aria-hidden": "true" }),
3299
+ /* @__PURE__ */ jsx("div", { className: cn(radioRingVariants()), "aria-hidden": "true", children: /* @__PURE__ */ jsx("div", { className: cn(radioDotVariants()), "aria-hidden": "true" }) })
3300
+ ] })
3301
+ ] }),
3302
+ children && /* @__PURE__ */ jsx("span", { className: cn(radioLabelVariants()), children })
3495
3303
  ]
3496
3304
  }
3497
3305
  );
@@ -3504,7 +3312,7 @@ var RadioGroup = forwardRef(
3504
3312
  children,
3505
3313
  // State props
3506
3314
  orientation = "vertical",
3507
- isInvalid: _isInvalid = false,
3315
+ isInvalid = false,
3508
3316
  isDisabled = false,
3509
3317
  // Styling
3510
3318
  className,
@@ -3527,6 +3335,7 @@ var RadioGroup = forwardRef(
3527
3335
  {
3528
3336
  ...restPropsWithoutHtmlAttrs,
3529
3337
  isDisabled,
3338
+ isInvalid,
3530
3339
  ref,
3531
3340
  className: cn("flex flex-col", className),
3532
3341
  "data-testid": dataTestId,
@@ -3534,11 +3343,8 @@ var RadioGroup = forwardRef(
3534
3343
  "div",
3535
3344
  {
3536
3345
  ...labelProps,
3537
- className: cn(
3538
- radioGroupLabelVariants({
3539
- disabled: isDisabled
3540
- })
3541
- ),
3346
+ "data-disabled": isDisabled ? "" : void 0,
3347
+ className: cn(radioGroupLabelVariants()),
3542
3348
  children: props.label
3543
3349
  }
3544
3350
  ),
@@ -3547,8 +3353,7 @@ var RadioGroup = forwardRef(
3547
3353
  {
3548
3354
  className: cn(
3549
3355
  radioGroupVariants({
3550
- orientation,
3551
- disabled: isDisabled
3356
+ orientation
3552
3357
  })
3553
3358
  ),
3554
3359
  children