@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/README.md CHANGED
@@ -12,7 +12,7 @@ A modern, accessible React component library implementing Google's Material Desi
12
12
 
13
13
  ## ✅ Status
14
14
 
15
- > **Latest Release: v0.16.0** (2026-06-10)
15
+ > **Latest Release: v0.17.0** (2026-06-10)
16
16
  >
17
17
  > **29 MD3 components** published to npm with full TypeScript support and WCAG 2.1 AA accessibility.
18
18
  >
@@ -138,14 +138,14 @@ See [THEMING.md](./THEMING.md) for the full customization guide.
138
138
 
139
139
  ### Phase 1b: Form Components ✅
140
140
 
141
- | Component | Status | Description |
142
- | ------------ | ------ | --------------------------------------------------------------------------- |
143
- | `TextField` | ✅ | MD3 expressive variants-vs-states, prefix/suffix, notched outline (v0.14.0) |
144
- | `Checkbox` | ✅ | MD3 variants-vs-states, spec-accurate icons (v0.8.1) |
145
- | `Radio` | ✅ | Radio button input |
146
- | `RadioGroup` | ✅ | Vertical and horizontal orientation |
147
- | `Switch` | ✅ | Toggle with variants-vs-states arch. |
148
- | `Slider` | ✅ | Standard, centered, range; discrete stops |
141
+ | Component | Status | Description |
142
+ | ------------ | ------ | ---------------------------------------------------------------------------------- |
143
+ | `TextField` | ✅ | MD3 expressive variants-vs-states, prefix/suffix, notched outline (v0.14.0) |
144
+ | `Checkbox` | ✅ | MD3 variants-vs-states, spec-accurate icons (v0.8.1) |
145
+ | `Radio` | ✅ | MD3 expressive variants-vs-states architecture, slot CVAs, spring motion (v0.17.0) |
146
+ | `RadioGroup` | ✅ | Vertical and horizontal orientation, `isInvalid` forwarding fix (v0.17.0) |
147
+ | `Switch` | ✅ | Toggle with variants-vs-states arch. |
148
+ | `Slider` | ✅ | Standard, centered, range; discrete stops |
149
149
 
150
150
  ### Phase 2: Navigation ✅
151
151
 
package/dist/index.cjs CHANGED
@@ -3144,224 +3144,102 @@ var RadioGroupHeadless = React.forwardRef(
3144
3144
  }
3145
3145
  );
3146
3146
  RadioGroupHeadless.displayName = "RadioGroupHeadless";
