@tinybigui/react 0.4.2 → 0.6.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.cjs CHANGED
@@ -1118,81 +1118,113 @@ var IconButtonHeadless = React.forwardRef(
1118
1118
  tabIndex = 0,
1119
1119
  onMouseDown,
1120
1120
  type,
1121
- selected,
1121
+ isSelected,
1122
+ isToggle = false,
1123
+ isDisabled = false,
1122
1124
  "aria-label": ariaLabel,
1123
1125
  title,
1124
1126
  ...props
1125
1127
  }, forwardedRef) => {
1126
1128
  const internalRef = React.useRef(null);
1127
1129
  const ref = forwardedRef ?? internalRef;
1128
- const { buttonProps } = reactAria.useButton(
1130
+ const { buttonProps, isPressed } = reactAria.useButton(
1129
1131
  {
1130
1132
  ...props,
1131
- // Ensure element type is 'button' for proper semantics
1132
1133
  elementType: "button",
1133
- // Pass aria-label
1134
- "aria-label": ariaLabel
1134
+ "aria-label": ariaLabel,
1135
+ isDisabled
1135
1136
  },
1136
1137
  ref
1137
1138
  );
1139
+ const { isHovered, hoverProps } = reactAria.useHover({ isDisabled });
1140
+ const { isFocusVisible, focusProps } = reactAria.useFocusRing();
1138
1141
  const domProps = utils.filterDOMProps(props);
1139
- const mergedProps = utils.mergeProps(
1140
- buttonProps,
1141
- domProps,
1142
+ const mergedProps = utils.mergeProps(buttonProps, hoverProps, focusProps, domProps, {
1143
+ tabIndex,
1144
+ className,
1145
+ onMouseDown,
1146
+ type: type ?? "button",
1147
+ ...title && { title },
1148
+ // aria-pressed only when acting as a toggle button
1149
+ ...isToggle && { "aria-pressed": isSelected ?? false }
1150
+ });
1151
+ return /* @__PURE__ */ jsxRuntime.jsx(
1152
+ "button",
1142
1153
  {
1143
- tabIndex,
1144
- className,
1145
- onMouseDown,
1146
- type: type ?? "button",
1147
- // Add aria-pressed for toggle buttons (only if selected is defined)
1148
- ...selected !== void 0 && { "aria-pressed": selected },
1149
- // Add title if provided
1150
- ...title && { title }
1154
+ ...mergedProps,
1155
+ ref,
1156
+ type: type === "submit" ? "submit" : type === "reset" ? "reset" : "button",
1157
+ ...getInteractionDataAttributes({
1158
+ isHovered,
1159
+ isFocusVisible,
1160
+ isPressed,
1161
+ ...isToggle ? { isSelected: isSelected ?? false } : {},
1162
+ isDisabled
1163
+ }),
1164
+ "data-toggle": isToggle ? "" : void 0,
1165
+ children
1151
1166
  }
1152
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1153
- );
1154
- return (
1155
- // eslint-disable-next-line react/button-has-type
1156
- /* @__PURE__ */ jsxRuntime.jsx("button", { ...mergedProps, ref, type: type ?? "button", children })
1157
1167
  );
1158
1168
  }
1159
1169
  );
1160
1170
  IconButtonHeadless.displayName = "IconButtonHeadless";
1161
- var iconButtonVariants = classVarianceAuthority.cva(
1171
+ var iconButtonRootVariants = classVarianceAuthority.cva(
1162
1172
  [
1163
- // Base classes (always applied)
1164
- "relative inline-flex items-center justify-center cursor-pointer",
1165
- "overflow-hidden rounded-full",
1166
- // Circular shape
1167
- // Split MD3 transition: btn-transition handles spatial (border-radius) with asymmetric
1168
- // easing (decelerate by default, switched to expressive via btn-transition-selected when
1169
- // the button is group-selected) and effects (color/bg/shadow) with standard spring.
1173
+ // Layout
1174
+ "relative inline-flex items-center justify-center",
1175
+ "cursor-pointer select-none",
1176
+ "overflow-hidden",
1177
+ // Corner radius driven by CSS variable set per shape×size in compoundVariants.
1178
+ // Fallback 9999px is only reached if both shape and size props are absent,
1179
+ // which cannot happen in normal usage.
1180
+ "rounded-[var(--ib-radius,9999px)]",
1181
+ // Split MD3 transition via the existing btn-transition utility:
1182
+ // border-radius → emphasized-decelerate (no overshoot, no sharp-corner flash)
1183
+ // color/bg/border/opacity → standard-fast-effects (no overshoot on effects)
1184
+ // This is identical to the approach used by Button/connected ButtonGroup and is
1185
+ // the standard fix for the 9999px overshoot problem documented in styles.css.
1170
1186
  "btn-transition",
1171
- "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
1172
- // State layers — effects token: opacity only, no overshoot (separate ::before pseudo-element)
1173
- "before:absolute before:inset-0 before:rounded-[inherit]",
1174
- "before:transition-opacity before:duration-spring-standard-fast-effects before:ease-spring-standard-fast-effects",
1175
- "before:bg-current before:opacity-0",
1176
- "hover:before:opacity-8",
1177
- "focus-visible:before:opacity-12",
1178
- "active:before:opacity-12"
1187
+ // Background + border + text driven from CSS role variables
1188
+ "bg-[var(--ib-bg,transparent)]",
1189
+ "border border-[var(--ib-border,transparent)]",
1190
+ "text-[var(--ib-fg,currentColor)]",
1191
+ // Toggle: off state (data-toggle present but data-selected absent)
1192
+ // Uses doubly-chained selector to beat single-chain specificity of defaults
1193
+ "data-[toggle]:bg-[var(--ib-bg-off,var(--ib-bg,transparent))]",
1194
+ "data-[toggle]:text-[var(--ib-fg-off,var(--ib-fg,currentColor))]",
1195
+ // Selected state
1196
+ "data-[selected]:bg-[var(--ib-bg-on,var(--ib-bg,transparent))]",
1197
+ "data-[selected]:text-[var(--ib-fg-on,var(--ib-fg,currentColor))]",
1198
+ "data-[selected]:border-transparent",
1199
+ // Press shape-morph: radius collapses to --ib-radius-press on press
1200
+ // (only has visual effect when --ib-radius-press differs from --ib-radius)
1201
+ "data-[pressed]:rounded-[var(--ib-radius-press,var(--ib-radius,9999px))]",
1202
+ // Focus ring (outline, not a state layer — stays outside overflow-hidden
1203
+ // because it's drawn as outline on the root element itself)
1204
+ "outline-none",
1205
+ "group-data-[focus-visible]/icon-button:outline-2",
1206
+ "group-data-[focus-visible]/icon-button:outline-offset-2",
1207
+ "group-data-[focus-visible]/icon-button:outline-secondary",
1208
+ // Disabled — content opacity 38%, container handled per variant via CSS vars
1209
+ "data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none",
1210
+ "data-[disabled]:text-on-surface/38"
1211
+ // Filled/tonal/outlined-selected backgrounds collapse to on-surface/12 — set
1212
+ // via compoundVariants on the root for variants that have a container.
1213
+ // For variants with transparent bg (standard, outlined-unselected) we do nothing.
1179
1214
  ],
1180
1215
  {
1181
1216
  variants: {
1182
1217
  /**
1183
- * Button variant (MD3 specification)
1218
+ * Visual style variant (MD3 icon button types)
1184
1219
  */
1185
1220
  variant: {
1186
- standard: "bg-transparent",
1187
- // No background
1188
- filled: "shadow-none",
1189
- // Solid background
1190
- tonal: "",
1191
- // Container background
1192
- outlined: "bg-transparent border border-outline"
1221
+ standard: "",
1222
+ filled: "data-[disabled]:bg-on-surface/12",
1223
+ tonal: "data-[disabled]:bg-on-surface/12",
1224
+ outlined: ""
1193
1225
  },
1194
1226
  /**
1195
- * Color scheme (MD3 color roles)
1227
+ * Color scheme sets CSS role variables via compoundVariants.
1196
1228
  */
1197
1229
  color: {
1198
1230
  primary: "",
@@ -1201,180 +1233,400 @@ var iconButtonVariants = classVarianceAuthority.cva(
1201
1233
  error: ""
1202
1234
  },
1203
1235
  /**
1204
- * Button size (square dimensions)
1236
+ * Size tier (M3 Expressive 5-tier)
1205
1237
  */
1206
1238
  size: {
1207
- small: "h-8 w-8",
1208
- // 32×32px
1209
- medium: "h-10 w-10",
1210
- // 40×40px (default)
1211
- large: "h-12 w-12"
1212
- // 48×48px
1239
+ xsmall: "h-8",
1240
+ small: "h-10",
1241
+ medium: "h-14",
1242
+ large: "h-24",
1243
+ xlarge: "h-[8.5rem]"
1213
1244
  },
1214
1245
  /**
1215
- * Selected state (for toggle buttons)
1246
+ * Width variant adjusts container width
1216
1247
  */
1217
- selected: {
1218
- true: "",
1219
- false: ""
1248
+ width: {
1249
+ narrow: "",
1250
+ default: "",
1251
+ wide: ""
1220
1252
  },
1221
1253
  /**
1222
- * Disabled state
1254
+ * Shape — base values only; per-size radii set via compoundVariants below.
1255
+ *
1256
+ * round: --ib-radius = half the container height (true circle), set per size.
1257
+ * --ib-radius-press = square corner for that size (set per size).
1258
+ * Morph distance is small (e.g. 28px → 16px for medium), so the
1259
+ * emphasized-decelerate curve from btn-transition produces a smooth,
1260
+ * non-overshooting transition. The old 9999px fallback caused the
1261
+ * spring to overshoot below 0 = sharp-corner flash.
1262
+ * square: --ib-radius = size-tiered MD3 corner, set per size. No press morph.
1223
1263
  */
1224
- isDisabled: {
1225
- true: "pointer-events-none cursor-not-allowed opacity-38",
1226
- false: ""
1264
+ shape: {
1265
+ round: [],
1266
+ square: []
1227
1267
  }
1228
1268
  },
1229
- /**
1230
- * Compound variants - combinations of variant + color + selected
1231
- */
1232
1269
  compoundVariants: [
1233
- // ====================
1234
- // STANDARD VARIANTS
1235
- // ====================
1236
- {
1237
- variant: "standard",
1238
- selected: false,
1239
- className: "text-on-surface-variant"
1240
- },
1270
+ // ══════════════════════════════════════════════════════════════════════
1271
+ // SIZE × WIDTH — container width
1272
+ // ══════════════════════════════════════════════════════════════════════
1273
+ { size: "xsmall", width: "narrow", className: "w-6" },
1274
+ { size: "xsmall", width: "default", className: "w-8" },
1275
+ { size: "xsmall", width: "wide", className: "w-10" },
1276
+ { size: "small", width: "narrow", className: "w-8" },
1277
+ { size: "small", width: "default", className: "w-10" },
1278
+ { size: "small", width: "wide", className: "w-13" },
1279
+ { size: "medium", width: "narrow", className: "w-12" },
1280
+ { size: "medium", width: "default", className: "w-14" },
1281
+ { size: "medium", width: "wide", className: "w-18" },
1282
+ { size: "large", width: "narrow", className: "w-18" },
1283
+ { size: "large", width: "default", className: "w-24" },
1284
+ { size: "large", width: "wide", className: "w-32" },
1285
+ { size: "xlarge", width: "narrow", className: "w-24" },
1286
+ { size: "xlarge", width: "default", className: "w-[8.5rem]" },
1287
+ { size: "xlarge", width: "wide", className: "w-42" },
1288
+ // ══════════════════════════════════════════════════════════════════════
1289
+ // SHAPE × SIZE — corner radii for both round and square shapes
1290
+ // ══════════════════════════════════════════════════════════════════════
1291
+ //
1292
+ // Round rest radius = half container height (true circle).
1293
+ // Using the exact half-height keeps the morph distance small, so the
1294
+ // no-overshoot emphasized-decelerate curve in btn-transition produces a
1295
+ // smooth animation. Using 9999px was the original cause of the sharp-
1296
+ // corner flash (the spring overshoots below 0 before settling).
1297
+ //
1298
+ // xsmall h-8 = 32px → half = 16px = 1rem
1299
+ // small h-10 = 40px → half = 20px = 1.25rem
1300
+ // medium h-14 = 56px → half = 28px = 1.75rem
1301
+ // large h-24 = 96px → half = 48px = 3rem
1302
+ // xlarge h-34 = 136px → half = 68px = 4.25rem
1303
+ //
1304
+ // Round press-morph target = MD3 square corner for that size tier.
1305
+ // Square rest radius = same MD3 corner (no morph).
1306
+ // ── round: rest radius (half height) ──────────────────────────────────
1307
+ { shape: "round", size: "xsmall", className: "[--ib-radius:1rem]" },
1308
+ { shape: "round", size: "small", className: "[--ib-radius:1.25rem]" },
1309
+ { shape: "round", size: "medium", className: "[--ib-radius:1.75rem]" },
1310
+ { shape: "round", size: "large", className: "[--ib-radius:3rem]" },
1311
+ { shape: "round", size: "xlarge", className: "[--ib-radius:4.25rem]" },
1312
+ // ── round: press-morph target (square corner for that size) ───────────
1313
+ { shape: "round", size: "xsmall", className: "[--ib-radius-press:0.75rem]" },
1314
+ { shape: "round", size: "small", className: "[--ib-radius-press:0.75rem]" },
1315
+ { shape: "round", size: "medium", className: "[--ib-radius-press:1rem]" },
1316
+ { shape: "round", size: "large", className: "[--ib-radius-press:1.75rem]" },
1317
+ { shape: "round", size: "xlarge", className: "[--ib-radius-press:1.75rem]" },
1318
+ // ── square: rest radius (MD3 shape scale) ─────────────────────────────
1319
+ // xsmall / small → 12px (0.75rem), medium → 16px (1rem), large / xlarge → 28px (1.75rem)
1320
+ { shape: "square", size: "xsmall", className: "[--ib-radius:0.75rem]" },
1321
+ { shape: "square", size: "small", className: "[--ib-radius:0.75rem]" },
1322
+ { shape: "square", size: "medium", className: "[--ib-radius:1rem]" },
1323
+ { shape: "square", size: "large", className: "[--ib-radius:1.75rem]" },
1324
+ { shape: "square", size: "xlarge", className: "[--ib-radius:1.75rem]" },
1325
+ // ══════════════════════════════════════════════════════════════════════
1326
+ // VARIANT × COLOR — CSS role variable assignments
1327
+ // Only variant × color (design-time decisions); no state variants here.
1328
+ // ══════════════════════════════════════════════════════════════════════
1329
+ // ── STANDARD ──────────────────────────────────────────────────────────
1330
+ // Non-toggle standard: transparent bg, on-surface-variant fg
1331
+ // Selected: primary fg
1332
+ // State layer: on-surface-variant (unselected) / primary (selected)
1241
1333
  {
1242
1334
  variant: "standard",
1243
- selected: true,
1244
- className: "text-primary"
1245
- },
1246
- // ====================
1247
- // FILLED VARIANTS (UNSELECTED)
1248
- // ====================
1249
- {
1250
- variant: "filled",
1251
1335
  color: "primary",
1252
- selected: false,
1253
- className: "bg-primary text-on-primary"
1336
+ className: [
1337
+ "[--ib-bg:transparent]",
1338
+ "[--ib-fg:var(--color-on-surface-variant)]",
1339
+ "[--ib-fg-on:var(--color-primary)]",
1340
+ "[--ib-sl:var(--color-on-surface-variant)]",
1341
+ // toggle-off same as non-toggle
1342
+ "[--ib-bg-off:transparent]",
1343
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1344
+ // toggle-on: selected
1345
+ "[--ib-bg-on:transparent]"
1346
+ ]
1254
1347
  },
1255
1348
  {
1256
- variant: "filled",
1349
+ variant: "standard",
1257
1350
  color: "secondary",
1258
- selected: false,
1259
- className: "bg-secondary text-on-secondary"
1351
+ className: [
1352
+ "[--ib-bg:transparent]",
1353
+ "[--ib-fg:var(--color-on-surface-variant)]",
1354
+ "[--ib-fg-on:var(--color-secondary)]",
1355
+ "[--ib-sl:var(--color-on-surface-variant)]",
1356
+ "[--ib-bg-off:transparent]",
1357
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1358
+ "[--ib-bg-on:transparent]"
1359
+ ]
1260
1360
  },
1261
1361
  {
1262
- variant: "filled",
1362
+ variant: "standard",
1263
1363
  color: "tertiary",
1264
- selected: false,
1265
- className: "bg-tertiary text-on-tertiary"
1364
+ className: [
1365
+ "[--ib-bg:transparent]",
1366
+ "[--ib-fg:var(--color-on-surface-variant)]",
1367
+ "[--ib-fg-on:var(--color-tertiary)]",
1368
+ "[--ib-sl:var(--color-on-surface-variant)]",
1369
+ "[--ib-bg-off:transparent]",
1370
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1371
+ "[--ib-bg-on:transparent]"
1372
+ ]
1266
1373
  },
1267
1374
  {
1268
- variant: "filled",
1375
+ variant: "standard",
1269
1376
  color: "error",
1270
- selected: false,
1271
- className: "bg-error text-on-error"
1377
+ className: [
1378
+ "[--ib-bg:transparent]",
1379
+ "[--ib-fg:var(--color-on-surface-variant)]",
1380
+ "[--ib-fg-on:var(--color-error)]",
1381
+ "[--ib-sl:var(--color-on-surface-variant)]",
1382
+ "[--ib-bg-off:transparent]",
1383
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1384
+ "[--ib-bg-on:transparent]"
1385
+ ]
1272
1386
  },
1273
- // ====================
1274
- // FILLED VARIANTS (SELECTED - uses container colors)
1275
- // ====================
1387
+ // ── FILLED ────────────────────────────────────────────────────────────
1388
+ // Non-toggle: bg primary / fg on-primary
1389
+ // Toggle off: bg surface-container-highest / fg primary
1390
+ // Toggle on (selected): bg primary / fg on-primary
1391
+ // State layer: on-primary (non-toggle / selected), primary (toggle-off)
1276
1392
  {
1277
1393
  variant: "filled",
1278
1394
  color: "primary",
1279
- selected: true,
1280
- className: "bg-primary-container text-on-primary-container"
1395
+ className: [
1396
+ "[--ib-bg:var(--color-primary)]",
1397
+ "[--ib-fg:var(--color-on-primary)]",
1398
+ "[--ib-sl:var(--color-on-primary)]",
1399
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1400
+ "[--ib-fg-off:var(--color-primary)]",
1401
+ "[--ib-bg-on:var(--color-primary)]",
1402
+ "[--ib-fg-on:var(--color-on-primary)]"
1403
+ ]
1281
1404
  },
1282
1405
  {
1283
1406
  variant: "filled",
1284
1407
  color: "secondary",
1285
- selected: true,
1286
- className: "bg-secondary-container text-on-secondary-container"
1408
+ className: [
1409
+ "[--ib-bg:var(--color-secondary)]",
1410
+ "[--ib-fg:var(--color-on-secondary)]",
1411
+ "[--ib-sl:var(--color-on-secondary)]",
1412
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1413
+ "[--ib-fg-off:var(--color-secondary)]",
1414
+ "[--ib-bg-on:var(--color-secondary)]",
1415
+ "[--ib-fg-on:var(--color-on-secondary)]"
1416
+ ]
1287
1417
  },
1288
1418
  {
1289
1419
  variant: "filled",
1290
1420
  color: "tertiary",
1291
- selected: true,
1292
- className: "bg-tertiary-container text-on-tertiary-container"
1421
+ className: [
1422
+ "[--ib-bg:var(--color-tertiary)]",
1423
+ "[--ib-fg:var(--color-on-tertiary)]",
1424
+ "[--ib-sl:var(--color-on-tertiary)]",
1425
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1426
+ "[--ib-fg-off:var(--color-tertiary)]",
1427
+ "[--ib-bg-on:var(--color-tertiary)]",
1428
+ "[--ib-fg-on:var(--color-on-tertiary)]"
1429
+ ]
1293
1430
  },
1294
1431
  {
1295
1432
  variant: "filled",
1296
1433
  color: "error",
1297
- selected: true,
1298
- className: "bg-error-container text-on-error-container"
1434
+ className: [
1435
+ "[--ib-bg:var(--color-error)]",
1436
+ "[--ib-fg:var(--color-on-error)]",
1437
+ "[--ib-sl:var(--color-on-error)]",
1438
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1439
+ "[--ib-fg-off:var(--color-error)]",
1440
+ "[--ib-bg-on:var(--color-error)]",
1441
+ "[--ib-fg-on:var(--color-on-error)]"
1442
+ ]
1299
1443
  },
1300
- // ====================
1301
- // TONAL VARIANTS (UNSELECTED)
1302
- // ====================
1444
+ // ── TONAL ─────────────────────────────────────────────────────────────
1445
+ // Non-toggle: bg secondary-container / fg on-secondary-container
1446
+ // Toggle off: bg surface-container-highest / fg on-surface-variant
1447
+ // Toggle on (selected): bg secondary-container / fg on-secondary-container
1303
1448
  {
1304
1449
  variant: "tonal",
1305
1450
  color: "primary",
1306
- selected: false,
1307
- className: "bg-secondary-container text-on-secondary-container"
1451
+ className: [
1452
+ "[--ib-bg:var(--color-secondary-container)]",
1453
+ "[--ib-fg:var(--color-on-secondary-container)]",
1454
+ "[--ib-sl:var(--color-on-secondary-container)]",
1455
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1456
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1457
+ "[--ib-bg-on:var(--color-secondary-container)]",
1458
+ "[--ib-fg-on:var(--color-on-secondary-container)]"
1459
+ ]
1308
1460
  },
1309
1461
  {
1310
1462
  variant: "tonal",
1311
1463
  color: "secondary",
1312
- selected: false,
1313
- className: "bg-secondary-container text-on-secondary-container"
1464
+ className: [
1465
+ "[--ib-bg:var(--color-secondary-container)]",
1466
+ "[--ib-fg:var(--color-on-secondary-container)]",
1467
+ "[--ib-sl:var(--color-on-secondary-container)]",
1468
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1469
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1470
+ "[--ib-bg-on:var(--color-secondary-container)]",
1471
+ "[--ib-fg-on:var(--color-on-secondary-container)]"
1472
+ ]
1314
1473
  },
1315
1474
  {
1316
1475
  variant: "tonal",
1317
1476
  color: "tertiary",
1318
- selected: false,
1319
- className: "bg-tertiary-container text-on-tertiary-container"
1477
+ className: [
1478
+ "[--ib-bg:var(--color-tertiary-container)]",
1479
+ "[--ib-fg:var(--color-on-tertiary-container)]",
1480
+ "[--ib-sl:var(--color-on-tertiary-container)]",
1481
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1482
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1483
+ "[--ib-bg-on:var(--color-tertiary-container)]",
1484
+ "[--ib-fg-on:var(--color-on-tertiary-container)]"
1485
+ ]
1320
1486
  },
1321
1487
  {
1322
1488
  variant: "tonal",
1323
1489
  color: "error",
1324
- selected: false,
1325
- className: "bg-error-container text-on-error-container"
1490
+ className: [
1491
+ "[--ib-bg:var(--color-error-container)]",
1492
+ "[--ib-fg:var(--color-on-error-container)]",
1493
+ "[--ib-sl:var(--color-on-error-container)]",
1494
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1495
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1496
+ "[--ib-bg-on:var(--color-error-container)]",
1497
+ "[--ib-fg-on:var(--color-on-error-container)]"
1498
+ ]
1326
1499
  },
1327
- // ====================
1328
- // TONAL VARIANTS (SELECTED - uses tertiary container)
1329
- // ====================
1500
+ // ── OUTLINED ──────────────────────────────────────────────────────────
1501
+ // Non-toggle: transparent bg, border-outline, on-surface-variant fg
1502
+ // Toggle off: same as non-toggle
1503
+ // Toggle on (selected): inverse-surface bg, inverse-on-surface fg, no border
1504
+ // Disabled: border becomes on-surface/12 (set via Tailwind utility on root)
1330
1505
  {
1331
- variant: "tonal",
1332
- selected: true,
1333
- className: "bg-tertiary-container text-on-tertiary-container"
1506
+ variant: "outlined",
1507
+ color: "primary",
1508
+ className: [
1509
+ "[--ib-bg:transparent]",
1510
+ "[--ib-fg:var(--color-on-surface-variant)]",
1511
+ "[--ib-sl:var(--color-on-surface-variant)]",
1512
+ "[--ib-border:var(--color-outline)]",
1513
+ "[--ib-bg-off:transparent]",
1514
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1515
+ "[--ib-bg-on:var(--color-inverse-surface)]",
1516
+ "[--ib-fg-on:var(--color-inverse-on-surface)]",
1517
+ // Disabled outlined border
1518
+ "data-[disabled]:border-on-surface/12"
1519
+ ]
1334
1520
  },
1335
- // ====================
1336
- // OUTLINED VARIANTS (UNSELECTED)
1337
- // ====================
1338
1521
  {
1339
1522
  variant: "outlined",
1340
- selected: false,
1341
- className: "text-on-surface-variant"
1523
+ color: "secondary",
1524
+ className: [
1525
+ "[--ib-bg:transparent]",
1526
+ "[--ib-fg:var(--color-on-surface-variant)]",
1527
+ "[--ib-sl:var(--color-on-surface-variant)]",
1528
+ "[--ib-border:var(--color-outline)]",
1529
+ "[--ib-bg-off:transparent]",
1530
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1531
+ "[--ib-bg-on:var(--color-inverse-surface)]",
1532
+ "[--ib-fg-on:var(--color-inverse-on-surface)]",
1533
+ "data-[disabled]:border-on-surface/12"
1534
+ ]
1342
1535
  },
1343
- // ====================
1344
- // OUTLINED VARIANTS (SELECTED - uses inverse colors)
1345
- // ====================
1346
1536
  {
1347
1537
  variant: "outlined",
1348
- selected: true,
1349
- className: "bg-inverse-surface text-inverse-on-surface border-transparent"
1538
+ color: "tertiary",
1539
+ className: [
1540
+ "[--ib-bg:transparent]",
1541
+ "[--ib-fg:var(--color-on-surface-variant)]",
1542
+ "[--ib-sl:var(--color-on-surface-variant)]",
1543
+ "[--ib-border:var(--color-outline)]",
1544
+ "[--ib-bg-off:transparent]",
1545
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1546
+ "[--ib-bg-on:var(--color-inverse-surface)]",
1547
+ "[--ib-fg-on:var(--color-inverse-on-surface)]",
1548
+ "data-[disabled]:border-on-surface/12"
1549
+ ]
1550
+ },
1551
+ {
1552
+ variant: "outlined",
1553
+ color: "error",
1554
+ className: [
1555
+ "[--ib-bg:transparent]",
1556
+ "[--ib-fg:var(--color-on-surface-variant)]",
1557
+ "[--ib-sl:var(--color-on-surface-variant)]",
1558
+ "[--ib-border:var(--color-outline)]",
1559
+ "[--ib-bg-off:transparent]",
1560
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1561
+ "[--ib-bg-on:var(--color-inverse-surface)]",
1562
+ "[--ib-fg-on:var(--color-inverse-on-surface)]",
1563
+ "data-[disabled]:border-on-surface/12"
1564
+ ]
1350
1565
  }
1351
1566
  ],
1352
- /**
1353
- * Default variants
1354
- */
1355
1567
  defaultVariants: {
1356
1568
  variant: "standard",
1357
1569
  color: "primary",
1358
1570
  size: "medium",
1359
- selected: false,
1360
- isDisabled: false
1571
+ width: "default",
1572
+ shape: "round"
1573
+ }
1574
+ }
1575
+ );
1576
+ var iconButtonStateLayerVariants = classVarianceAuthority.cva([
1577
+ "absolute inset-0 rounded-[inherit] pointer-events-none opacity-0",
1578
+ "bg-[var(--ib-sl,currentColor)]",
1579
+ // Effects transition (opacity — no spatial overshoot)
1580
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
1581
+ // Interaction opacities (MD3: hover 8%, focus/pressed 10%)
1582
+ "group-data-[hovered]/icon-button:opacity-8",
1583
+ "group-data-[focus-visible]/icon-button:opacity-10",
1584
+ "group-data-[pressed]/icon-button:opacity-10",
1585
+ // No state layer when disabled
1586
+ "group-data-[disabled]/icon-button:hidden"
1587
+ ]);
1588
+ var iconButtonIconVariants = classVarianceAuthority.cva(
1589
+ [
1590
+ "relative z-10 inline-flex shrink-0 items-center justify-center",
1591
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects"
1592
+ ],
1593
+ {
1594
+ variants: {
1595
+ size: {
1596
+ xsmall: "size-5",
1597
+ // 20dp
1598
+ small: "size-6",
1599
+ // 24dp
1600
+ medium: "size-6",
1601
+ // 24dp
1602
+ large: "size-8",
1603
+ // 32dp
1604
+ xlarge: "size-10"
1605
+ // 40dp
1606
+ }
1607
+ },
1608
+ defaultVariants: {
1609
+ size: "medium"
1361
1610
  }
1362
1611
  }
1363
1612
  );
1364
1613
  var IconButton = React.forwardRef(
1365
1614
  ({
1366
- // Variant props (CVA)
1615
+ // Variant props (CVA / design-time)
1367
1616
  variant = "standard",
1368
1617
  color = "primary",
1369
1618
  size = "medium",
1619
+ width = "default",
1620
+ shape = "round",
1370
1621
  // IconButton specific props
1371
1622
  children,
1623
+ selectedIcon,
1372
1624
  value,
1373
1625
  selected,
1374
1626
  disableRipple = false,
1375
1627
  className,
1376
1628
  // React Aria props
1377
- isDisabled: propIsDisabled = false,
1629
+ isDisabled = false,
1378
1630
  onPress,
1379
1631
  onMouseDown,
1380
1632
  "aria-label": ariaLabel,
@@ -1393,7 +1645,8 @@ var IconButton = React.forwardRef(
1393
1645
  console.warn("[IconButton] IconButton should have an icon as children.");
1394
1646
  }
1395
1647
  }
1396
- const isDisabled = propIsDisabled;
1648
+ const isToggle = selected !== void 0;
1649
+ const isSelected = isToggle ? selected ?? false : false;
1397
1650
  const { onMouseDown: handleRipple, ripples } = useRipple({
1398
1651
  disabled: isDisabled || disableRipple
1399
1652
  });
@@ -1411,32 +1664,37 @@ var IconButton = React.forwardRef(
1411
1664
  ...getConnectedRadiusClasses(groupCtx, value),
1412
1665
  groupCtx.enforceMinWidth ? "min-w-12" : ""
1413
1666
  ] : [];
1667
+ const iconNode = isToggle && isSelected && selectedIcon ? selectedIcon : children;
1414
1668
  return /* @__PURE__ */ jsxRuntime.jsxs(
1415
1669
  IconButtonHeadless,
1416
1670
  {
1417
1671
  ref,
1418
1672
  className: cn(
1419
- // CVA variants includes btn-transition for asymmetric border-radius easing
1420
- iconButtonVariants({ variant, color, size, selected: selected ?? false, isDisabled }),
1421
- // Asymmetric border-radius easing: expressive when selected, decelerate when not.
1422
- // btn-transition-selected overrides --_btn-radius-easing to the bouncy spring while
1423
- // the button is gaining the pill shape; removal restores decelerate for the return
1424
- // path, preventing the overshoot-to-0px sharp-corner flash.
1673
+ // Root CVA — sets CSS role variables, dimensions, shape, transitions
1674
+ iconButtonRootVariants({ variant, color, size, width, shape }),
1675
+ // Group scope for child slot selectors
1676
+ "group/icon-button",
1677
+ // ButtonGroup asymmetric border-radius easing (connected selection morph)
1425
1678
  isGroupSelected ? "btn-transition-selected" : "",
1426
1679
  ...connectedClasses,
1427
- // User custom classes
1680
+ // Consumer custom classes
1428
1681
  className
1429
1682
  ),
1430
1683
  "aria-label": ariaLabel,
1684
+ isSelected,
1685
+ isToggle,
1431
1686
  "data-variant": variant,
1432
1687
  "data-color": color,
1688
+ "data-size": size,
1689
+ "data-width": width,
1690
+ "data-shape": shape,
1433
1691
  "data-group-selected": isGroupSelected ? "" : void 0,
1434
- ...selected !== void 0 && { selected },
1435
1692
  ...title && { title },
1436
1693
  ...mergedPropsValue,
1437
1694
  children: [
1695
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: iconButtonStateLayerVariants(), "aria-hidden": "true", "data-state-layer": "" }),
1438
1696
  ripples,
1439
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative z-10 inline-flex shrink-0", children })
1697
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: iconButtonIconVariants({ size }), "data-icon-slot": "", "aria-hidden": "true", children: iconNode })
1440
1698
  ]
1441
1699
  }
1442
1700
  );
@@ -1452,232 +1710,273 @@ var FABHeadless = React.forwardRef(
1452
1710
  type,
1453
1711
  "aria-label": ariaLabel,
1454
1712
  title,
1455
- ...props
1713
+ ...restProps
1456
1714
  }, forwardedRef) => {
1457
1715
  const internalRef = React.useRef(null);
1458
1716
  const ref = forwardedRef ?? internalRef;
1459
1717
  const { buttonProps } = reactAria.useButton(
1460
1718
  {
1461
- ...props,
1719
+ ...restProps,
1462
1720
  elementType: "button"
1463
1721
  },
1464
1722
  ref
1465
1723
  );
1466
- const domProps = utils.filterDOMProps(props);
1467
- const mergedProps = utils.mergeProps(buttonProps, domProps, {
1468
- tabIndex,
1469
- className,
1470
- onMouseDown,
1471
- type: type ?? "button",
1472
- "aria-label": ariaLabel,
1473
- // Add aria-label
1474
- // Add title if provided
1475
- ...title && { title }
1476
- });
1724
+ const {
1725
+ isDisabled: _isDisabled,
1726
+ onPress: _onPress,
1727
+ onPressStart: _onPressStart,
1728
+ onPressEnd: _onPressEnd,
1729
+ onPressChange: _onPressChange,
1730
+ onPressUp: _onPressUp,
1731
+ ...htmlAttrs
1732
+ } = restProps;
1733
+ const mergedProps = utils.mergeProps(
1734
+ buttonProps,
1735
+ {
1736
+ tabIndex,
1737
+ className,
1738
+ onMouseDown,
1739
+ "aria-label": ariaLabel,
1740
+ ...title !== void 0 && { title }
1741
+ },
1742
+ htmlAttrs
1743
+ );
1477
1744
  return (
1478
- // eslint-disable-next-line react/button-has-type
1479
- /* @__PURE__ */ jsxRuntime.jsx("button", { ...mergedProps, ref, children })
1745
+ // eslint-disable-next-line react/button-has-type -- type is dynamically passed from props
1746
+ /* @__PURE__ */ jsxRuntime.jsx("button", { ...mergedProps, ref, type: type ?? "button", children })
1480
1747
  );
1481
1748
  }
1482
1749
  );
1483
1750
  FABHeadless.displayName = "FABHeadless";
1484
1751
  var fabVariants = classVarianceAuthority.cva(
1485
1752
  [
1486
- // Base classes (always applied)
1487
- "relative inline-flex items-center justify-center cursor-pointer",
1488
- "overflow-hidden",
1489
- "transition-all duration-200",
1490
- "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
1753
+ // Layout NO overflow-hidden here (focus ring must extend outside)
1754
+ "relative inline-flex items-center justify-center cursor-pointer select-none",
1491
1755
  "shrink-0",
1492
- // Prevent shrinking in flex containers
1493
- // State layers (hover, focus, active)
1494
- "before:absolute before:inset-0 before:rounded-[inherit] before:transition-opacity before:duration-200",
1495
- "before:bg-current before:opacity-0",
1496
- "hover:before:opacity-8",
1497
- "focus-visible:before:opacity-12",
1498
- "active:before:opacity-12",
1499
- // Elevation (floating appearance)
1500
- "shadow-elevation-3",
1501
- // Default elevation
1502
- "hover:shadow-elevation-4"
1503
- // Hover elevation
1756
+ // Effects transition: color / bg / shadow — standard spring, no overshoot
1757
+ "transition-[color,background-color,box-shadow]",
1758
+ "duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
1759
+ // Disabled — self-targeting data-[x]: selectors (not group-data — these target the root itself)
1760
+ "data-[disabled]:bg-on-surface/12 data-[disabled]:text-on-surface/38",
1761
+ "data-[disabled]:shadow-none data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none"
1504
1762
  ],
1505
1763
  {
1506
1764
  variants: {
1507
1765
  /**
1508
- * FAB size (controls dimensions and icon size)
1766
+ * FAB size controls container dimensions, corner radius, and icon slot size.
1509
1767
  */
1510
1768
  size: {
1511
- small: [
1512
- "h-10 w-10",
1513
- // 40×40px
1514
- "p-2",
1515
- // 8px padding for 24px icon
1516
- "rounded-xl",
1517
- // 12px corner radius (not fully rounded!)
1518
- "m-1"
1519
- // 4px margin for 48×48px touch target
1520
- ],
1521
- medium: [
1769
+ /**
1770
+ * fab (default) — 56×56dp, 16dp corner radius.
1771
+ * MD3 standard FAB.
1772
+ */
1773
+ fab: [
1522
1774
  "h-14 w-14",
1523
- // 56×56px
1524
- "p-4",
1525
- // 16px padding for 24px icon
1775
+ // 56dp
1526
1776
  "rounded-2xl"
1527
- // 16px corner radius
1777
+ // 16dp corner
1528
1778
  ],
1779
+ /**
1780
+ * medium — 80×80dp, 20dp corner radius.
1781
+ * M3 Expressive Medium FAB. Previously this value mapped to 56dp;
1782
+ * it is now remapped to the Expressive 80dp Medium FAB.
1783
+ */
1784
+ medium: [
1785
+ "h-20 w-20",
1786
+ // 80dp
1787
+ "rounded-[20px]"
1788
+ // 20dp corner (large-increased shape token)
1789
+ ],
1790
+ /**
1791
+ * large — 96×96dp, 28dp corner radius.
1792
+ */
1529
1793
  large: [
1530
1794
  "h-24 w-24",
1531
- // 96×96px
1532
- "p-[30px]",
1533
- // 30px padding for 36px icon
1795
+ // 96dp
1534
1796
  "rounded-[28px]"
1535
- // 28px corner radius (custom value)
1797
+ // 28dp corner
1536
1798
  ],
1799
+ /**
1800
+ * extended — 56dp height, variable width, 16dp corner.
1801
+ * Icon + text label side by side.
1802
+ * Padding: 16dp leading (icon side), 20dp trailing (text side).
1803
+ * Gap: 12dp between icon and label (MD3 spec).
1804
+ */
1537
1805
  extended: [
1538
1806
  "h-14",
1539
- // 56px height (same as medium)
1807
+ // 56dp height
1540
1808
  "rounded-2xl",
1541
- // 16px corner radius
1809
+ // 16dp corner
1542
1810
  "pl-4 pr-5",
1543
- // Asymmetric padding: 16px leading, 20px trailing
1544
- "gap-2"
1545
- // 8px gap between icon and text
1811
+ // 16dp leading, 20dp trailing
1812
+ "gap-3"
1813
+ // 12dp gap between icon and label
1814
+ ],
1815
+ /**
1816
+ * @deprecated Use `fab` (56dp) instead.
1817
+ * small — 40×40dp, 12dp corner radius. No longer recommended in M3 Expressive.
1818
+ * Kept functional for backward compatibility.
1819
+ */
1820
+ small: [
1821
+ "h-10 w-10",
1822
+ // 40dp
1823
+ "rounded-xl",
1824
+ // 12dp corner
1825
+ "m-1"
1826
+ // 4dp margin for 48×48dp minimum touch target
1546
1827
  ]
1547
1828
  },
1548
1829
  /**
1549
- * Color scheme (MD3 color roles)
1830
+ * FAB color controls container + on-color.
1831
+ * State-layer color in fabStateLayerVariants must match icon/on-color.
1550
1832
  */
1551
1833
  color: {
1552
- primary: "",
1553
- secondary: "",
1554
- tertiary: "",
1555
- surface: ""
1556
- },
1557
- /**
1558
- * Disabled state
1559
- */
1560
- isDisabled: {
1561
- true: "pointer-events-none cursor-not-allowed !bg-on-surface/12 !text-on-surface/38 !shadow-none",
1562
- false: ""
1834
+ "primary-container": [
1835
+ "bg-primary-container text-on-primary-container",
1836
+ // Elevation base=3, hover=4 (state driven), disabled handled by root data-[disabled]
1837
+ "shadow-elevation-3",
1838
+ "group-data-[hovered]/fab:shadow-elevation-4",
1839
+ // Focus / pressed: return to elevation-3
1840
+ // Doubled attribute selector gives higher specificity than single hover selector
1841
+ "group-data-[focus-visible]/fab:shadow-elevation-3",
1842
+ "group-data-[pressed]/fab:group-data-[pressed]/fab:shadow-elevation-3"
1843
+ ],
1844
+ "secondary-container": [
1845
+ "bg-secondary-container text-on-secondary-container",
1846
+ "shadow-elevation-3",
1847
+ "group-data-[hovered]/fab:shadow-elevation-4",
1848
+ "group-data-[focus-visible]/fab:shadow-elevation-3",
1849
+ "group-data-[pressed]/fab:group-data-[pressed]/fab:shadow-elevation-3"
1850
+ ],
1851
+ "tertiary-container": [
1852
+ "bg-tertiary-container text-on-tertiary-container",
1853
+ "shadow-elevation-3",
1854
+ "group-data-[hovered]/fab:shadow-elevation-4",
1855
+ "group-data-[focus-visible]/fab:shadow-elevation-3",
1856
+ "group-data-[pressed]/fab:group-data-[pressed]/fab:shadow-elevation-3"
1857
+ ],
1858
+ primary: [
1859
+ "bg-primary text-on-primary",
1860
+ "shadow-elevation-3",
1861
+ "group-data-[hovered]/fab:shadow-elevation-4",
1862
+ "group-data-[focus-visible]/fab:shadow-elevation-3",
1863
+ "group-data-[pressed]/fab:group-data-[pressed]/fab:shadow-elevation-3"
1864
+ ],
1865
+ secondary: [
1866
+ "bg-secondary text-on-secondary",
1867
+ "shadow-elevation-3",
1868
+ "group-data-[hovered]/fab:shadow-elevation-4",
1869
+ "group-data-[focus-visible]/fab:shadow-elevation-3",
1870
+ "group-data-[pressed]/fab:group-data-[pressed]/fab:shadow-elevation-3"
1871
+ ],
1872
+ tertiary: [
1873
+ "bg-tertiary text-on-tertiary",
1874
+ "shadow-elevation-3",
1875
+ "group-data-[hovered]/fab:shadow-elevation-4",
1876
+ "group-data-[focus-visible]/fab:shadow-elevation-3",
1877
+ "group-data-[pressed]/fab:group-data-[pressed]/fab:shadow-elevation-3"
1878
+ ],
1879
+ /** @deprecated Use `primary-container` instead. */
1880
+ surface: [
1881
+ "bg-surface-container-high text-primary",
1882
+ "shadow-elevation-3",
1883
+ "group-data-[hovered]/fab:shadow-elevation-4",
1884
+ "group-data-[focus-visible]/fab:shadow-elevation-3",
1885
+ "group-data-[pressed]/fab:group-data-[pressed]/fab:shadow-elevation-3"
1886
+ ]
1563
1887
  }
1564
1888
  },
1565
- /**
1566
- * Compound variants - combinations of size + color
1567
- */
1568
- compoundVariants: [
1569
- // ====================
1570
- // PRIMARY COLOR
1571
- // ====================
1572
- {
1573
- color: "primary",
1574
- size: "small",
1575
- className: "bg-primary-container text-on-primary-container"
1576
- },
1577
- {
1578
- color: "primary",
1579
- size: "medium",
1580
- className: "bg-primary-container text-on-primary-container"
1581
- },
1582
- {
1583
- color: "primary",
1584
- size: "large",
1585
- className: "bg-primary-container text-on-primary-container"
1586
- },
1587
- {
1588
- color: "primary",
1589
- size: "extended",
1590
- className: "bg-primary-container text-on-primary-container"
1591
- },
1592
- // ====================
1593
- // SECONDARY COLOR
1594
- // ====================
1595
- {
1596
- color: "secondary",
1597
- size: "small",
1598
- className: "bg-secondary-container text-on-secondary-container"
1599
- },
1600
- {
1601
- color: "secondary",
1602
- size: "medium",
1603
- className: "bg-secondary-container text-on-secondary-container"
1604
- },
1605
- {
1606
- color: "secondary",
1607
- size: "large",
1608
- className: "bg-secondary-container text-on-secondary-container"
1609
- },
1610
- {
1611
- color: "secondary",
1612
- size: "extended",
1613
- className: "bg-secondary-container text-on-secondary-container"
1614
- },
1615
- // ====================
1616
- // TERTIARY COLOR
1617
- // ====================
1618
- {
1619
- color: "tertiary",
1620
- size: "small",
1621
- className: "bg-tertiary-container text-on-tertiary-container"
1622
- },
1623
- {
1624
- color: "tertiary",
1625
- size: "medium",
1626
- className: "bg-tertiary-container text-on-tertiary-container"
1627
- },
1628
- {
1629
- color: "tertiary",
1630
- size: "large",
1631
- className: "bg-tertiary-container text-on-tertiary-container"
1632
- },
1633
- {
1634
- color: "tertiary",
1635
- size: "extended",
1636
- className: "bg-tertiary-container text-on-tertiary-container"
1637
- },
1638
- // ====================
1639
- // SURFACE COLOR
1640
- // ====================
1641
- {
1642
- color: "surface",
1643
- size: "small",
1644
- className: "bg-surface text-primary"
1645
- },
1646
- {
1647
- color: "surface",
1648
- size: "medium",
1649
- className: "bg-surface text-primary"
1650
- },
1651
- {
1652
- color: "surface",
1653
- size: "large",
1654
- className: "bg-surface text-primary"
1889
+ defaultVariants: {
1890
+ size: "fab",
1891
+ color: "primary-container"
1892
+ }
1893
+ }
1894
+ );
1895
+ var fabStateLayerVariants = classVarianceAuthority.cva(
1896
+ [
1897
+ "absolute inset-0 rounded-[inherit] overflow-hidden pointer-events-none opacity-0",
1898
+ // Effects transition — opacity must not overshoot
1899
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
1900
+ // Hover: 8%
1901
+ "group-data-[hovered]/fab:opacity-8",
1902
+ // Focus: 10%
1903
+ "group-data-[focus-visible]/fab:opacity-10",
1904
+ // Pressed: 10% — doubled selector wins over hover's 8%
1905
+ "group-data-[pressed]/fab:group-data-[pressed]/fab:opacity-10",
1906
+ // No state layer when disabled
1907
+ "group-data-[disabled]/fab:hidden"
1908
+ ],
1909
+ {
1910
+ variants: {
1911
+ color: {
1912
+ "primary-container": "bg-on-primary-container",
1913
+ "secondary-container": "bg-on-secondary-container",
1914
+ "tertiary-container": "bg-on-tertiary-container",
1915
+ primary: "bg-on-primary",
1916
+ secondary: "bg-on-secondary",
1917
+ tertiary: "bg-on-tertiary",
1918
+ /** @deprecated */
1919
+ surface: "bg-primary"
1920
+ }
1921
+ },
1922
+ defaultVariants: { color: "primary-container" }
1923
+ }
1924
+ );
1925
+ var fabFocusRingVariants = classVarianceAuthority.cva([
1926
+ "pointer-events-none absolute inset-[-3px] rounded-[inherit]",
1927
+ "outline outline-2 outline-offset-0 outline-secondary",
1928
+ // Effects transition — opacity change must not overshoot
1929
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
1930
+ "opacity-0",
1931
+ "group-data-[focus-visible]/fab:opacity-100"
1932
+ ]);
1933
+ var fabIconVariants = classVarianceAuthority.cva(
1934
+ [
1935
+ "relative z-10 inline-flex shrink-0 items-center justify-center",
1936
+ // Color transition uses effects token (no spatial overshoot on color)
1937
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects"
1938
+ ],
1939
+ {
1940
+ variants: {
1941
+ size: {
1942
+ fab: "size-6",
1943
+ // 24dp
1944
+ medium: "size-7",
1945
+ // 28dp
1946
+ large: "size-9",
1947
+ // 36dp
1948
+ extended: "size-6",
1949
+ // 24dp
1950
+ small: "size-6"
1951
+ // 24dp
1655
1952
  },
1656
- {
1657
- color: "surface",
1658
- size: "extended",
1659
- className: "bg-surface text-primary"
1953
+ hidden: {
1954
+ true: "invisible",
1955
+ false: ""
1660
1956
  }
1661
- ],
1662
- /**
1663
- * Default variants
1664
- */
1957
+ },
1665
1958
  defaultVariants: {
1666
- size: "medium",
1667
- color: "primary",
1668
- isDisabled: false
1959
+ size: "fab",
1960
+ hidden: false
1669
1961
  }
1670
1962
  }
1671
1963
  );
1964
+ var fabLabelVariants = classVarianceAuthority.cva([
1965
+ "relative z-10 inline-flex items-center",
1966
+ "text-label-large tracking-[0.1px]"
1967
+ ]);
1672
1968
  var Spinner2 = () => /* @__PURE__ */ jsxRuntime.jsxs(
1673
1969
  "svg",
1674
1970
  {
1675
1971
  role: "progressbar",
1676
1972
  "aria-label": "Loading",
1677
- className: "h-6 w-6 animate-spin",
1973
+ className: "relative z-10 animate-spin",
1678
1974
  xmlns: "http://www.w3.org/2000/svg",
1679
1975
  fill: "none",
1680
1976
  viewBox: "0 0 24 24",
1977
+ width: "1em",
1978
+ height: "1em",
1979
+ style: { fontSize: "inherit" },
1681
1980
  children: [
1682
1981
  /* @__PURE__ */ jsxRuntime.jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
1683
1982
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1693,80 +1992,98 @@ var Spinner2 = () => /* @__PURE__ */ jsxRuntime.jsxs(
1693
1992
  );
1694
1993
  var FAB = React.forwardRef(
1695
1994
  ({
1696
- // Variant props (CVA)
1697
- size = "medium",
1698
- color = "primary",
1699
- // FAB specific props
1995
+ // Variant props
1996
+ size = "fab",
1997
+ color = "primary-container",
1998
+ // Content
1700
1999
  icon,
1701
2000
  children,
1702
- "aria-label": ariaLabel,
2001
+ // State
1703
2002
  loading = false,
1704
2003
  disableRipple = false,
2004
+ isDisabled = false,
2005
+ // Styling
1705
2006
  className,
1706
- // React Aria props
1707
- isDisabled: propIsDisabled = false,
1708
- onPress,
1709
- onMouseDown,
2007
+ // Accessibility
2008
+ "aria-label": ariaLabel,
1710
2009
  title,
2010
+ // Passthrough — forwarded to FABHeadless
2011
+ tabIndex = 0,
2012
+ type = "button",
2013
+ // Passed through to FABHeadless → useButton
1711
2014
  ...props
1712
2015
  }, ref) => {
2016
+ const fabRef = React.useRef(null);
2017
+ const resolvedRef = ref ?? fabRef;
2018
+ const isFABDisabled = isDisabled || loading;
2019
+ const [isPressed, setIsPressed] = React.useState(false);
2020
+ const handlePressStart = React.useCallback(() => setIsPressed(true), []);
2021
+ const handlePressEnd = React.useCallback(() => setIsPressed(false), []);
2022
+ const { isHovered, hoverProps } = reactAria.useHover({ isDisabled: isFABDisabled });
2023
+ const { isFocusVisible, focusProps } = reactAria.useFocusRing();
2024
+ const { onMouseDown: handleRipple, ripples } = useRipple({
2025
+ disabled: isFABDisabled || disableRipple
2026
+ });
1713
2027
  if (process.env.NODE_ENV === "development") {
1714
2028
  if (!icon) {
1715
- console.warn("[FAB] FAB must have an icon. Please provide the icon prop.");
2029
+ console.warn("[FAB] FAB must have an icon. Please provide the `icon` prop.");
1716
2030
  }
1717
2031
  if (size === "extended" && !children) {
1718
- console.warn("[FAB] Extended FAB requires text label as children.");
2032
+ console.warn("[FAB] Extended FAB requires a text label as `children`.");
1719
2033
  }
1720
2034
  if (size !== "extended" && children) {
1721
2035
  console.warn(
1722
- "[FAB] Children (text) is only used for extended FAB. For icon-only FAB, use icon prop only."
2036
+ "[FAB] `children` (text label) is only rendered for `size='extended'`. For icon-only FABs, use the `icon` prop only."
2037
+ );
2038
+ }
2039
+ if (size === "small") {
2040
+ console.warn(
2041
+ "[FAB] `size='small'` is deprecated in M3 Expressive. Use `size='fab'` (56dp) instead."
2042
+ );
2043
+ }
2044
+ if (color === "surface") {
2045
+ console.warn(
2046
+ "[FAB] `color='surface'` is deprecated in M3 Expressive. Use `color='primary-container'` instead."
1723
2047
  );
1724
2048
  }
1725
2049
  }
1726
- const isDisabled = propIsDisabled || loading;
1727
- const { onMouseDown: handleRipple, ripples } = useRipple({
1728
- disabled: isDisabled || disableRipple
1729
- });
1730
- const mergedOnMouseDown = (e) => {
1731
- onMouseDown?.(e);
1732
- handleRipple(e);
1733
- };
1734
- const mergedPropsValue = utils.mergeProps(props, {
1735
- ...onPress && { onPress },
1736
- onMouseDown: mergedOnMouseDown,
1737
- isDisabled
1738
- });
1739
2050
  return /* @__PURE__ */ jsxRuntime.jsxs(
1740
2051
  FABHeadless,
1741
2052
  {
1742
- ref,
2053
+ ...reactAria.mergeProps(
2054
+ hoverProps,
2055
+ focusProps,
2056
+ { onPressStart: handlePressStart, onPressEnd: handlePressEnd },
2057
+ props
2058
+ ),
2059
+ ref: resolvedRef,
2060
+ type,
2061
+ isDisabled: isFABDisabled,
2062
+ tabIndex,
2063
+ onMouseDown: handleRipple,
2064
+ "aria-label": ariaLabel,
2065
+ ...title !== void 0 && { title },
2066
+ ...getInteractionDataAttributes({
2067
+ isHovered,
2068
+ isFocusVisible,
2069
+ isPressed,
2070
+ isDisabled: isFABDisabled
2071
+ }),
2072
+ "data-with-icon": icon ? "" : void 0,
2073
+ "data-loading": loading ? "" : void 0,
1743
2074
  className: cn(
1744
- // Base classes
1745
- "relative inline-flex items-center justify-center",
1746
- "overflow-hidden transition-all duration-200",
1747
- "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
1748
- "shrink-0",
1749
- // State layers (hover, focus, active)
1750
- "before:absolute before:inset-0 before:rounded-[inherit] before:transition-opacity before:duration-200",
1751
- "before:bg-current before:opacity-0",
1752
- "hover:before:opacity-8",
1753
- "focus-visible:before:opacity-12",
1754
- "active:before:opacity-12",
1755
- // Elevation
1756
- "shadow-elevation-3 hover:shadow-elevation-4",
1757
- // CVA variants
1758
- fabVariants({ size, color, isDisabled }),
1759
- // User custom classes
2075
+ fabVariants({ size, color }),
2076
+ // group/fab: enables group-data-[x]/fab child selectors in all slots
2077
+ "group/fab",
1760
2078
  className
1761
2079
  ),
1762
- "aria-label": ariaLabel,
1763
- ...title && { title },
1764
- ...mergedPropsValue,
1765
2080
  children: [
1766
2081
  ripples,
1767
- icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("relative z-10 inline-flex shrink-0", loading && "invisible"), children: icon }),
1768
- loading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative z-10", children: /* @__PURE__ */ jsxRuntime.jsx(Spinner2, {}) }),
1769
- size === "extended" && children && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative z-10 inline-flex items-center text-sm font-medium tracking-[0.1px]", children })
2082
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(fabStateLayerVariants({ color })), "aria-hidden": "true" }),
2083
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(fabFocusRingVariants()), "aria-hidden": "true" }),
2084
+ icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(fabIconVariants({ size, hidden: loading })), children: icon }),
2085
+ loading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(fabIconVariants({ size })), children: /* @__PURE__ */ jsxRuntime.jsx(Spinner2, {}) }),
2086
+ size === "extended" && children && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(fabLabelVariants()), children })
1770
2087
  ]
1771
2088
  }
1772
2089
  );