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