3147
+ var radioRootVariants = classVarianceAuthority.cva([
3148
+ "relative inline-flex items-center cursor-pointer select-none",
3149
+ // Disabled state — self-targeting so children inherit via group
3150
+ "data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none",
3151
+ "data-[disabled]:opacity-38"
3152
+ ]);
3153
+ var radioControlVariants = classVarianceAuthority.cva([
3154
+ "relative flex items-center justify-center flex-shrink-0",
3155
+ "size-10"
3156
+ // 40dp touch target (MD3 spec)
3157
+ ]);
3158
+ var radioFocusRingVariants = classVarianceAuthority.cva([
3159
+ "pointer-events-none absolute inset-0 rounded-full",
3160
+ "outline outline-2 outline-offset-2 outline-secondary",
3161
+ // Effects transition for opacity
3162
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
3163
+ "opacity-0",
3164
+ "group-data-[focus-visible]/radio:opacity-100"
3165
+ ]);
3166
+ var radioTargetVariants = classVarianceAuthority.cva([
3167
+ "absolute inset-0 rounded-full overflow-hidden",
3168
+ "flex items-center justify-center"
3169
+ ]);
3170
+ var radioStateLayerVariants = classVarianceAuthority.cva([
3171
+ "absolute inset-0 rounded-full pointer-events-none opacity-0",
3172
+ // Base state-layer color (unselected)
3173
+ "bg-on-surface",
3174
+ // Effects transition for opacity + color
3175
+ "transition-[opacity,background-color] duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
3176
+ // Selected state-layer color
3177
+ "group-data-[selected]/radio:bg-primary",
3178
+ // Error state-layer color (overrides selected via cascade position)
3179
+ "group-data-[invalid]/radio:bg-error",
3180
+ // Interaction opacities (MD3: hover 8%, focus/pressed 10%)
3181
+ "group-data-[hovered]/radio:opacity-8",
3182
+ "group-data-[focus-visible]/radio:opacity-10",
3183
+ "group-data-[pressed]/radio:opacity-10",
3184
+ // No state layer when disabled
3185
+ "group-data-[disabled]/radio:hidden"
3186
+ ]);
3187
+ var radioRingVariants = classVarianceAuthority.cva([
3188
+ "relative z-10 size-5 rounded-full border-2 flex items-center justify-center flex-shrink-0",
3189
+ // Base (unselected, enabled)
3190
+ "border-on-surface-variant",
3191
+ // Effects transition for border-color
3192
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
3193
+ // Selected — border becomes primary
3194
+ "group-data-[selected]/radio:border-primary",
3195
+ // Error — placed after selected so it overrides by cascade order
3196
+ "group-data-[invalid]/radio:border-error",
3197
+ // Disabled — on-surface/38 opacity value
3198
+ "group-data-[disabled]/radio:border-on-surface/38"
3199
+ ]);
3200
+ var radioDotVariants = classVarianceAuthority.cva([
3201
+ "size-2.5 rounded-full origin-center",
3202
+ // Spatial transition for scale — springs may overshoot (intentional)
3203
+ "transition-transform duration-spring-standard-fast-spatial ease-spring-standard-fast-spatial",
3204
+ // Base fill color (shown when selected)
3205
+ "bg-primary",
3206
+ // Hidden when unselected
3207
+ "scale-0",
3208
+ // Visible when selected
3209
+ "group-data-[selected]/radio:scale-100",
3210
+ // Error — error fill color (placed after selected by cascade)
3211
+ "group-data-[invalid]/radio:bg-error",
3212
+ // Disabled — use on-surface/38 opacity
3213
+ "group-data-[disabled]/radio:bg-on-surface/38"
3214
+ ]);
3215
+ var radioLabelVariants = classVarianceAuthority.cva(["text-body-medium text-on-surface select-none ml-4"]);
3147
3216
  var radioGroupVariants = classVarianceAuthority.cva(
3148
- [
3149
- // Base classes (always applied to group wrapper)
3150
- "flex",
3151
- "gap-4"
3152
- // 16px spacing between radios (MD3 standard)
3153
- ],
3217
+ ["flex", "gap-1"],
3218
+ // 4dp gap padding around each 40dp control handles visual spacing
3154
3219
  {
3155
3220
  variants: {
3156
3221
  /**
3157
3222
  * Layout orientation
3223
+ * @default "vertical"
3158
3224
  */
3159
3225
  orientation: {
3160
3226
  vertical: "flex-col",
3161
3227
  horizontal: "flex-row flex-wrap"
3162
- },
3163
- /**
3164
- * Disabled state
3165
- */
3166
- disabled: {
3167
- true: "",
3168
- false: ""
3169
- }
3170
- },
3171
- defaultVariants: {
3172
- orientation: "vertical",
3173
- disabled: false
3174
- }
3175
- }
3176
- );
3177
- var radioGroupLabelVariants = classVarianceAuthority.cva(
3178
- [
3179
- "text-sm font-medium",
3180
- // MD3: Body Medium
3181
- "text-on-surface",
3182
- "mb-3"
3183
- // Spacing below label (12px)
3184
- ],
3185
- {
3186
- variants: {
3187
- disabled: {
3188
- true: "opacity-38",
3189
- false: ""
3190
- }
3191
- },
3192
- defaultVariants: {
3193
- disabled: false
3194
- }
3195
- }
3196
- );
3197
- var radioVariants = classVarianceAuthority.cva(
3198
- [
3199
- // Base classes (always applied to label wrapper)
3200
- "relative inline-flex items-center cursor-pointer select-none",
3201
- "transition-opacity duration-200"
3202
- ],
3203
- {
3204
- variants: {
3205
- /**
3206
- * Disabled state
3207
- */
3208
- disabled: {
3209
- true: "opacity-38 cursor-not-allowed pointer-events-none",
3210
- false: ""
3211
- }
3212
- },
3213
- defaultVariants: {
3214
- disabled: false
3215
- }
3216
- }
3217
- );
3218
- var radioContainerVariants = classVarianceAuthority.cva(
3219
- [
3220
- // Base classes for radio visual container
3221
- "relative inline-flex items-center justify-center",
3222
- "w-10 h-10",
3223
- // 40x40dp touch target (MD3 spec)
3224
- "flex-shrink-0",
3225
- "transition-all duration-200",
3226
- "m-1",
3227
- // 4px margin around radio for spacing (total 8px gap between radios)
3228
- // State layer (hover, focus, active) - MD3 spec: 8%/12%/12% opacity
3229
- "before:absolute before:inset-0 before:rounded-full before:transition-opacity before:duration-200",
3230
- "before:bg-current before:opacity-0",
3231
- "hover:before:opacity-8",
3232
- "active:before:opacity-12"
3233
- ],
3234
- {
3235
- variants: {
3236
- /**
3237
- * Radio state (determines visual appearance)
3238
- */
3239
- state: {
3240
- unselected: "text-on-surface-variant",
3241
- selected: "text-primary"
3242
- },
3243
- /**
3244
- * Error/invalid state
3245
- */
3246
- isInvalid: {
3247
- true: "text-error",
3248
- false: ""
3249
- },
3250
- /**
3251
- * Disabled state
3252
- */
3253
- disabled: {
3254
- true: "text-on-surface pointer-events-none",
3255
- false: ""
3256
- }
3257
- },
3258
- compoundVariants: [
3259
- // Error state overrides normal colors for all states
3260
- {
3261
- state: "unselected",
3262
- isInvalid: true,
3263
- disabled: false,
3264
- className: "text-error"
3265
- },
3266
- {
3267
- state: "selected",
3268
- isInvalid: true,
3269
- disabled: false,
3270
- className: "text-error"
3271
- }
3272
- ],
3273
- defaultVariants: {
3274
- state: "unselected",
3275
- isInvalid: false,
3276
- disabled: false
3277
- }
3278
- }
3279
- );
3280
- var radioIconOuterVariants = classVarianceAuthority.cva(
3281
- [
3282
- // Base classes for the radio outer circle
3283
- "transition-all duration-200 stroke-current stroke-2 fill-transparent"
3284
- ],
3285
- {
3286
- variants: {
3287
- /**
3288
- * Radio state
3289
- */
3290
- state: {
3291
- unselected: [],
3292
- selected: []
3293
- },
3294
- /**
3295
- * Disabled state
3296
- */
3297
- disabled: {
3298
- true: ["fill-transparent", "stroke-current", "stroke-2"],
3299
- false: ""
3300
- }
3301
- },
3302
- compoundVariants: [
3303
- // Disabled + selected state overrides fill
3304
- {
3305
- state: "selected",
3306
- disabled: true,
3307
- className: "fill-current stroke-none"
3308
- }
3309
- ],
3310
- defaultVariants: {
3311
- state: "unselected",
3312
- disabled: false
3313
- }
3314
- }
3315
- );
3316
- var radioIconInnerVariants = classVarianceAuthority.cva(["transition-all origin-center duration-200"], {
3317
- variants: {
3318
- /**
3319
- * Visibility based on state
3320
- */
3321
- state: {
3322
- selected: ["fill-current"],
3323
- unselected: ["fill-transparent"]
3324
- },
3325
- visible: {
3326
- true: "opacity-100 scale-100 fill-current",
3327
- false: "opacity-0 scale-0"
3328
- }
3329
- },
3330
- defaultVariants: {
3331
- visible: false
3332
- }
3333
- });
3334
- var radioLabelVariants = classVarianceAuthority.cva(
3335
- [
3336
- "text-sm",
3337
- // MD3: Body Medium (14px)
3338
- "text-on-surface",
3339
- "select-none"
3340
- ],
3341
- {
3342
- variants: {
3343
- disabled: {
3344
- true: "",
3345
- false: ""
3346
3228
  }
3347
3229
  },
3348
3230
  defaultVariants: {
3349
- disabled: false
3231
+ orientation: "vertical"
3350
3232
  }
3351
3233
  }
3352
3234
  );
