@matteoaliano/forest-ui 1.2.4 → 1.2.5

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
@@ -222,7 +222,7 @@ __export(src_exports, {
222
222
  TextField: () => import_TextField.default,
223
223
  TextareaAutosize: () => import_TextareaAutosize.default,
224
224
  ToggleButton: () => ToggleButton,
225
- ToggleButtonGroup: () => import_ToggleButtonGroup.default,
225
+ ToggleButtonGroup: () => ToggleButtonGroup,
226
226
  Toolbar: () => import_Toolbar.default,
227
227
  Tooltip: () => Tooltip,
228
228
  Typography: () => import_Typography.default,
@@ -591,19 +591,13 @@ function buildComponents(tokens2) {
591
591
  }
592
592
  };
593
593
  const bevelHover = {
594
- borderTopWidth: 4,
595
- borderBottomWidth: 1,
596
- borderTopLeftRadius: radius4.lg,
597
- borderTopRightRadius: radius4.lg,
598
- borderBottomLeftRadius: radius4.md,
599
- borderBottomRightRadius: radius4.md
594
+ borderTopWidth: 3,
595
+ borderBottomWidth: 1
600
596
  };
601
597
  const bevelPress = {
602
598
  // Snap the press faster than the 300ms hover flip so it feels tactile.
603
599
  transition: "border-width 120ms ease, border-radius 120ms ease, opacity 120ms ease",
604
600
  borderTopWidth: 1,
605
- borderTopLeftRadius: radius4.md,
606
- borderTopRightRadius: radius4.md,
607
601
  opacity: 0.75
608
602
  };
609
603
  const bevel3D = {
@@ -614,9 +608,7 @@ function buildComponents(tokens2) {
614
608
  borderTopWidth: 1,
615
609
  borderRightWidth: 1,
616
610
  borderLeftWidth: 1,
617
- borderBottomWidth: 4,
618
- borderBottomLeftRadius: radius4.lg,
619
- borderBottomRightRadius: radius4.lg,
611
+ borderBottomWidth: 3,
620
612
  "&::before": {
621
613
  content: '""',
622
614
  position: "absolute",
@@ -653,6 +645,21 @@ function buildComponents(tokens2) {
653
645
  }
654
646
  }
655
647
  },
648
+ // ─── ButtonBase (global) ────────────────────────────────────────
649
+ // Kills MUI's TouchRipple everywhere in one place. Every clickable in the
650
+ // library renders through ButtonBase, so this reaches Button, IconButton,
651
+ // Fab, ToggleButton, CardActionArea, ListItemButton, MenuItem, Tab, Chip,
652
+ // PaginationItem, StepButton, BottomNavigationAction, SpeedDialAction and
653
+ // the Pickers' day/month/year cells without a per-component opt-out.
654
+ //
655
+ // The press feedback is the faux-3D bevel instead: the raised bottom edge
656
+ // collapses and the face dims on :active (see `bevelPress`). An expanding
657
+ // circle on top of that reads as a second, competing press affordance.
658
+ MuiButtonBase: {
659
+ defaultProps: {
660
+ disableRipple: true
661
+ }
662
+ },
656
663
  // ─── Button ─────────────────────────────────────────────────────
657
664
  MuiButton: {
658
665
  defaultProps: {
@@ -672,7 +679,7 @@ function buildComponents(tokens2) {
672
679
  const colorScales = { error: error2, warning: warning2, success: success2, info: info2 };
673
680
  const faceScale = ownerState.color ? colorScales[ownerState.color] : void 0;
674
681
  return {
675
- borderRadius: radius4.md,
682
+ borderRadius: radius4.lg,
676
683
  fontWeight: 600,
677
684
  boxShadow: tokenShadows.xs,
678
685
  height: 32,
@@ -690,12 +697,7 @@ function buildComponents(tokens2) {
690
697
  borderTopWidth: 1,
691
698
  borderRightWidth: 1,
692
699
  borderLeftWidth: 1,
693
- borderBottomWidth: 4,
694
- // Round the thick-bevel side more than the rest so the inner
695
- // surface isn't squared off. Follows the bevel: bottom at rest,
696
- // top on hover.
697
- borderBottomLeftRadius: radius4.lg,
698
- borderBottomRightRadius: radius4.lg,
700
+ borderBottomWidth: 3,
699
701
  // Contained buttons have no border of their own — give them a
700
702
  // translucent bevel. Outlined keeps its own border color.
701
703
  ...isContained && {
@@ -720,12 +722,8 @@ function buildComponents(tokens2) {
720
722
  transition: "top 500ms ease-in-out"
721
723
  },
722
724
  "&:hover": {
723
- borderTopWidth: 4,
725
+ borderTopWidth: 3,
724
726
  borderBottomWidth: 1,
725
- borderTopLeftRadius: radius4.lg,
726
- borderTopRightRadius: radius4.lg,
727
- borderBottomLeftRadius: radius4.md,
728
- borderBottomRightRadius: radius4.md,
729
727
  // One step lighter than the default bevel (60% → 70%) so it
730
728
  // stays visible against the brightened face on hover.
731
729
  ...isContained && {
@@ -752,8 +750,6 @@ function buildComponents(tokens2) {
752
750
  // like a tactile click rather than a slow settle.
753
751
  transition: "border-width 120ms ease, border-radius 120ms ease, opacity 120ms ease",
754
752
  borderTopWidth: 1,
755
- borderTopLeftRadius: radius4.md,
756
- borderTopRightRadius: radius4.md,
757
753
  opacity: 0.75
758
754
  },
759
755
  // Unified disabled treatment across the button family (Button,
@@ -768,7 +764,7 @@ function buildComponents(tokens2) {
768
764
  borderTopWidth: 1,
769
765
  borderRightWidth: 1,
770
766
  borderLeftWidth: 1,
771
- borderBottomWidth: 4
767
+ borderBottomWidth: 3
772
768
  },
773
769
  "@media (prefers-reduced-motion: reduce)": {
774
770
  transition: "none",
@@ -803,7 +799,7 @@ function buildComponents(tokens2) {
803
799
  },
804
800
  styleOverrides: {
805
801
  root: {
806
- borderRadius: radius4.md,
802
+ borderRadius: radius4.lg,
807
803
  // contained group → children already render as contained Buttons; only
808
804
  // re-pin the bevel border color, which MUI's own grouped-contained
809
805
  // default (palette grey) otherwise overrides away from the standalone
@@ -843,7 +839,7 @@ function buildComponents(tokens2) {
843
839
  backgroundColor: "transparent",
844
840
  color: buttonColors4.tertiaryColor.fg,
845
841
  borderWidth: 0,
846
- borderRadius: radius4.md,
842
+ borderRadius: radius4.lg,
847
843
  boxShadow: "none",
848
844
  "&::before": { display: "none" },
849
845
  "&:active": { opacity: 1 },
@@ -875,17 +871,17 @@ function buildComponents(tokens2) {
875
871
  MuiIconButton: {
876
872
  styleOverrides: {
877
873
  root: ({ theme }) => ({
878
- borderRadius: radius4.md,
874
+ borderRadius: radius4.lg,
879
875
  padding: 6,
880
876
  "& .MuiSvgIcon-root": {
881
877
  fontSize: 20
882
878
  },
883
879
  "&.ForestIconButton": {
884
880
  ...bevel3D,
885
- // Square 32×32 with border-box so the faux-3D bevel (1px top + 4px
881
+ // Square 32×32 with border-box so the faux-3D bevel (1px top + 3px
886
882
  // bottom, 1px left/right) is absorbed rather than added on top —
887
- // otherwise it rendered a 37×34 control instead of matching the
888
- // standalone Button's 32px height.
883
+ // otherwise it rendered an oversized control instead of matching
884
+ // the standalone Button's 32px height.
889
885
  height: 32,
890
886
  width: 32,
891
887
  boxSizing: "border-box",
@@ -942,8 +938,7 @@ function buildComponents(tokens2) {
942
938
  // ─── FAB ────────────────────────────────────────────────────────
943
939
  // A Fab is a round contained Button: same faux-3D bevel, light sweep,
944
940
  // brightness lift, and press. The border radii are left untouched so the
945
- // circular / extended silhouettes survive the bevel flip (no radius swap
946
- // like Button/IconButton do on their rounded corners).
941
+ // circular / extended silhouettes survive the bevel flip.
947
942
  MuiFab: {
948
943
  styleOverrides: {
949
944
  root: ({ theme, ownerState }) => ({
@@ -959,7 +954,7 @@ function buildComponents(tokens2) {
959
954
  borderTopWidth: 1,
960
955
  borderRightWidth: 1,
961
956
  borderLeftWidth: 1,
962
- borderBottomWidth: 4,
957
+ borderBottomWidth: 3,
963
958
  "&::before": {
964
959
  content: '""',
965
960
  position: "absolute",
@@ -977,7 +972,7 @@ function buildComponents(tokens2) {
977
972
  },
978
973
  "&:hover": {
979
974
  boxShadow: tokenShadows.xs,
980
- borderTopWidth: 4,
975
+ borderTopWidth: 3,
981
976
  borderBottomWidth: 1,
982
977
  // Light mode: dark face, brightness lift reads as hover. Dark
983
978
  // mode: the face is already light — a lift blows out the top
@@ -1192,7 +1187,10 @@ function buildComponents(tokens2) {
1192
1187
  MuiAutocomplete: {
1193
1188
  styleOverrides: {
1194
1189
  paper: {
1195
- borderRadius: radius4.md,
1190
+ // No borderRadius here on purpose — it inherits MuiPaper's `rounded`
1191
+ // (radius.lg), so the popup matches Menu, Popover and every other
1192
+ // Paper surface. It used to set radius.md and was the one popup in
1193
+ // the system with a sharper corner.
1196
1194
  boxShadow: tokenShadows.lg,
1197
1195
  border: `1px solid ${border4.secondary}`,
1198
1196
  marginTop: 4
@@ -1206,10 +1204,42 @@ function buildComponents(tokens2) {
1206
1204
  lineHeight: "20px"
1207
1205
  }
1208
1206
  },
1207
+ // MUI hardcodes this slot's own padding/marginRight in its source
1208
+ // (padding: 2, marginRight: -2), which wins over the global
1209
+ // MuiIconButton root override at equal specificity — left alone, the
1210
+ // popup toggle renders as a small 24px button that doesn't fill the
1211
+ // 32px input row and stops short of the edge. Restated here at the
1212
+ // same size/inset as the DatePicker's open-picker button (32px,
1213
+ // padding 6, flush against the input's 12px right padding) so both
1214
+ // end-of-input icon buttons match.
1215
+ popupIndicator: {
1216
+ padding: 6,
1217
+ // Not a straight copy of the DatePicker's -12px: that value cancels
1218
+ // the input row's 12px padding directly, but this button's
1219
+ // grandparent (`.MuiAutocomplete-endAdornment`) is itself inset 9px
1220
+ // from the input's true right edge by MUI's own variant styles.
1221
+ // -9px reaches through that inset to land flush, matching the
1222
+ // DatePicker's open-picker button exactly.
1223
+ marginRight: -9
1224
+ },
1225
+ // Same MUI-hardcoded-slot problem as popupIndicator, one size down
1226
+ // (padding: 4, marginRight: -2) — sized to match it here. The
1227
+ // popupIndicator's own marginRight anchors it to the input edge
1228
+ // independent of clearIndicator's box, so this needs no matching
1229
+ // offset: with both buttons the same size, 0 sits them edge-to-edge,
1230
+ // and their equal padding already reads as the gap between the icons.
1231
+ clearIndicator: {
1232
+ padding: 6,
1233
+ marginRight: 0
1234
+ },
1209
1235
  option: {
1210
1236
  fontSize: 14,
1211
1237
  lineHeight: "20px",
1212
- padding: "6px 12px",
1238
+ // An Autocomplete option is a plain <li class="MuiAutocomplete-option">,
1239
+ // not a MenuItem, so MuiMenuItem's radius never reaches it. Match it
1240
+ // here so the hover/selected highlight reads as the same pill a Select
1241
+ // draws, inset by the listbox's 4px padding.
1242
+ borderRadius: radius4.md,
1213
1243
  // Hover + selected backgrounds come from MUI's theme-aware action
1214
1244
  // overlays (a light overlay on dark, so they stay visible — a solid
1215
1245
  // dark-grey tier here reads as "no hover"). Selected keeps the text
@@ -1219,7 +1249,14 @@ function buildComponents(tokens2) {
1219
1249
  }
1220
1250
  },
1221
1251
  listbox: {
1222
- padding: 4
1252
+ padding: 4,
1253
+ // Padding has to live here, not in the `option` slot: MUI nests its
1254
+ // own option styles under the listbox, so a rule from `option` ties
1255
+ // on specificity and loses on source order. This selector outranks
1256
+ // it. 6px 8px matches MuiMenuItem.
1257
+ "& .MuiAutocomplete-option": {
1258
+ padding: "6px 8px"
1259
+ }
1223
1260
  }
1224
1261
  }
1225
1262
  },
@@ -1231,7 +1268,7 @@ function buildComponents(tokens2) {
1231
1268
  styleOverrides: {
1232
1269
  root: {
1233
1270
  color: border4.primary,
1234
- borderRadius: radius4.xs,
1271
+ borderRadius: radius4.sm,
1235
1272
  padding: 0,
1236
1273
  width: 20,
1237
1274
  height: 20,
@@ -1248,7 +1285,7 @@ function buildComponents(tokens2) {
1248
1285
  "&.Mui-focusVisible": {
1249
1286
  outline: "none",
1250
1287
  boxShadow: focusRings4.brand,
1251
- borderRadius: radius4.xs
1288
+ borderRadius: radius4.sm
1252
1289
  },
1253
1290
  "&.Mui-disabled": {
1254
1291
  color: border4.primary
@@ -1384,13 +1421,13 @@ function buildComponents(tokens2) {
1384
1421
  color: buttonColors4.primary.fg,
1385
1422
  borderTopWidth: 1,
1386
1423
  borderBottomWidth: 1,
1387
- borderTopLeftRadius: radius4.md,
1388
- borderTopRightRadius: radius4.md,
1389
- borderBottomLeftRadius: radius4.md,
1390
- borderBottomRightRadius: radius4.md,
1424
+ borderTopLeftRadius: radius4.lg,
1425
+ borderTopRightRadius: radius4.lg,
1426
+ borderBottomLeftRadius: radius4.lg,
1427
+ borderBottomRightRadius: radius4.lg,
1391
1428
  opacity: 0.75,
1392
1429
  // The pressed-in cue is carried by the flattened bevel (bottom
1393
- // border 4px -> 1px) alone in light mode. On the dark-mode face
1430
+ // border 3px -> 1px) alone in light mode. On the dark-mode face
1394
1431
  // that geometry reads too faintly, so it keeps the inner shadow.
1395
1432
  boxShadow: "none",
1396
1433
  ...theme.applyStyles("dark", {
@@ -1403,8 +1440,8 @@ function buildComponents(tokens2) {
1403
1440
  filter: "none",
1404
1441
  borderTopWidth: 1,
1405
1442
  borderBottomWidth: 1,
1406
- borderTopLeftRadius: radius4.md,
1407
- borderTopRightRadius: radius4.md
1443
+ borderTopLeftRadius: radius4.lg,
1444
+ borderTopRightRadius: radius4.lg
1408
1445
  }
1409
1446
  },
1410
1447
  // variant="outlined" (Forest wrapper class): the outlined <Button> /
@@ -1448,7 +1485,7 @@ function buildComponents(tokens2) {
1448
1485
  borderTopWidth: 1,
1449
1486
  borderRightWidth: 1,
1450
1487
  borderLeftWidth: 1,
1451
- borderBottomWidth: 4,
1488
+ borderBottomWidth: 3,
1452
1489
  "&::before": { display: "none" },
1453
1490
  // selected+disabled keeps the pressed-in cue, greyed.
1454
1491
  "&.Mui-selected": {
@@ -1465,7 +1502,7 @@ function buildComponents(tokens2) {
1465
1502
  MuiToggleButtonGroup: {
1466
1503
  styleOverrides: {
1467
1504
  root: {
1468
- borderRadius: radius4.md,
1505
+ borderRadius: radius4.lg,
1469
1506
  overflow: "hidden"
1470
1507
  },
1471
1508
  // The child ToggleButton's selected/hover/active rules set per-corner
@@ -1482,14 +1519,14 @@ function buildComponents(tokens2) {
1482
1519
  },
1483
1520
  "&:first-of-type": {
1484
1521
  "&, &:hover, &:active, &.Mui-selected": {
1485
- borderTopLeftRadius: radius4.md,
1486
- borderBottomLeftRadius: radius4.md
1522
+ borderTopLeftRadius: radius4.lg,
1523
+ borderBottomLeftRadius: radius4.lg
1487
1524
  }
1488
1525
  },
1489
1526
  "&:last-of-type": {
1490
1527
  "&, &:hover, &:active, &.Mui-selected": {
1491
- borderTopRightRadius: radius4.md,
1492
- borderBottomRightRadius: radius4.md
1528
+ borderTopRightRadius: radius4.lg,
1529
+ borderBottomRightRadius: radius4.lg
1493
1530
  }
1494
1531
  }
1495
1532
  }
@@ -1515,7 +1552,27 @@ function buildComponents(tokens2) {
1515
1552
  border: `1px solid ${border4.secondary}`,
1516
1553
  backgroundColor: bg4.primary,
1517
1554
  boxShadow: tokenShadows.xs,
1518
- overflow: "hidden"
1555
+ overflow: "hidden",
1556
+ // Which surface a wrapped CardActionArea should paint. The action
1557
+ // area can't read the Card's own `background-color` (the Card goes
1558
+ // transparent when it wraps one, see below), so the colour is handed
1559
+ // down as a custom property instead. `<Card color="secondary">`
1560
+ // overrides it alongside its own background.
1561
+ "--forest-card-surface": bg4.primary,
1562
+ // A Card wrapping a CardActionArea hands its whole frame over: the
1563
+ // action area already draws the border, surface, radius and shadow,
1564
+ // and flips the radius on hover. Keeping the Card's own stacks a
1565
+ // second, static border just outside the moving one.
1566
+ "&:has(> .MuiCardActionArea-root)": {
1567
+ border: "none",
1568
+ borderRadius: 0,
1569
+ backgroundColor: "transparent",
1570
+ boxShadow: "none",
1571
+ // The action area clips its own content (it has overflow: hidden
1572
+ // for the glaze), so the Card must not also clip — its square
1573
+ // corners would cut the action area's rounded ones.
1574
+ overflow: "visible"
1575
+ }
1519
1576
  }
1520
1577
  }
1521
1578
  },
@@ -2613,52 +2670,67 @@ function buildComponents(tokens2) {
2613
2670
  }
2614
2671
  },
2615
2672
  // ─── CardActionArea ─────────────────────────────────────────────
2616
- // Clickable-card affordance: mirrors the Button's faux-3D bevel (thick edge
2617
- // that flips bottom->top on hover), light glaze sweep, border-color shift,
2618
- // and press-to-sink. This is what visually marks a card as clickable use
2619
- // <Card><CardActionArea>…</CardActionArea></Card> for any interactive card.
2673
+ // Clickable-card affordance: the outlined Button's interaction at card
2674
+ // scale. Same faux-3D bevel (thick edge that flips bottom->top on hover),
2675
+ // same glaze sweep, same brightness darken on hover, same press-to-sink
2676
+ // the only difference is the radius, which stays on the card steps
2677
+ // (xl / 2xl) rather than the button's (md / lg). Use
2678
+ // <Card><CardActionArea>…</CardActionArea></Card> for any interactive card;
2679
+ // the Card hands over its border and surface when it sees one.
2620
2680
  MuiCardActionArea: {
2621
2681
  styleOverrides: {
2622
2682
  root: {
2623
2683
  position: "relative",
2624
2684
  overflow: "hidden",
2625
- borderRadius: radius4.xl,
2685
+ borderRadius: radius4["2xl"],
2626
2686
  borderStyle: "solid",
2627
- borderColor: border4.primary,
2687
+ // The token the outlined Button reads, not `border.primary`, so the
2688
+ // two stay identical under any preset.
2689
+ borderColor: buttonColors4.secondaryColor.border,
2690
+ // Painted here rather than on the Card, which goes transparent so its
2691
+ // square box can't clip these corners. Falls back to the default card
2692
+ // surface when a CardActionArea is used outside a Card.
2693
+ backgroundColor: `var(--forest-card-surface, ${bg4.primary})`,
2628
2694
  borderTopWidth: 1,
2629
2695
  borderRightWidth: 1,
2630
2696
  borderLeftWidth: 1,
2631
- borderBottomWidth: 4,
2632
- borderBottomLeftRadius: radius4["2xl"],
2633
- borderBottomRightRadius: radius4["2xl"],
2697
+ borderBottomWidth: 3,
2634
2698
  boxShadow: tokenShadows.xs,
2635
- transition: "box-shadow 300ms ease, border-width 300ms ease, border-radius 300ms ease, border-color 300ms ease, opacity 300ms ease",
2636
- // Moving glaze — a soft light streak that sweeps top->bottom on hover.
2699
+ transition: "filter 300ms ease, border-width 300ms ease, border-radius 300ms ease, opacity 300ms ease, background-color 300ms ease",
2700
+ "&:focus-visible": {
2701
+ boxShadow: focusRings4.brandShadowXs
2702
+ },
2703
+ // Moving glaze — a soft light streak that sweeps top->bottom on
2704
+ // hover. Same parameters as the outlined Button's sweep.
2637
2705
  "&::before": {
2638
2706
  content: '""',
2639
2707
  position: "absolute",
2640
2708
  left: 0,
2641
2709
  top: "-150%",
2642
2710
  width: "100%",
2643
- height: 6,
2711
+ height: 5,
2644
2712
  borderRadius: 4,
2645
- backgroundColor: "color-mix(in srgb, currentColor 40%, #fff)",
2646
- boxShadow: "0 0 28px 6px color-mix(in srgb, currentColor 40%, #fff)",
2647
- opacity: 0.35,
2713
+ backgroundColor: "color-mix(in srgb, currentColor 45%, #fff)",
2714
+ boxShadow: "0 0 24px 4px color-mix(in srgb, currentColor 45%, #fff)",
2715
+ opacity: 0.2,
2648
2716
  filter: "blur(3px)",
2649
2717
  pointerEvents: "none",
2650
- transition: "top 600ms ease-in-out",
2718
+ transition: "top 500ms ease-in-out",
2651
2719
  zIndex: 1
2652
2720
  },
2653
2721
  "&:hover": {
2654
- borderColor: border4.brand,
2655
- borderTopWidth: 4,
2656
- borderBottomWidth: 1,
2657
- borderTopLeftRadius: radius4["2xl"],
2658
- borderTopRightRadius: radius4["2xl"],
2659
- borderBottomLeftRadius: radius4.xl,
2660
- borderBottomRightRadius: radius4.xl,
2661
- boxShadow: tokenShadows.lg
2722
+ // Darken the whole surface via brightness, the same mechanism the
2723
+ // outlined Button uses — no border-color shift; the border stays
2724
+ // put and only the bevel geometry moves. Lighter than the Button's
2725
+ // 0.97 on purpose: the filter dims a card's media and copy along
2726
+ // with its surface, so the same amount reads much heavier here.
2727
+ filter: "brightness(0.995)",
2728
+ // The Button drops its resting shadow on hover (MUI's
2729
+ // disableElevation), so the raised edge is the only depth cue
2730
+ // mid-interaction. Matched here for parity.
2731
+ boxShadow: "none",
2732
+ borderTopWidth: 3,
2733
+ borderBottomWidth: 1
2662
2734
  },
2663
2735
  "&:hover::before": {
2664
2736
  top: "150%"
@@ -2668,15 +2740,13 @@ function buildComponents(tokens2) {
2668
2740
  "&:active": {
2669
2741
  transition: "border-width 120ms ease, border-radius 120ms ease, opacity 120ms ease",
2670
2742
  borderTopWidth: 1,
2671
- borderTopLeftRadius: radius4.xl,
2672
- borderTopRightRadius: radius4.xl,
2673
- opacity: 0.85
2674
- },
2675
- "&:hover .MuiCardActionArea-focusHighlight": {
2676
- opacity: 0.04
2743
+ opacity: 0.75
2677
2744
  },
2678
- "&.Mui-focusVisible .MuiCardActionArea-focusHighlight": {
2679
- opacity: 0.08
2745
+ // MUI's own hover/focus tint overlay. The outlined Button has no
2746
+ // equivalent (it suppresses the variant hover background), so the
2747
+ // brightness darken above is the only hover feedback.
2748
+ "& .MuiCardActionArea-focusHighlight": {
2749
+ opacity: 0
2680
2750
  },
2681
2751
  "@media (prefers-reduced-motion: reduce)": {
2682
2752
  transition: "none",
@@ -3434,8 +3504,8 @@ var container = {
3434
3504
  paddingMobile: 16,
3435
3505
  // Corner radius of the App Shell's content surface where it meets the
3436
3506
  // chrome. Deliberately a layout value rather than a step on the `radius`
3437
- // scale: that scale governs components (and the sharper alkemy+ preset caps
3438
- // it at 12), so a 24 sitting in it would misread as a component radius.
3507
+ // scale: that scale governs components (and the alkemy+ preset caps it at
3508
+ // 22), so a 24 sitting in it would misread as a component radius.
3439
3509
  shellRadius: 24
3440
3510
  };
3441
3511
  var widths = {
@@ -3702,15 +3772,15 @@ var alpha = {
3702
3772
  var highlight = magenta;
3703
3773
  var radius2 = {
3704
3774
  ...radius,
3705
- xxs: 1,
3706
- xs: 2,
3707
- sm: 3,
3708
- md: 4,
3709
- lg: 5,
3710
- xl: 6,
3711
- "2xl": 8,
3712
- "3xl": 10,
3713
- "4xl": 12
3775
+ xxs: 2,
3776
+ xs: 4,
3777
+ sm: 6,
3778
+ md: 8,
3779
+ lg: 11,
3780
+ xl: 14,
3781
+ "2xl": 16,
3782
+ "3xl": 20,
3783
+ "4xl": 22
3714
3784
  };
3715
3785
  var text2 = {
3716
3786
  primary: gray[50],
@@ -4025,15 +4095,15 @@ var colors = {
4025
4095
  };
4026
4096
  var radius3 = {
4027
4097
  ...radius,
4028
- xxs: 1,
4029
- xs: 2,
4030
- sm: 3,
4031
- md: 4,
4032
- lg: 5,
4033
- xl: 6,
4034
- "2xl": 8,
4035
- "3xl": 10,
4036
- "4xl": 12
4098
+ xxs: 2,
4099
+ xs: 4,
4100
+ sm: 6,
4101
+ md: 8,
4102
+ lg: 11,
4103
+ xl: 14,
4104
+ "2xl": 16,
4105
+ "3xl": 20,
4106
+ "4xl": 22
4037
4107
  };
4038
4108
  var text3 = {
4039
4109
  primary: gray[900],
@@ -4367,7 +4437,7 @@ function Button({ variant = "contained", ...props }) {
4367
4437
  // src/components/ButtonGroup/index.tsx
4368
4438
  var import_ButtonGroup = __toESM(require("@mui/material/ButtonGroup"));
4369
4439
  var import_jsx_runtime3 = require("react/jsx-runtime");
4370
- function ButtonGroup({ variant = "contained", ...props }) {
4440
+ function ButtonGroup({ variant = "outlined", ...props }) {
4371
4441
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ButtonGroup.default, { variant, ...props });
4372
4442
  }
4373
4443
 
@@ -5119,15 +5189,28 @@ var Logo = (0, import_react3.forwardRef)(
5119
5189
  Logo.displayName = "Logo";
5120
5190
 
5121
5191
  // src/components/ToggleButton/index.tsx
5192
+ var import_react4 = require("react");
5122
5193
  var import_ToggleButton = __toESM(require("@mui/material/ToggleButton"));
5123
5194
  var import_ToggleButtonGroup = __toESM(require("@mui/material/ToggleButtonGroup"));
5124
5195
  var import_jsx_runtime10 = require("react/jsx-runtime");
5196
+ var ToggleButtonVariantContext = (0, import_react4.createContext)(
5197
+ void 0
5198
+ );
5199
+ function ToggleButtonGroup({
5200
+ variant = "outlined",
5201
+ ...props
5202
+ }) {
5203
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ToggleButtonVariantContext.Provider, { value: variant, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ToggleButtonGroup.default, { ...props }) });
5204
+ }
5125
5205
  function ToggleButton({
5126
- variant = "contained",
5206
+ variant,
5127
5207
  className,
5128
5208
  ...props
5129
5209
  }) {
5130
- const cls = variant === "outlined" ? ["ForestToggleButton-outlined", className].filter(Boolean).join(" ") : className;
5210
+ var _a;
5211
+ const groupVariant = (0, import_react4.useContext)(ToggleButtonVariantContext);
5212
+ const resolved = (_a = variant != null ? variant : groupVariant) != null ? _a : "outlined";
5213
+ const cls = resolved === "outlined" ? ["ForestToggleButton-outlined", className].filter(Boolean).join(" ") : className;
5131
5214
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ToggleButton.default, { className: cls, ...props });
5132
5215
  }
5133
5216
 
@@ -5360,7 +5443,12 @@ function Card({ color = "primary", sx, ...props }) {
5360
5443
  {
5361
5444
  ...props,
5362
5445
  sx: [
5363
- color === "secondary" && { backgroundColor: "background.paper" },
5446
+ color === "secondary" && {
5447
+ backgroundColor: "background.paper",
5448
+ // Also hand the surface to a wrapped CardActionArea, which paints the
5449
+ // card's background itself (the Card goes transparent around one).
5450
+ "--forest-card-surface": "var(--mui-palette-background-paper)"
5451
+ },
5364
5452
  ...Array.isArray(sx) ? sx : [sx]
5365
5453
  ]
5366
5454
  }
@@ -5475,7 +5563,7 @@ var import_Tab = __toESM(require("@mui/material/Tab"));
5475
5563
  var import_TabScrollButton = __toESM(require("@mui/material/TabScrollButton"));
5476
5564
 
5477
5565
  // src/components/Sidebar/index.tsx
5478
- var import_react4 = require("react");
5566
+ var import_react5 = require("react");
5479
5567
  var import_Drawer2 = __toESM(require("@mui/material/Drawer"));
5480
5568
  var import_Box = __toESM(require("@mui/material/Box"));
5481
5569
  var import_ChevronLeftOutlined = __toESM(require("@mui/icons-material/ChevronLeftOutlined"));
@@ -5485,14 +5573,15 @@ var import_ListItemIcon2 = __toESM(require("@mui/material/ListItemIcon"));
5485
5573
  var import_ListItemText3 = __toESM(require("@mui/material/ListItemText"));
5486
5574
  var import_Tooltip2 = __toESM(require("@mui/material/Tooltip"));
5487
5575
  var import_jsx_runtime18 = require("react/jsx-runtime");
5488
- var SidebarContext = (0, import_react4.createContext)({
5576
+ var SidebarContext = (0, import_react5.createContext)({
5489
5577
  open: true,
5490
5578
  collapsedWidth: 48,
5491
5579
  expandedWidth: 240
5492
5580
  });
5493
5581
  function useSidebar() {
5494
- return (0, import_react4.useContext)(SidebarContext);
5582
+ return (0, import_react5.useContext)(SidebarContext);
5495
5583
  }
5584
+ var TOGGLE_SIZE = 28;
5496
5585
  function SidebarNav({
5497
5586
  open,
5498
5587
  onOpenChange,
@@ -5503,6 +5592,7 @@ function SidebarNav({
5503
5592
  children,
5504
5593
  header,
5505
5594
  footer,
5595
+ toggleEdgeAllowance = 12,
5506
5596
  sx,
5507
5597
  ...rest
5508
5598
  }) {
@@ -5510,6 +5600,60 @@ function SidebarNav({
5510
5600
  const drawerWidth = open ? expandedWidth : collapsedWidth;
5511
5601
  const layoutWidth = isHover ? collapsedWidth : drawerWidth;
5512
5602
  const shouldShowToggle = showToggle != null ? showToggle : !isHover;
5603
+ const wrapperRef = (0, import_react5.useRef)(null);
5604
+ const toggleRef = (0, import_react5.useRef)(null);
5605
+ const edgeYRef = (0, import_react5.useRef)(null);
5606
+ const [edgeActive, setEdgeActive] = (0, import_react5.useState)(false);
5607
+ const [toggleFocused, setToggleFocused] = (0, import_react5.useState)(false);
5608
+ const writeY = (y) => {
5609
+ edgeYRef.current = y;
5610
+ if (y === null) return;
5611
+ const el = toggleRef.current;
5612
+ if (!el) return;
5613
+ el.style.top = "0px";
5614
+ el.style.transform = `translate3d(50%, ${y - TOGGLE_SIZE / 2}px, 0)`;
5615
+ };
5616
+ const resetY = () => {
5617
+ edgeYRef.current = null;
5618
+ const el = toggleRef.current;
5619
+ if (!el) return;
5620
+ el.style.top = "";
5621
+ el.style.transform = "";
5622
+ };
5623
+ (0, import_react5.useEffect)(() => {
5624
+ if (!shouldShowToggle) return;
5625
+ const onMove = (e) => {
5626
+ const el = wrapperRef.current;
5627
+ if (!el) return;
5628
+ const r = el.getBoundingClientRect();
5629
+ const withinBand = Math.abs(e.clientX - r.right) <= toggleEdgeAllowance && e.clientY >= r.top && e.clientY <= r.bottom;
5630
+ const half = TOGGLE_SIZE / 2;
5631
+ const cur = edgeYRef.current;
5632
+ const overToggle = cur !== null && Math.abs(e.clientX - r.right) <= half && Math.abs(e.clientY - (r.top + cur)) <= half;
5633
+ let next = null;
5634
+ if (withinBand || overToggle) {
5635
+ const max = Math.max(r.height - half, half);
5636
+ next = Math.min(Math.max(e.clientY - r.top, half), max);
5637
+ }
5638
+ const prev = edgeYRef.current;
5639
+ if (prev === next) return;
5640
+ if (prev !== null && next !== null && Math.abs(prev - next) < 0.5) return;
5641
+ writeY(next);
5642
+ if (prev === null !== (next === null)) setEdgeActive(next !== null);
5643
+ };
5644
+ const onLeave = () => {
5645
+ if (edgeYRef.current === null) return;
5646
+ writeY(null);
5647
+ setEdgeActive(false);
5648
+ };
5649
+ window.addEventListener("pointermove", onMove, { passive: true });
5650
+ document.addEventListener("pointerleave", onLeave);
5651
+ return () => {
5652
+ window.removeEventListener("pointermove", onMove);
5653
+ document.removeEventListener("pointerleave", onLeave);
5654
+ };
5655
+ }, [shouldShowToggle, toggleEdgeAllowance]);
5656
+ const toggleVisible = edgeActive || toggleFocused;
5513
5657
  const hoverHandlers = isHover ? {
5514
5658
  onMouseEnter: () => onOpenChange(true),
5515
5659
  onMouseLeave: () => onOpenChange(false)
@@ -5517,6 +5661,7 @@ function SidebarNav({
5517
5661
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SidebarContext.Provider, { value: { open, collapsedWidth, expandedWidth }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
5518
5662
  import_Box.default,
5519
5663
  {
5664
+ ref: wrapperRef,
5520
5665
  sx: {
5521
5666
  position: "relative",
5522
5667
  width: layoutWidth,
@@ -5580,50 +5725,94 @@ function SidebarNav({
5580
5725
  )
5581
5726
  }
5582
5727
  ),
5583
- shouldShowToggle && // The forest Button carries the faux-3D bevel/glaze/press, sized down
5584
- // to a square icon toggle. Face is the tonal secondary (light grey in
5585
- // light / charcoal in dark), not the primary brand fill — the toggle
5586
- // is a utility control, not a call to action.
5728
+ shouldShowToggle && // Positioning shell: pinned to the wrapper's top-right, moved down the
5729
+ // edge with a transform written by the pointermove handler (see
5730
+ // `writeY`). Only opacity transitions position snaps to the pointer
5731
+ // so the toggle appears under the cursor and tracks it 1:1.
5587
5732
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5588
- Button,
5733
+ import_Box.default,
5589
5734
  {
5590
- "aria-label": open ? "Collapse sidebar" : "Expand sidebar",
5591
- onClick: () => onOpenChange(!open),
5735
+ ref: toggleRef,
5592
5736
  sx: (theme) => ({
5593
5737
  position: "absolute",
5594
- top: "50%",
5595
5738
  right: 0,
5596
- transform: "translate(50%, -50%)",
5739
+ width: TOGGLE_SIZE,
5740
+ height: TOGGLE_SIZE,
5741
+ // Resting spot for keyboard focus and touch, where no pointer
5742
+ // sets a transform: vertically centred on the edge. `writeY`
5743
+ // overrides both `top` and `transform` inline while tracking
5744
+ // and leaves them in place on hide so the exit fades in situ.
5745
+ top: "50%",
5746
+ transform: "translate3d(50%, -50%, 0)",
5747
+ // Hidden until the pointer reaches the edge band. Kept mounted so
5748
+ // it stays in the tab order and can be reached by keyboard.
5749
+ opacity: toggleVisible ? 1 : 0,
5750
+ pointerEvents: toggleVisible ? "auto" : "none",
5751
+ // Enter fast with a strong ease-out so it reads as "already
5752
+ // there"; exit a touch shorter — the user has moved on.
5753
+ transition: toggleVisible ? "opacity 120ms cubic-bezier(0.19, 1, 0.22, 1)" : "opacity 100ms ease",
5754
+ "@media (prefers-reduced-motion: reduce)": {
5755
+ transition: "none"
5756
+ },
5757
+ // No hover on touch: there is no pointer to bring the toggle out,
5758
+ // so it stays visible at its resting position.
5759
+ "@media (hover: none)": {
5760
+ opacity: 1,
5761
+ pointerEvents: "auto"
5762
+ },
5597
5763
  // Above both the AppShell content surface (drawer + 1, which
5598
5764
  // paints its shadow over the sidebar) and the hover-expanded
5599
5765
  // drawer (drawer + 2) — the toggle straddles the seam and has
5600
5766
  // to stay clickable and whole on top of both.
5601
- zIndex: theme.zIndex.drawer + 3,
5602
- minWidth: 0,
5603
- width: 28,
5604
- height: 28,
5605
- padding: 0,
5606
- // Match the shell surface (bg.primary = lightest grey) so the
5607
- // toggle reads as part of the sidebar, not a darker chip.
5608
- backgroundColor: `var(--mui-palette-bg-primary, ${bg.primary})`,
5609
- color: `var(--mui-palette-text-primary, ${text.primary})`,
5610
- // Light-grey standard border instead of the contained bevel's
5611
- // darker greyscale (fg.tertiary) — the toggle is a light utility
5612
- // control, so the heavier border read too dark.
5613
- borderColor: `var(--mui-palette-border-primary, ${border.primary})`,
5614
- "& .MuiSvgIcon-root": {
5615
- fontSize: 18
5616
- },
5617
- "&:hover": {
5618
- backgroundColor: `var(--mui-palette-bg-quaternary, ${bg.quaternary})`,
5619
- // The contained brightness lift assumes a dark face — on the
5620
- // light-grey light-mode face it would blow out. In dark the
5621
- // hover token matches the face, so a small lift is the cue.
5622
- filter: "none",
5623
- ...theme.applyStyles("dark", { filter: "brightness(1.15)" })
5624
- }
5767
+ zIndex: theme.zIndex.drawer + 3
5625
5768
  }),
5626
- children: open ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_ChevronLeftOutlined.default, {}) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_ChevronRightOutlined.default, {})
5769
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5770
+ Button,
5771
+ {
5772
+ "aria-label": open ? "Collapse sidebar" : "Expand sidebar",
5773
+ onClick: (e) => {
5774
+ onOpenChange(!open);
5775
+ writeY(null);
5776
+ setEdgeActive(false);
5777
+ if (!e.currentTarget.matches(":focus-visible")) {
5778
+ e.currentTarget.blur();
5779
+ }
5780
+ },
5781
+ onFocus: (e) => {
5782
+ const kb = e.currentTarget.matches(":focus-visible");
5783
+ if (kb) resetY();
5784
+ setToggleFocused(kb);
5785
+ },
5786
+ onBlur: () => setToggleFocused(false),
5787
+ sx: (theme) => ({
5788
+ minWidth: 0,
5789
+ width: TOGGLE_SIZE,
5790
+ height: TOGGLE_SIZE,
5791
+ padding: 0,
5792
+ transition: "background-color 300ms ease",
5793
+ // Match the shell surface (bg.primary = lightest grey) so the
5794
+ // toggle reads as part of the sidebar, not a darker chip.
5795
+ backgroundColor: `var(--mui-palette-bg-primary, ${bg.primary})`,
5796
+ color: `var(--mui-palette-text-primary, ${text.primary})`,
5797
+ // Light-grey standard border instead of the contained bevel's
5798
+ // darker greyscale (fg.tertiary) — the toggle is a light
5799
+ // utility control, so the heavier border read too dark.
5800
+ borderColor: `var(--mui-palette-border-primary, ${border.primary})`,
5801
+ "& .MuiSvgIcon-root": {
5802
+ fontSize: 18
5803
+ },
5804
+ "&:hover": {
5805
+ backgroundColor: `var(--mui-palette-bg-quaternary, ${bg.quaternary})`,
5806
+ // The contained brightness lift assumes a dark face — on the
5807
+ // light-grey light-mode face it would blow out. In dark the
5808
+ // hover token matches the face, so a small lift is the cue.
5809
+ filter: "none",
5810
+ ...theme.applyStyles("dark", { filter: "brightness(1.15)" })
5811
+ }
5812
+ }),
5813
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_ChevronLeftOutlined.default, {}) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_ChevronRightOutlined.default, {})
5814
+ }
5815
+ )
5627
5816
  }
5628
5817
  )
5629
5818
  ]
@@ -5631,8 +5820,9 @@ function SidebarNav({
5631
5820
  ) });
5632
5821
  }
5633
5822
  var itemSx = {
5634
- // Preset radius.md (4px) same corner radius as buttons and inputs.
5635
- borderRadius: "4px",
5823
+ // Off-scale on purpose: the alkemy+ preset's radius steps run 1→12, so 11px
5824
+ // sits between "3xl" (10) and "4xl" (12) with no token to name it.
5825
+ borderRadius: "11px",
5636
5826
  // The MuiListItemButton override sets 12px; 8px here absorbs the container's
5637
5827
  // new 8px gutter so the icon column stays exactly 16px from the sidebar edge,
5638
5828
  // where it has always been.
@@ -5698,7 +5888,7 @@ function SidebarItem({
5698
5888
  }
5699
5889
 
5700
5890
  // src/components/AppShell/index.tsx
5701
- var import_react5 = require("react");
5891
+ var import_react6 = require("react");
5702
5892
  var import_Box2 = __toESM(require("@mui/material/Box"));
5703
5893
  var import_Drawer3 = __toESM(require("@mui/material/Drawer"));
5704
5894
  var import_List2 = __toESM(require("@mui/material/List"));
@@ -5749,8 +5939,8 @@ function AppShell({
5749
5939
  }) {
5750
5940
  const theme = (0, import_styles4.useTheme)();
5751
5941
  const isMobile = (0, import_useMediaQuery.default)(theme.breakpoints.down("sm"));
5752
- const [sidebarOpen, setSidebarOpen] = (0, import_react5.useState)(defaultSidebarOpen);
5753
- const [mobileOpen, setMobileOpen] = (0, import_react5.useState)(false);
5942
+ const [sidebarOpen, setSidebarOpen] = (0, import_react6.useState)(defaultSidebarOpen);
5943
+ const [mobileOpen, setMobileOpen] = (0, import_react6.useState)(false);
5754
5944
  const navContent = nav != null ? nav : navItems && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_List2.default, { children: navItems.map(({ label, icon, active, endAdornment, ...link }) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5755
5945
  SidebarItem,
5756
5946
  {