3235
+ var radioGroupLabelVariants = classVarianceAuthority.cva([
3236
+ "text-body-medium font-medium",
3237
+ "text-on-surface",
3238
+ "mb-3",
3239
+ "data-[disabled]:opacity-38"
3240
+ ]);
3353
3241
  var Radio = React.forwardRef(
3354
- ({
3355
- // Content props
3356
- children,
3357
- // State props
3358
- disableRipple = false,
3359
- isDisabled = false,
3360
- // Styling
3361
- className,
3362
- // Other props
3363
- ...props
3364
- }, forwardedRef) => {
3242
+ ({ children, disableRipple = false, isDisabled = false, className, ...props }, forwardedRef) => {
3365
3243
  const state = React.useContext(RadioGroupContext);
3366
3244
  if (!state) {
3367
3245
  throw new Error("Radio must be used within a RadioGroup");
@@ -3376,127 +3254,57 @@ var Radio = React.forwardRef(
3376
3254
  "data-testid": _dataTestId,
3377
3255
  id: _htmlId,
3378
3256
  title: _htmlTitle,
3379
- ...restPropsWithoutHtmlAttrs
3257
+ ...ariaProps
3380
3258
  } = props;
3381
3259
  const {
3382
3260
  inputProps,
3383
3261
  isSelected,
3384
- isDisabled: radioIsDisabled
3262
+ isDisabled: radioIsDisabled,
3263
+ isPressed
3385
3264
  } = reactAria.useRadio(
3386
- {
3387
- ...restPropsWithoutHtmlAttrs,
3388
- value: props.value
3389
- },
3265
+ { ...ariaProps, value: props.value },
3390
3266
  state,
3391
3267
  ref
3392
3268
  );
3393
3269
  const { isFocusVisible, focusProps } = reactAria.useFocusRing();
3394
3270
  const finalIsDisabled = isDisabled || radioIsDisabled;
3395
- const visualState = isSelected ? "selected" : "unselected";
3271
+ const { isHovered, hoverProps } = reactAria.useHover({ isDisabled: finalIsDisabled });
3272
+ const isInvalid = state.validationState === "invalid";
3396
3273
  const { onMouseDown: handleRipple, ripples } = useRipple({
3397
3274
  disabled: finalIsDisabled || disableRipple
3398
3275
  });
3399
3276
  if (process.env.NODE_ENV === "development") {
3400
- const ariaProps = restPropsWithoutHtmlAttrs;
3401
- if (!children && !ariaProps["aria-label"] && !ariaProps["aria-labelledby"]) {
3402
- console.warn(
3403
- "[Radio] Radio should have a label (children) or aria-label for accessibility."
3404
- );
3277
+ const a = ariaProps;
3278
+ if (!children && !a["aria-label"] && !a["aria-labelledby"]) {
3279
+ console.warn("[Radio] Provide a label via children or aria-label for accessibility.");
3405
3280
  }
3406
3281
  }
3407
- const isInvalid = state.validationState === "invalid";
3408
3282
  return /* @__PURE__ */ jsxRuntime.jsxs(
3409
3283
  "label",
3410
3284
  {
3411
- className: cn(
3412
- radioVariants({
3413
- disabled: finalIsDisabled
3414
- }),
3415
- className
3416
- ),
3285
+ ...reactAria.mergeProps(hoverProps),
3286
+ className: cn(radioRootVariants(), "group/radio", className),
3417
3287
  "data-testid": dataTestId,
3418
3288
  title: htmlTitle,
3289
+ ...getInteractionDataAttributes({
3290
+ isHovered,
3291
+ isFocusVisible,
3292
+ isPressed,
3293
+ isSelected,
3294
+ isDisabled: finalIsDisabled,
3295
+ isInvalid
3296
+ }),
3419
3297
  children: [
3420
3298
  /* @__PURE__ */ jsxRuntime.jsx(reactAria.VisuallyHidden, { children: /* @__PURE__ */ jsxRuntime.jsx("input", { ...reactAria.mergeProps(inputProps, focusProps), ref, id: htmlId }) }),
3421
- /* @__PURE__ */ jsxRuntime.jsxs(
3422
- "div",
3423
- {
3424
- role: "presentation",
3425
- className: cn(
3426
- radioContainerVariants({
3427
- state: visualState,
3428
- isInvalid,
3429
- disabled: finalIsDisabled
3430
- })
3431
- ),
3432
- onMouseDown: handleRipple,
3433
- children: [
3434
- ripples,
3435
- /* @__PURE__ */ jsxRuntime.jsxs(
3436
- "svg",
3437
- {
3438
- width: "20",
3439
- height: "20",
3440
- viewBox: "0 0 20 20",
3441
- "aria-hidden": "true",
3442
- className: "relative z-10",
3443
- children: [
3444
- /* @__PURE__ */ jsxRuntime.jsx(
3445
- "circle",
3446
- {
3447
- cx: "10",
3448
- cy: "10",
3449
- r: "9",
3450
- className: cn(
3451
- radioIconOuterVariants({
3452
- state: visualState,
3453
- disabled: finalIsDisabled
3454
- })
3455
- )
3456
- }
3457
- ),
3458
- /* @__PURE__ */ jsxRuntime.jsx(
3459
- "circle",
3460
- {
3461
- cx: "10",
3462
- cy: "10",
3463
- r: "5",
3464
- className: cn(
3465
- radioIconInnerVariants({
3466
- visible: isSelected
3467
- })
3468
- )
3469
- }
3470
- ),
3471
- isFocusVisible && /* @__PURE__ */ jsxRuntime.jsx(
3472
- "circle",
3473
- {
3474
- cx: "10",
3475
- cy: "10",
3476
- r: "13",
3477
- fill: "none",
3478
- stroke: "currentColor",
3479
- strokeWidth: "2",
3480
- className: "animate-pulse"
3481
- }
3482
- )
3483
- ]
3484
- }
3485
- )
3486
- ]
3487
- }
3488
- ),
3489
- children && /* @__PURE__ */ jsxRuntime.jsx(
3490
- "span",
3491
- {
3492
- className: cn(
3493
- radioLabelVariants({
3494
- disabled: finalIsDisabled
3495
- })
3496
- ),
3497
- children
3498
- }
3499
- )
3299
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "presentation", className: cn(radioControlVariants()), children: [
3300
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(radioFocusRingVariants()), "aria-hidden": "true" }),
3301
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "presentation", className: cn(radioTargetVariants()), onMouseDown: handleRipple, children: [
3302
+ ripples,
3303
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(radioStateLayerVariants()), "aria-hidden": "true" }),
3304
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(radioRingVariants()), "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(radioDotVariants()), "aria-hidden": "true" }) })
3305
+ ] })
3306
+ ] }),
3307
+ children && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(radioLabelVariants()), children })
3500
3308
  ]
3501
3309
  }
3502
3310
  );
@@ -3509,7 +3317,7 @@ var RadioGroup = React.forwardRef(
3509
3317
  children,
3510
3318
  // State props
3511
3319
  orientation = "vertical",
3512
- isInvalid: _isInvalid = false,
3320
+ isInvalid = false,
3513
3321
  isDisabled = false,
3514
3322
  // Styling
3515
3323
  className,
@@ -3532,6 +3340,7 @@ var RadioGroup = React.forwardRef(
3532
3340
  {
3533
3341
  ...restPropsWithoutHtmlAttrs,
3534
3342
  isDisabled,
3343
+ isInvalid,
3535
3344
  ref,
3536
3345
  className: cn("flex flex-col", className),
3537
3346
  "data-testid": dataTestId,
@@ -3539,11 +3348,8 @@ var RadioGroup = React.forwardRef(
3539
3348
  "div",
3540
3349
  {
3541
3350
  ...labelProps,
3542
- className: cn(
3543
- radioGroupLabelVariants({
3544
- disabled: isDisabled
3545
- })
3546
- ),
3351
+ "data-disabled": isDisabled ? "" : void 0,
3352
+ className: cn(radioGroupLabelVariants()),
3547
3353
  children: props.label
3548
3354
  }
3549
3355
  ),
@@ -3552,8 +3358,7 @@ var RadioGroup = React.forwardRef(
3552
3358
  {
3553
3359
  className: cn(
3554
3360
  radioGroupVariants({
3555
- orientation,
3556
- disabled: isDisabled
3361
+ orientation
3557
3362
  })
3558
3363
  ),
3559
3364
  children