@tinybigui/react 0.21.1 → 0.23.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
@@ -467,6 +467,10 @@ var buttonVariants = cva(
467
467
  * Filled/Tonal hover→level-1, focus/pressed→level-0
468
468
  * Elevated base→level-1, hover→level-2, focus/pressed→level-1
469
469
  * Outlined/Text no elevation
470
+ *
471
+ * Self-targeting `data-[x]:` is used for elevation because these classes
472
+ * sit on the root element (the group host) — group-data descendant
473
+ * selectors cannot match an element against itself.
470
474
  */
471
475
  variant: {
472
476
  /**
@@ -475,17 +479,17 @@ var buttonVariants = cva(
475
479
  * Elevation: 0 base → 1 hover → 0 focus → 0 pressed
476
480
  */
477
481
  filled: [
478
- "bg-primary text-on-primary shadow-none",
482
+ "text-on-primary shadow-none",
479
483
  // Hover: gains level-1 elevation
480
- "group-data-[hovered]/button:shadow-elevation-1",
484
+ "data-[hovered]:shadow-elevation-1",
481
485
  // Focus/pressed: shadow must explicitly return to level-0
482
486
  // (doubled attribute selector → higher specificity than hover)
483
- "group-data-[focus-visible]/button:shadow-none",
484
- "group-data-[pressed]/button:group-data-[pressed]/button:shadow-none",
485
- // Disabled overrides
486
- "group-data-[disabled]/button:bg-on-surface/12",
487
- "group-data-[disabled]/button:text-on-surface/38",
488
- "group-data-[disabled]/button:shadow-none"
487
+ "data-[focus-visible]:data-[focus-visible]:shadow-none",
488
+ "data-[pressed]:data-[pressed]:data-[pressed]:shadow-none",
489
+ // Disabled overrides — root owns text color + shadow only;
490
+ // disabled bg override lives on buttonContainerVariants (filled variant)
491
+ "data-[disabled]:text-on-surface/38",
492
+ "data-[disabled]:shadow-none"
489
493
  ],
490
494
  /**
491
495
  * Outlined — medium emphasis. Transparent with border.
@@ -493,10 +497,10 @@ var buttonVariants = cva(
493
497
  * Elevation: always 0
494
498
  */
495
499
  outlined: [
496
- "bg-transparent border border-outline text-primary",
500
+ "border border-outline text-primary",
497
501
  // Disabled overrides
498
- "group-data-[disabled]/button:border-on-surface/12",
499
- "group-data-[disabled]/button:text-on-surface/38"
502
+ "data-[disabled]:border-on-surface/12",
503
+ "data-[disabled]:text-on-surface/38"
500
504
  ],
501
505
  /**
502
506
  * Tonal — secondary emphasis.
@@ -504,16 +508,15 @@ var buttonVariants = cva(
504
508
  * Elevation: 0 base → 1 hover → 0 focus → 0 pressed
505
509
  */
506
510
  tonal: [
507
- "bg-secondary-container text-on-secondary-container shadow-none",
511
+ "text-on-secondary-container shadow-none",
508
512
  // Hover: gains level-1 elevation (same as filled)
509
- "group-data-[hovered]/button:shadow-elevation-1",
513
+ "data-[hovered]:shadow-elevation-1",
510
514
  // Focus/pressed: return to level-0
511
- "group-data-[focus-visible]/button:shadow-none",
512
- "group-data-[pressed]/button:group-data-[pressed]/button:shadow-none",
515
+ "data-[focus-visible]:data-[focus-visible]:shadow-none",
516
+ "data-[pressed]:data-[pressed]:data-[pressed]:shadow-none",
513
517
  // Disabled overrides
514
- "group-data-[disabled]/button:bg-on-surface/12",
515
- "group-data-[disabled]/button:text-on-surface/38",
516
- "group-data-[disabled]/button:shadow-none"
518
+ "data-[disabled]:text-on-surface/38",
519
+ "data-[disabled]:shadow-none"
517
520
  ],
518
521
  /**
519
522
  * Elevated — separation via shadow.
@@ -521,17 +524,16 @@ var buttonVariants = cva(
521
524
  * Elevation: 1 base → 2 hover → 1 focus → 1 pressed
522
525
  */
523
526
  elevated: [
524
- "bg-surface-container-low text-primary shadow-elevation-1",
527
+ "text-primary shadow-elevation-1",
525
528
  // Hover: gains extra elevation
526
- "group-data-[hovered]/button:shadow-elevation-2",
529
+ "data-[hovered]:shadow-elevation-2",
527
530
  // Focus/pressed: return to base level-1
528
531
  // (doubled selector wins over single hover selector at same cascade position)
529
- "group-data-[focus-visible]/button:shadow-elevation-1",
530
- "group-data-[pressed]/button:group-data-[pressed]/button:shadow-elevation-1",
532
+ "data-[focus-visible]:data-[focus-visible]:shadow-elevation-1",
533
+ "data-[pressed]:data-[pressed]:data-[pressed]:shadow-elevation-1",
531
534
  // Disabled overrides
532
- "group-data-[disabled]/button:bg-on-surface/12",
533
- "group-data-[disabled]/button:text-on-surface/38",
534
- "group-data-[disabled]/button:shadow-none"
535
+ "data-[disabled]:text-on-surface/38",
536
+ "data-[disabled]:shadow-none"
535
537
  ],
536
538
  /**
537
539
  * Text — lowest emphasis.
@@ -539,9 +541,9 @@ var buttonVariants = cva(
539
541
  * Elevation: always 0
540
542
  */
541
543
  text: [
542
- "bg-transparent text-primary",
544
+ "text-primary",
543
545
  // Disabled overrides
544
- "group-data-[disabled]/button:text-on-surface/38"
546
+ "data-[disabled]:text-on-surface/38"
545
547
  ]
546
548
  },
547
549
  /**
@@ -579,6 +581,29 @@ var buttonVariants = cva(
579
581
  }
580
582
  }
581
583
  );
584
+ var buttonContainerVariants = cva(
585
+ [
586
+ "absolute inset-0 rounded-[inherit] pointer-events-none",
587
+ // Effects transition for background-color — no overshoot
588
+ "transition-[background-color] duration-spring-standard-fast-effects ease-spring-standard-fast-effects"
589
+ ],
590
+ {
591
+ variants: {
592
+ variant: {
593
+ // MD3 disabled: filled containers replace bg with on-surface/12.
594
+ // group-data descendant selector targets this child span (not the root host).
595
+ filled: "bg-primary group-data-[disabled]/button:bg-on-surface/12",
596
+ // outlined/text: container stays transparent when disabled — only border + label fade.
597
+ outlined: "bg-transparent",
598
+ // MD3 disabled: tonal and elevated containers also replace bg with on-surface/12.
599
+ tonal: "bg-secondary-container group-data-[disabled]/button:bg-on-surface/12",
600
+ elevated: "bg-surface-container-low group-data-[disabled]/button:bg-on-surface/12",
601
+ text: "bg-transparent"
602
+ }
603
+ },
604
+ defaultVariants: { variant: "filled" }
605
+ }
606
+ );
582
607
  var buttonStateLayerVariants = cva(
583
608
  [
584
609
  "absolute inset-0 rounded-[inherit] overflow-hidden pointer-events-none opacity-0",
@@ -881,6 +906,7 @@ var Button = forwardRef(
881
906
  className
882
907
  ),
883
908
  children: [
909
+ /* @__PURE__ */ jsx("span", { className: cn(buttonContainerVariants({ variant })), "aria-hidden": "true" }),
884
910
  ripples,
885
911
  /* @__PURE__ */ jsx("span", { className: cn(buttonStateLayerVariants({ variant })), "aria-hidden": "true" }),
886
912
  /* @__PURE__ */ jsx("span", { className: cn(buttonFocusRingVariants()), "aria-hidden": "true" }),
@@ -5947,293 +5973,526 @@ var CardActions = forwardRef(function CardActions2({ children, className }, ref)
5947
5973
  return /* @__PURE__ */ jsx("div", { ref, className: cn("flex items-center justify-end gap-2 p-4 pt-0", className), children });
5948
5974
  });
5949
5975
  CardActions.displayName = "CardActions";
5950
- var MenuContext = createContext(null);
5951
- function useMenuContext() {
5952
- return useContext(MenuContext);
5953
- }
5954
- function TriggerBridge({ children }) {
5955
- const ctx = useSlottedContext(ButtonContext);
5956
- const localRef = useRef(null);
5957
- const { ref: contextRef, ...ctxProps } = ctx ?? {};
5958
- const mergedCallbackRef = useCallback(
5959
- (node) => {
5960
- localRef.current = node;
5961
- if (!contextRef) return;
5962
- if (typeof contextRef === "function") {
5963
- contextRef(node);
5964
- } else {
5965
- contextRef.current = node;
5966
- }
5967
- },
5968
- [contextRef]
5969
- );
5970
- const { buttonProps } = useButton({ ...ctxProps, elementType: "button" }, localRef);
5971
- if (!isValidElement(children)) return /* @__PURE__ */ jsx(Fragment, { children });
5972
- return cloneElement(
5973
- children,
5974
- { ...buttonProps, ref: mergedCallbackRef }
5975
- );
5976
- }
5977
- function HeadlessMenuTrigger({
5978
- children,
5979
- placement = "bottom start",
5980
- shouldFlip = true,
5981
- ...rest
5982
- }) {
5983
- const childrenArray = Array.isArray(children) ? children : [children];
5984
- const [triggerChild, menuChild] = childrenArray;
5985
- return /* @__PURE__ */ jsxs(MenuTrigger$1, { ...rest, children: [
5986
- /* @__PURE__ */ jsx(TriggerBridge, { children: triggerChild }),
5987
- /* @__PURE__ */ jsx(Popover, { placement, shouldFlip, offset: 4, children: menuChild })
5988
- ] });
5989
- }
5990
- function HeadlessMenu({
5991
- className,
5992
- children,
5993
- "aria-label": ariaLabel,
5994
- ...props
5995
- }) {
5996
- const menuRef = useRef(null);
5997
- useLayoutEffect(() => {
5998
- if (ariaLabel && menuRef.current) {
5999
- menuRef.current.removeAttribute("aria-labelledby");
6000
- }
6001
- });
6002
- return /* @__PURE__ */ jsx(
6003
- Menu$1,
6004
- {
6005
- ...props,
6006
- ref: menuRef,
6007
- ...ariaLabel !== void 0 ? { "aria-label": ariaLabel } : {},
6008
- className: className ?? "",
6009
- children
6010
- }
6011
- );
6012
- }
6013
- HeadlessMenuTrigger.Menu = HeadlessMenu;
6014
- var HeadlessMenuItem = forwardRef(
6015
- function HeadlessMenuItem2({ children, className, ...props }, ref) {
6016
- return /* @__PURE__ */ jsx(MenuItem$1, { ...props, ref, className: className ?? "", children });
6017
- }
6018
- );
6019
- function HeadlessMenuSection({
6020
- children,
6021
- "aria-label": ariaLabel,
6022
- className
6023
- }) {
6024
- return /* @__PURE__ */ jsx(
6025
- MenuSection$1,
6026
- {
6027
- ...ariaLabel !== void 0 ? { "aria-label": ariaLabel } : {},
6028
- className: className ?? "",
6029
- children
6030
- }
6031
- );
6032
- }
6033
- function HeadlessMenuDivider({
6034
- className,
6035
- ...props
6036
- }) {
6037
- return /* @__PURE__ */ jsx(Separator, { ...props, className: className ?? "" });
6038
- }
6039
5976
  var menuContainerVariants = cva(
6040
5977
  [
6041
- // Elevation
6042
- "shadow-elevation-2",
6043
- // Width constraints per MD3 spec (112dp min / 280dp max)
5978
+ // Width constraints: 112dp min / 280dp max per MD3 spec
6044
5979
  "min-w-28 max-w-70",
6045
- // Layout
6046
- "py-2",
6047
- // Scroll: show scrollbar when content overflows; max height avoids clipping
6048
- "overflow-y-auto",
5980
+ "flex flex-col",
5981
+ // Scroll behaviour
6049
5982
  "max-h-[calc(var(--visual-viewport-height,100vh)-2rem)]",
6050
5983
  // Stacking
6051
5984
  "z-50",
6052
- // Focus outline handled by React Aria
6053
- "outline-none",
6054
- // GPU compositing — promotes menu to its own compositor layer so
6055
- // scale + opacity animations run without triggering layout reflow.
6056
- "will-change-[transform,opacity]",
6057
- // Pointer events blocked during animation to prevent accidental clicks
6058
- // on menu items while the panel is still animating in or out.
6059
- "data-[entering]:pointer-events-none data-[exiting]:pointer-events-none",
6060
- // ── Enter animation ────────────────────────────────────────────────────
6061
- // @keyframes menu-enter (defined in styles.css): scale(0.8)+opacity:0 →
6062
- // scale(1)+opacity:1 in 120ms with cubic-bezier(0,0,0.2,1) (standard
6063
- // decelerate — matches Angular Material's _mat-menu-enter keyframe).
6064
- "data-[entering]:animate-[menu-enter_120ms_cubic-bezier(0,0,0.2,1)_both]",
6065
- // ── Exit animation ─────────────────────────────────────────────────────
6066
- // @keyframes menu-exit (defined in styles.css): opacity:1 → opacity:0
6067
- // in 100ms after 25ms delay, linear — matches Angular Material's
6068
- // _mat-menu-exit keyframe (fade-only, no reverse scale).
6069
- "data-[exiting]:animate-[menu-exit_100ms_25ms_linear_both]",
6070
- // ── Transform origin (placement-aware) ────────────────────────────────
6071
- // RAC sets data-placement="bottom|top|left|right" on the Popover element.
6072
- // Default (bottom): origin at top edge (menu expands downward).
6073
- "origin-top",
6074
- // top: origin at bottom edge (menu expands upward)
6075
- "data-[placement=top]:origin-bottom",
6076
- // left: origin at right edge
6077
- "data-[placement=left]:origin-right",
6078
- // right: origin at left edge
6079
- "data-[placement=right]:origin-left",
6080
- // ── Reduced motion ────────────────────────────────────────────────────
6081
- // Skip both animations entirely for users who prefer reduced motion.
6082
- "motion-reduce:data-[entering]:animate-none motion-reduce:data-[exiting]:animate-none"
5985
+ "gap-0.5",
5986
+ // Focus outline delegated to React Aria
5987
+ "outline-none"
6083
5988
  ],
6084
5989
  {
6085
5990
  variants: {
6086
5991
  /**
6087
- * Color scheme — drives the container background.
6088
- * baseline+standard uses a separate compound variant.
5992
+ * Color scheme — drives item/segment background and content colors.
5993
+ * standard: surface-container-low item background.
5994
+ * vibrant: tertiary-container item background.
6089
5995
  */
6090
5996
  colorScheme: {
6091
5997
  standard: [],
6092
5998
  vibrant: []
6093
5999
  },
6094
6000
  /**
6095
- * Visual style — drives corner radius and baseline vs vertical background.
6001
+ * Visual style — drives corner radius and container background.
6002
+ *
6003
+ * baseline: solid surface-container, 4dp corners, 8dp vertical padding.
6004
+ * vertical: transparent container, 16dp corners, no container padding —
6005
+ * items own their segment surface, gaps reveal the page background.
6096
6006
  */
6097
6007
  menuStyle: {
6098
- baseline: ["rounded-xs", "bg-surface-container"],
6099
- vertical: ["rounded-lg", "bg-surface-container-low"]
6008
+ baseline: ["rounded-xs", "bg-surface-container", "py-2"],
6009
+ vertical: ["bg-transparent"]
6100
6010
  }
6101
6011
  },
6102
- compoundVariants: [
6103
- // Vertical + vibrant: tertiary container background
6104
- {
6105
- menuStyle: "vertical",
6106
- colorScheme: "vibrant",
6107
- class: ["bg-tertiary-container"]
6108
- }
6109
- ],
6110
6012
  defaultVariants: {
6111
6013
  colorScheme: "standard",
6112
6014
  menuStyle: "baseline"
6113
6015
  }
6114
6016
  }
6115
6017
  );
6018
+ var menuPopoverVariants = cva([
6019
+ "will-change-[transform,opacity]",
6020
+ "data-[entering]:pointer-events-none data-[exiting]:pointer-events-none",
6021
+ "data-[entering]:animate-md-scale-in",
6022
+ "data-[exiting]:animate-md-scale-out",
6023
+ "origin-top",
6024
+ "data-[placement=top]:origin-bottom",
6025
+ "data-[placement=left]:origin-right",
6026
+ "data-[placement=right]:origin-left",
6027
+ "motion-reduce:data-[entering]:animate-none motion-reduce:data-[exiting]:animate-none"
6028
+ ]);
6116
6029
  var menuItemVariants = cva(
6117
6030
  [
6118
6031
  // Layout — height set by density context in MenuItem component
6032
+ // gap is style-specific: baseline = 12dp (gap-3), vertical = 8dp (gap-2)
6119
6033
  "relative flex w-full items-center",
6120
- "px-3 gap-3",
6121
- // Typography: Body Large per MD3 baseline spec
6122
- "text-body-large",
6034
+ // Typography: Label Large per MD3 menu spec
6035
+ "text-label-large",
6123
6036
  // Interaction
6124
6037
  "cursor-pointer select-none outline-none",
6125
- // State layer pseudo-element
6126
- "before:absolute before:inset-0 before:rounded-[inherit]",
6127
- "before:transition-opacity before:duration-short2 before:ease-standard",
6128
- "before:opacity-0",
6129
- // Hover state layer
6130
- "hover:before:opacity-8",
6131
- // Focus visible state layer
6132
- "focus-visible:before:opacity-12",
6133
- // Active pressed state layer
6134
- "active:before:opacity-12",
6135
- // Color transition for selection
6136
- "transition-colors duration-short2 ease-standard"
6038
+ // Color transition (effects — no overshoot)
6039
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
6040
+ // Disabled — self-targeting data-[x]: selectors (RAC emits data-disabled)
6041
+ "data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed"
6137
6042
  ],
6138
6043
  {
6139
6044
  variants: {
6140
6045
  /**
6141
- * Disabled state: reduces opacity and blocks interaction.
6046
+ * Color scheme drives item bg, default text/icon color, and selection colors.
6047
+ *
6048
+ * standard: surface-container-low bg, on-surface text, on-surface-variant icons.
6049
+ * Selected/open: tertiary-container bg highlight, on-tertiary-container content.
6050
+ * vibrant: tertiary-container bg, on-tertiary-container text AND icons.
6051
+ * Selected/open: tertiary bg highlight, on-tertiary content.
6142
6052
  */
6143
- isDisabled: {
6144
- true: ["opacity-38 cursor-not-allowed pointer-events-none"],
6145
- false: []
6053
+ colorScheme: {
6054
+ standard: ["text-on-surface"],
6055
+ vibrant: ["text-on-tertiary-container"]
6146
6056
  },
6147
6057
  /**
6148
- * Selected state: background and text color driven by compound variants.
6058
+ * Visual style drives padding, gap, segment surface, and corner rounding.
6059
+ *
6060
+ * baseline: 12dp h-padding, 12dp icon-to-label gap, no item background (container provides it).
6061
+ * vertical: 12dp h-padding, 8dp icon-to-label gap, item owns segment surface, segmented rounding
6062
+ * via first/last + adjacent-sibling gap selectors.
6149
6063
  */
6150
- isSelected: {
6151
- true: [],
6152
- false: []
6064
+ menuStyle: {
6065
+ baseline: ["px-3", "gap-3"],
6066
+ vertical: [
6067
+ "px-3",
6068
+ "gap-2",
6069
+ // Default: inner item (4dp all corners)
6070
+ "rounded-md"
6071
+ // Last item in the whole menu → 16dp bottom corners
6072
+ ]
6073
+ }
6074
+ },
6075
+ compoundVariants: [
6076
+ // vertical + standard: selected/open text → on-tertiary-container
6077
+ {
6078
+ menuStyle: "vertical",
6079
+ colorScheme: "standard",
6080
+ class: [
6081
+ "data-[selected]:text-on-tertiary-container",
6082
+ "data-[open]:text-on-tertiary-container"
6083
+ ]
6084
+ },
6085
+ // vertical + vibrant: selected/open text → on-tertiary
6086
+ {
6087
+ menuStyle: "vertical",
6088
+ colorScheme: "vibrant",
6089
+ class: ["data-[selected]:text-on-tertiary", "data-[open]:text-on-tertiary"]
6090
+ }
6091
+ ],
6092
+ defaultVariants: {
6093
+ colorScheme: "standard",
6094
+ menuStyle: "baseline"
6095
+ }
6096
+ }
6097
+ );
6098
+ var menuItemHighlightVariants = cva(
6099
+ [
6100
+ "absolute inset-0 pointer-events-none",
6101
+ // Inherit the item's own corner radius (inner 4dp or outer 16dp)
6102
+ "rounded-[inherit]",
6103
+ // Effects transition for background-color — no overshoot
6104
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
6105
+ // z-0: below state layer (z-[1]) and content (z-10)
6106
+ "z-0"
6107
+ ],
6108
+ {
6109
+ variants: {
6110
+ menuStyle: {
6111
+ baseline: [],
6112
+ vertical: []
6153
6113
  },
6154
- /**
6155
- * Color scheme: drives default text and state layer colors.
6156
- * - standard: on-surface text, on-surface state layer
6157
- * - vibrant (vertical only): on-tertiary-container text + state layer
6158
- */
6159
6114
  colorScheme: {
6160
- standard: ["text-on-surface", "before:bg-on-surface"],
6161
- vibrant: ["text-on-tertiary-container", "before:bg-on-tertiary-container"]
6115
+ standard: [
6116
+ // baseline selected bg
6117
+ "group-data-[selected]/menuitem:bg-surface-container-highest"
6118
+ ],
6119
+ vibrant: [
6120
+ // baseline + vibrant: use surface-container-highest as fallback
6121
+ "group-data-[selected]/menuitem:bg-surface-container-highest"
6122
+ ]
6123
+ }
6124
+ },
6125
+ compoundVariants: [
6126
+ // vertical + standard: selected/open highlight → tertiary-container
6127
+ {
6128
+ menuStyle: "vertical",
6129
+ colorScheme: "standard",
6130
+ class: [
6131
+ "group-data-[selected]/menuitem:bg-tertiary-container",
6132
+ "group-data-[open]/menuitem:bg-tertiary-container"
6133
+ ]
6134
+ },
6135
+ // vertical + vibrant: selected/open highlight → tertiary
6136
+ {
6137
+ menuStyle: "vertical",
6138
+ colorScheme: "vibrant",
6139
+ class: [
6140
+ "group-data-[selected]/menuitem:bg-tertiary",
6141
+ "group-data-[open]/menuitem:bg-tertiary"
6142
+ ]
6143
+ }
6144
+ ],
6145
+ defaultVariants: {
6146
+ menuStyle: "baseline",
6147
+ colorScheme: "standard"
6148
+ }
6149
+ }
6150
+ );
6151
+ var menuItemStateLayerVariants = cva(
6152
+ [
6153
+ "absolute inset-0 rounded-[inherit] overflow-hidden pointer-events-none opacity-0",
6154
+ // Effects transition — opacity must NOT overshoot
6155
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
6156
+ // Hover: 8%
6157
+ "group-data-[hovered]/menuitem:opacity-8",
6158
+ // Pressed: 10%, doubled selector wins over hover at same cascade position
6159
+ "group-data-[pressed]/menuitem:group-data-[pressed]/menuitem:opacity-10",
6160
+ // No state layer when disabled
6161
+ "group-data-[disabled]/menuitem:hidden",
6162
+ // z-[1]: above highlight layer (z-0), below content (z-10)
6163
+ "z-[1]"
6164
+ ],
6165
+ {
6166
+ variants: {
6167
+ colorScheme: {
6168
+ standard: ["bg-on-surface"],
6169
+ vibrant: ["bg-on-tertiary-container"]
6162
6170
  },
6163
- /**
6164
- * Visual style: drives corner radius on items (vertical uses rounded-lg
6165
- * inherited from container, items stay flat inside).
6166
- */
6167
6171
  menuStyle: {
6168
6172
  baseline: [],
6169
6173
  vertical: []
6170
6174
  }
6171
6175
  },
6172
6176
  compoundVariants: [
6173
- // ── Baseline selection (both colorSchemes) ──────────────────────────
6177
+ // vertical + standard: selected/open state layer → on-tertiary-container
6178
+ {
6179
+ menuStyle: "vertical",
6180
+ colorScheme: "standard",
6181
+ class: [
6182
+ "group-data-[selected]/menuitem:bg-on-tertiary-container",
6183
+ "group-data-[open]/menuitem:bg-on-tertiary-container"
6184
+ ]
6185
+ },
6186
+ // vertical + vibrant: selected/open state layer → on-tertiary
6174
6187
  {
6175
- isSelected: true,
6176
- menuStyle: "baseline",
6188
+ menuStyle: "vertical",
6189
+ colorScheme: "vibrant",
6177
6190
  class: [
6178
- "bg-surface-container-highest"
6179
- // text-on-surface already applied by standard colorScheme variant
6191
+ "group-data-[selected]/menuitem:bg-on-tertiary",
6192
+ "group-data-[open]/menuitem:bg-on-tertiary"
6180
6193
  ]
6194
+ }
6195
+ ],
6196
+ defaultVariants: {
6197
+ colorScheme: "standard",
6198
+ menuStyle: "baseline"
6199
+ }
6200
+ }
6201
+ );
6202
+ var menuItemFocusRingVariants = cva([
6203
+ "pointer-events-none absolute inset-0 rounded-[inherit]",
6204
+ "outline outline-2 -outline-offset-2 outline-secondary",
6205
+ // Effects transition — opacity must not overshoot
6206
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
6207
+ "opacity-0",
6208
+ "group-data-[focus-visible]/menuitem:opacity-100",
6209
+ // z-[2]: above state layer (z-[1]) and highlight (z-0), below content (z-10)
6210
+ "z-[2]"
6211
+ ]);
6212
+ var menuItemIconVariants = cva(
6213
+ [
6214
+ "relative z-10 shrink-0 flex items-center justify-center",
6215
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
6216
+ // Disabled: 38% opacity on icon color
6217
+ "group-data-[disabled]/menuitem:text-on-surface/38"
6218
+ ],
6219
+ {
6220
+ variants: {
6221
+ colorScheme: {
6222
+ standard: ["text-on-surface-variant"],
6223
+ vibrant: ["text-on-tertiary-container"]
6181
6224
  },
6182
- // ── Vertical + Standard selection ───────────────────────────────────
6225
+ menuStyle: {
6226
+ baseline: ["h-6 w-6"],
6227
+ vertical: ["h-5 w-5"]
6228
+ }
6229
+ },
6230
+ compoundVariants: [
6231
+ // vertical + standard: selected/open icon → on-tertiary-container
6183
6232
  {
6184
- isSelected: true,
6185
6233
  menuStyle: "vertical",
6186
6234
  colorScheme: "standard",
6187
6235
  class: [
6188
- "bg-tertiary-container",
6189
- "text-on-tertiary-container",
6190
- "before:bg-on-tertiary-container"
6236
+ "group-data-[selected]/menuitem:text-on-tertiary-container",
6237
+ "group-data-[open]/menuitem:text-on-tertiary-container"
6191
6238
  ]
6192
6239
  },
6193
- // ── Vertical + Vibrant selection ─────────────────────────────────────
6240
+ // vertical + vibrant: selected/open icon → on-tertiary
6194
6241
  {
6195
- isSelected: true,
6196
6242
  menuStyle: "vertical",
6197
6243
  colorScheme: "vibrant",
6198
- class: ["bg-tertiary", "text-on-tertiary", "before:bg-on-tertiary"]
6244
+ class: [
6245
+ "group-data-[selected]/menuitem:text-on-tertiary",
6246
+ "group-data-[open]/menuitem:text-on-tertiary"
6247
+ ]
6199
6248
  }
6200
6249
  ],
6201
6250
  defaultVariants: {
6202
- isDisabled: false,
6203
- isSelected: false,
6204
6251
  colorScheme: "standard",
6205
6252
  menuStyle: "baseline"
6206
6253
  }
6207
6254
  }
6208
6255
  );
6209
6256
  var menuSectionVariants = cva(["flex flex-col w-full"]);
6210
- var menuSectionHeaderVariants = cva([
6211
- "px-3 pt-2 pb-1",
6212
- "text-title-small text-on-surface-variant",
6213
- "select-none"
6214
- ]);
6215
- var menuDividerVariants = cva(["border-t border-outline-variant", "my-2 mx-0"]);
6216
- cva(["h-2 w-full"]);
6217
- var menuItemTrailingTextVariants = cva([
6218
- "ml-auto shrink-0 text-label-large text-on-surface-variant",
6219
- "select-none"
6220
- ]);
6221
- var menuItemDescriptionVariants = cva([
6222
- "text-body-medium text-on-surface-variant",
6223
- "select-none"
6224
- ]);
6225
- var Menu = forwardRef(function Menu2({
6226
- children,
6227
- className,
6228
- colorScheme = "standard",
6229
- menuStyle = "baseline",
6230
- density = 0,
6231
- disableRipple = false,
6232
- selectionMode,
6233
- selectedKeys,
6234
- onSelectionChange,
6235
- ...props
6236
- }, _ref) {
6257
+ var menuSectionHeaderVariants = cva(
6258
+ [
6259
+ // 32dp tall region, content vertically centred, 12dp leading padding aligned with items
6260
+ "px-3 h-8 flex items-center",
6261
+ "text-title-small",
6262
+ "select-none"
6263
+ ],
6264
+ {
6265
+ variants: {
6266
+ colorScheme: {
6267
+ standard: ["text-on-surface-variant"],
6268
+ vibrant: ["text-on-tertiary-container"]
6269
+ }
6270
+ },
6271
+ defaultVariants: {
6272
+ colorScheme: "standard"
6273
+ }
6274
+ }
6275
+ );
6276
+ var menuDividerVariants = cva(["border-t border-outline-variant", "my-0.5 mx-3"]);
6277
+ cva(["h-0.5 w-full"]);
6278
+ var menuItemTrailingTextVariants = cva(
6279
+ [
6280
+ "ml-auto shrink-0 text-label-large",
6281
+ "select-none",
6282
+ "group-data-[disabled]/menuitem:text-on-surface/38"
6283
+ ],
6284
+ {
6285
+ variants: {
6286
+ colorScheme: {
6287
+ standard: ["text-on-surface-variant"],
6288
+ vibrant: ["text-on-tertiary-container"]
6289
+ },
6290
+ menuStyle: {
6291
+ baseline: [],
6292
+ vertical: [
6293
+ "group-data-[selected]/menuitem:text-on-tertiary-container",
6294
+ "group-data-[open]/menuitem:text-on-tertiary-container"
6295
+ ]
6296
+ }
6297
+ },
6298
+ compoundVariants: [
6299
+ // vertical + vibrant: selected/open trailing text → on-tertiary
6300
+ {
6301
+ menuStyle: "vertical",
6302
+ colorScheme: "vibrant",
6303
+ class: [
6304
+ "group-data-[selected]/menuitem:text-on-tertiary",
6305
+ "group-data-[open]/menuitem:text-on-tertiary"
6306
+ ]
6307
+ }
6308
+ ],
6309
+ defaultVariants: {
6310
+ colorScheme: "standard",
6311
+ menuStyle: "baseline"
6312
+ }
6313
+ }
6314
+ );
6315
+ var menuItemDescriptionVariants = cva(
6316
+ ["text-body-medium", "select-none", "group-data-[disabled]/menuitem:text-on-surface/38"],
6317
+ {
6318
+ variants: {
6319
+ colorScheme: {
6320
+ standard: ["text-on-surface-variant"],
6321
+ vibrant: ["text-on-tertiary-container"]
6322
+ },
6323
+ menuStyle: {
6324
+ baseline: [],
6325
+ vertical: [
6326
+ "group-data-[selected]/menuitem:text-on-tertiary-container",
6327
+ "group-data-[open]/menuitem:text-on-tertiary-container"
6328
+ ]
6329
+ }
6330
+ },
6331
+ compoundVariants: [
6332
+ // vertical + vibrant: selected/open description → on-tertiary
6333
+ {
6334
+ menuStyle: "vertical",
6335
+ colorScheme: "vibrant",
6336
+ class: [
6337
+ "group-data-[selected]/menuitem:text-on-tertiary",
6338
+ "group-data-[open]/menuitem:text-on-tertiary"
6339
+ ]
6340
+ }
6341
+ ],
6342
+ defaultVariants: {
6343
+ colorScheme: "standard",
6344
+ menuStyle: "baseline"
6345
+ }
6346
+ }
6347
+ );
6348
+ cva(
6349
+ [
6350
+ "flex flex-col w-full",
6351
+ "px-1 py-0.5 gap-0.5",
6352
+ "rounded-lg first:rounded-b-sm last:rounded-t-sm",
6353
+ "shadow-elevation-1"
6354
+ ],
6355
+ {
6356
+ variants: {
6357
+ menuStyle: {
6358
+ vertical: ["bg-surface-container-low"],
6359
+ baseline: []
6360
+ },
6361
+ colorScheme: {
6362
+ standard: ["bg-surface-container-low"],
6363
+ vibrant: ["bg-tertiary-container"]
6364
+ }
6365
+ },
6366
+ compoundVariants: [
6367
+ // vertical + standard: item background = surface-container-low
6368
+ {
6369
+ menuStyle: "vertical",
6370
+ colorScheme: "standard",
6371
+ class: ["bg-surface-container-low"]
6372
+ },
6373
+ // vertical + vibrant: item background = tertiary-container
6374
+ {
6375
+ menuStyle: "vertical",
6376
+ colorScheme: "vibrant",
6377
+ class: ["bg-tertiary-container"]
6378
+ }
6379
+ ],
6380
+ defaultVariants: {
6381
+ menuStyle: "vertical",
6382
+ colorScheme: "standard"
6383
+ }
6384
+ }
6385
+ );
6386
+ var MenuContext = createContext(null);
6387
+ function useMenuContext() {
6388
+ return useContext(MenuContext);
6389
+ }
6390
+ function TriggerBridge({ children }) {
6391
+ const ctx = useSlottedContext(ButtonContext);
6392
+ const localRef = useRef(null);
6393
+ const { ref: contextRef, ...ctxProps } = ctx ?? {};
6394
+ const mergedCallbackRef = useCallback(
6395
+ (node) => {
6396
+ localRef.current = node;
6397
+ if (!contextRef) return;
6398
+ if (typeof contextRef === "function") {
6399
+ contextRef(node);
6400
+ } else {
6401
+ contextRef.current = node;
6402
+ }
6403
+ },
6404
+ [contextRef]
6405
+ );
6406
+ const { buttonProps } = useButton({ ...ctxProps, elementType: "button" }, localRef);
6407
+ if (!isValidElement(children)) return /* @__PURE__ */ jsx(Fragment, { children });
6408
+ return cloneElement(
6409
+ children,
6410
+ { ...buttonProps, ref: mergedCallbackRef }
6411
+ );
6412
+ }
6413
+ function HeadlessMenuTrigger({
6414
+ children,
6415
+ placement = "bottom start",
6416
+ shouldFlip = true,
6417
+ ...rest
6418
+ }) {
6419
+ const childrenArray = Array.isArray(children) ? children : [children];
6420
+ const [triggerChild, menuChild] = childrenArray;
6421
+ return /* @__PURE__ */ jsxs(MenuTrigger$1, { ...rest, children: [
6422
+ /* @__PURE__ */ jsx(TriggerBridge, { children: triggerChild }),
6423
+ /* @__PURE__ */ jsx(
6424
+ Popover,
6425
+ {
6426
+ placement,
6427
+ shouldFlip,
6428
+ offset: 4,
6429
+ className: menuPopoverVariants(),
6430
+ children: menuChild
6431
+ }
6432
+ )
6433
+ ] });
6434
+ }
6435
+ function HeadlessMenu({
6436
+ className,
6437
+ children,
6438
+ "aria-label": ariaLabel,
6439
+ ...props
6440
+ }) {
6441
+ const menuRef = useRef(null);
6442
+ useLayoutEffect(() => {
6443
+ if (ariaLabel && menuRef.current) {
6444
+ menuRef.current.removeAttribute("aria-labelledby");
6445
+ }
6446
+ });
6447
+ return /* @__PURE__ */ jsx(
6448
+ Menu$1,
6449
+ {
6450
+ ...props,
6451
+ ref: menuRef,
6452
+ ...ariaLabel !== void 0 ? { "aria-label": ariaLabel } : {},
6453
+ className: className ?? "",
6454
+ children
6455
+ }
6456
+ );
6457
+ }
6458
+ HeadlessMenuTrigger.Menu = HeadlessMenu;
6459
+ var HeadlessMenuItem = forwardRef(
6460
+ function HeadlessMenuItem2({ children, className, ...props }, ref) {
6461
+ return /* @__PURE__ */ jsx(MenuItem$1, { ...props, ref, className: className ?? "", children });
6462
+ }
6463
+ );
6464
+ function HeadlessMenuSection({
6465
+ children,
6466
+ "aria-label": ariaLabel,
6467
+ className
6468
+ }) {
6469
+ return /* @__PURE__ */ jsx(
6470
+ MenuSection$1,
6471
+ {
6472
+ ...ariaLabel !== void 0 ? { "aria-label": ariaLabel } : {},
6473
+ className: className ?? "",
6474
+ children
6475
+ }
6476
+ );
6477
+ }
6478
+ function HeadlessMenuDivider({
6479
+ className,
6480
+ ...props
6481
+ }) {
6482
+ return /* @__PURE__ */ jsx(Separator, { ...props, className: className ?? "" });
6483
+ }
6484
+ var Menu = forwardRef(function Menu2({
6485
+ children,
6486
+ className,
6487
+ colorScheme = "standard",
6488
+ menuStyle = "baseline",
6489
+ density = 0,
6490
+ disableRipple = false,
6491
+ selectionMode,
6492
+ selectedKeys,
6493
+ onSelectionChange,
6494
+ ...props
6495
+ }, _ref) {
6237
6496
  const close = () => {
6238
6497
  };
6239
6498
  const contextValue = {
@@ -6281,7 +6540,13 @@ function CheckIcon() {
6281
6540
  }
6282
6541
  );
6283
6542
  }
6284
- var DENSITY_HEIGHT = {
6543
+ var BASELINE_DENSITY_HEIGHT = {
6544
+ 0: "h-12",
6545
+ [-1]: "h-11",
6546
+ [-2]: "h-10",
6547
+ [-3]: "h-9"
6548
+ };
6549
+ var VERTICAL_DENSITY_HEIGHT = {
6285
6550
  0: "h-12",
6286
6551
  [-1]: "h-11",
6287
6552
  [-2]: "h-10",
@@ -6304,19 +6569,18 @@ var MenuItem = forwardRef(function MenuItem2({
6304
6569
  const menuStyle = ctx?.menuStyle ?? "baseline";
6305
6570
  const density = ctx?.density ?? 0;
6306
6571
  const selectionMode = ctx?.selectionMode;
6307
- const heightClass = DENSITY_HEIGHT[density];
6572
+ const heightClass = menuStyle === "vertical" ? VERTICAL_DENSITY_HEIGHT[density] : BASELINE_DENSITY_HEIGHT[density];
6308
6573
  const isSelectionMenu = selectionMode != null;
6309
6574
  const { ripples, onMouseDown } = useRipple({ disabled: disableRipple });
6310
- const computeClassName = ({ isDisabled, isSelected }) => cn(
6311
- menuItemVariants({
6312
- isDisabled,
6313
- isSelected: isSelected ?? false,
6314
- colorScheme,
6315
- menuStyle
6316
- }),
6575
+ const computeClassName = ({ isSelected }) => cn(
6576
+ menuItemVariants({ colorScheme, menuStyle }),
6577
+ // group/menuitem scope: all slot children read state via group-data-[x]/menuitem
6578
+ "group/menuitem",
6317
6579
  // Height: auto when description is present (multi-line), otherwise density
6318
6580
  description ? "min-h-12 py-2 h-auto items-start" : heightClass,
6319
- className
6581
+ className,
6582
+ // Silence the isSelected lint — value consumed in render-prop below
6583
+ isSelected ? "" : ""
6320
6584
  );
6321
6585
  return /* @__PURE__ */ jsx(
6322
6586
  HeadlessMenuItem,
@@ -6326,24 +6590,48 @@ var MenuItem = forwardRef(function MenuItem2({
6326
6590
  className: computeClassName,
6327
6591
  onMouseDown,
6328
6592
  children: ({ isSelected }) => /* @__PURE__ */ jsxs(Fragment, { children: [
6329
- !disableRipple && /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-[inherit]", children: ripples }),
6330
- (leadingIcon != null || isSelectionMenu) && /* @__PURE__ */ jsx(
6593
+ /* @__PURE__ */ jsx(
6331
6594
  "span",
6332
6595
  {
6333
- className: "text-on-surface-variant relative z-10 flex h-6 w-6 shrink-0 items-center justify-center",
6334
6596
  "aria-hidden": "true",
6335
- children: isSelectionMenu && leadingIcon == null ? isSelected ? /* @__PURE__ */ jsx(CheckIcon, {}) : null : leadingIcon
6597
+ "data-testid": "menuitem-highlight",
6598
+ className: menuItemHighlightVariants({ colorScheme, menuStyle })
6336
6599
  }
6337
6600
  ),
6601
+ /* @__PURE__ */ jsx(
6602
+ "span",
6603
+ {
6604
+ "aria-hidden": "true",
6605
+ className: menuItemStateLayerVariants({ colorScheme, menuStyle })
6606
+ }
6607
+ ),
6608
+ /* @__PURE__ */ jsx(
6609
+ "span",
6610
+ {
6611
+ "aria-hidden": "true",
6612
+ "data-testid": "menuitem-focus-ring",
6613
+ className: menuItemFocusRingVariants()
6614
+ }
6615
+ ),
6616
+ !disableRipple && /* @__PURE__ */ jsx(
6617
+ "span",
6618
+ {
6619
+ className: cn(
6620
+ "pointer-events-none absolute inset-0 z-[3] overflow-hidden rounded-[inherit]"
6621
+ ),
6622
+ children: ripples
6623
+ }
6624
+ ),
6625
+ (leadingIcon != null || isSelectionMenu) && /* @__PURE__ */ jsx("span", { className: menuItemIconVariants({ colorScheme, menuStyle }), "aria-hidden": "true", children: isSelectionMenu && leadingIcon == null ? isSelected ? /* @__PURE__ */ jsx(CheckIcon, {}) : null : leadingIcon }),
6338
6626
  description != null ? /* @__PURE__ */ jsxs("span", { className: "relative z-10 flex min-w-0 flex-1 flex-col", children: [
6339
- /* @__PURE__ */ jsx("span", { className: "text-body-large", children }),
6340
- /* @__PURE__ */ jsx("span", { className: menuItemDescriptionVariants(), children: description })
6341
- ] }) : /* @__PURE__ */ jsx("span", { className: "text-body-large relative z-10 min-w-0 flex-1", children }),
6627
+ /* @__PURE__ */ jsx("span", { className: "text-label-large group-data-[disabled]/menuitem:text-on-surface/38", children }),
6628
+ /* @__PURE__ */ jsx("span", { className: menuItemDescriptionVariants({ colorScheme, menuStyle }), children: description })
6629
+ ] }) : /* @__PURE__ */ jsx("span", { className: "text-label-large group-data-[disabled]/menuitem:text-on-surface/38 relative z-10 min-w-0 flex-1", children }),
6342
6630
  badge != null && /* @__PURE__ */ jsx("span", { className: "relative z-10 shrink-0", children: badge }),
6343
6631
  trailingIcon != null && trailingText == null && /* @__PURE__ */ jsx(
6344
6632
  "span",
6345
6633
  {
6346
- className: "text-on-surface-variant relative z-10 ml-auto flex h-6 w-6 shrink-0 items-center justify-center",
6634
+ className: cn(menuItemIconVariants({ colorScheme, menuStyle }), "ml-auto"),
6347
6635
  "aria-hidden": "true",
6348
6636
  children: trailingIcon
6349
6637
  }
@@ -6351,7 +6639,10 @@ var MenuItem = forwardRef(function MenuItem2({
6351
6639
  trailingText != null && trailingIcon == null && /* @__PURE__ */ jsx(
6352
6640
  "span",
6353
6641
  {
6354
- className: cn(menuItemTrailingTextVariants(), "relative z-10"),
6642
+ className: cn(
6643
+ menuItemTrailingTextVariants({ colorScheme, menuStyle }),
6644
+ "relative z-10"
6645
+ ),
6355
6646
  "aria-keyshortcuts": trailingText,
6356
6647
  children: trailingText
6357
6648
  }
@@ -6367,6 +6658,8 @@ function MenuSection({
6367
6658
  className,
6368
6659
  "aria-label": ariaLabel
6369
6660
  }) {
6661
+ const ctx = useMenuContext();
6662
+ const colorScheme = ctx?.colorScheme ?? "standard";
6370
6663
  const sectionAriaLabel = ariaLabel ?? header;
6371
6664
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6372
6665
  showDivider && /* @__PURE__ */ jsx(HeadlessMenuDivider, { className: menuDividerVariants() }),
@@ -6376,7 +6669,7 @@ function MenuSection({
6376
6669
  "aria-label": sectionAriaLabel,
6377
6670
  className: cn(menuSectionVariants(), className),
6378
6671
  children: [
6379
- header && /* @__PURE__ */ jsx(Header, { className: menuSectionHeaderVariants(), "aria-hidden": "true", children: header }),
6672
+ header && /* @__PURE__ */ jsx(Header, { className: menuSectionHeaderVariants({ colorScheme }), "aria-hidden": "true", children: header }),
6380
6673
  children
6381
6674
  ]
6382
6675
  }
@@ -7716,13 +8009,14 @@ var DialogPanel = ({
7716
8009
  headlineId,
7717
8010
  contentId,
7718
8011
  onClose,
7719
- onTransitionEnd,
8012
+ onAnimationEnd,
7720
8013
  variant,
7721
8014
  isDismissable,
7722
8015
  wrapperClassName,
7723
8016
  className,
7724
8017
  animationState,
7725
8018
  getAnimationClassName,
8019
+ icon,
7726
8020
  children
7727
8021
  }) => {
7728
8022
  const panelRef = useRef(null);
@@ -7745,9 +8039,10 @@ var DialogPanel = ({
7745
8039
  panelRef
7746
8040
  );
7747
8041
  const panelClassName = cn(className, getAnimationClassName?.(animationState));
8042
+ const hasIcon = icon !== void 0 && icon !== null && variant === "basic";
7748
8043
  return (
7749
8044
  // Centering/positioning wrapper — structural only, no ARIA role
7750
- /* @__PURE__ */ jsx("div", { className: wrapperClassName, children: /* @__PURE__ */ jsx(
8045
+ /* @__PURE__ */ jsx("div", { className: wrapperClassName, children: /* @__PURE__ */ jsxs(
7751
8046
  "div",
7752
8047
  {
7753
8048
  ...mergeProps(overlayProps, dialogProps),
@@ -7756,8 +8051,12 @@ var DialogPanel = ({
7756
8051
  className: panelClassName,
7757
8052
  "data-animation-state": animationState,
7758
8053
  "data-variant": variant,
7759
- onTransitionEnd,
7760
- children
8054
+ "data-with-icon": hasIcon ? "" : void 0,
8055
+ onAnimationEnd,
8056
+ children: [
8057
+ hasIcon && icon,
8058
+ children
8059
+ ]
7761
8060
  }
7762
8061
  ) })
7763
8062
  );
@@ -7770,9 +8069,11 @@ var DialogHeadless = forwardRef(
7770
8069
  defaultOpen = false,
7771
8070
  onOpenChange,
7772
8071
  "aria-label": ariaLabel,
8072
+ icon,
7773
8073
  children,
7774
8074
  className,
7775
- scrimClassName,
8075
+ wrapperClassName,
8076
+ getScrimClassName,
7776
8077
  getAnimationClassName
7777
8078
  }, _ref) {
7778
8079
  const state = useOverlayTriggerState({
@@ -7806,7 +8107,7 @@ var DialogHeadless = forwardRef(
7806
8107
  closedRef.current = true;
7807
8108
  setAnimationState("exited");
7808
8109
  }
7809
- }, 150);
8110
+ }, 250);
7810
8111
  }
7811
8112
  }, [isOpen, animationState]);
7812
8113
  useEffect(
@@ -7817,16 +8118,20 @@ var DialogHeadless = forwardRef(
7817
8118
  },
7818
8119
  []
7819
8120
  );
7820
- const handleTransitionEnd = useCallback(() => {
7821
- if (animationState === "exiting" && !closedRef.current) {
7822
- if (exitFallbackRef.current !== null) {
7823
- clearTimeout(exitFallbackRef.current);
7824
- exitFallbackRef.current = null;
8121
+ const handleAnimationEnd = useCallback(
8122
+ (e) => {
8123
+ if (e.target !== e.currentTarget) return;
8124
+ if (animationState === "exiting" && !closedRef.current) {
8125
+ if (exitFallbackRef.current !== null) {
8126
+ clearTimeout(exitFallbackRef.current);
8127
+ exitFallbackRef.current = null;
8128
+ }
8129
+ closedRef.current = true;
8130
+ setAnimationState("exited");
7825
8131
  }
7826
- closedRef.current = true;
7827
- setAnimationState("exited");
7828
- }
7829
- }, [animationState]);
8132
+ },
8133
+ [animationState]
8134
+ );
7830
8135
  const baseId = useId();
7831
8136
  const headlineId = `${baseId}-dialog-headline`;
7832
8137
  const contentId = `${baseId}-dialog-content`;
@@ -7841,6 +8146,8 @@ var DialogHeadless = forwardRef(
7841
8146
  close();
7842
8147
  }
7843
8148
  }, [variant, close]);
8149
+ const resolvedWrapperClass = wrapperClassName ?? (variant === "basic" ? "fixed inset-0 z-50 flex items-center justify-center px-4" : "fixed inset-0 z-50");
8150
+ const resolvedScrimClass = getScrimClassName?.(animationState) ?? "fixed inset-0 z-40 bg-scrim/32";
7844
8151
  if (!isOpen && animationState === "exited") {
7845
8152
  return null;
7846
8153
  }
@@ -7849,7 +8156,7 @@ var DialogHeadless = forwardRef(
7849
8156
  "div",
7850
8157
  {
7851
8158
  "data-testid": "dialog-scrim",
7852
- className: scrimClassName,
8159
+ className: resolvedScrimClass,
7853
8160
  onClick: handleScrimClick,
7854
8161
  "aria-hidden": "true"
7855
8162
  }
@@ -7861,13 +8168,14 @@ var DialogHeadless = forwardRef(
7861
8168
  headlineId,
7862
8169
  contentId,
7863
8170
  onClose: close,
7864
- onTransitionEnd: handleTransitionEnd,
8171
+ onAnimationEnd: handleAnimationEnd,
7865
8172
  variant,
7866
8173
  isDismissable: variant === "basic",
7867
- wrapperClassName: variant === "basic" ? "fixed inset-0 z-50 flex items-center justify-center px-4" : "fixed inset-0 z-50",
8174
+ wrapperClassName: resolvedWrapperClass,
7868
8175
  className,
7869
8176
  animationState,
7870
8177
  getAnimationClassName,
8178
+ icon,
7871
8179
  children
7872
8180
  }
7873
8181
  ) })
@@ -7877,16 +8185,28 @@ var DialogHeadless = forwardRef(
7877
8185
  }
7878
8186
  );
7879
8187
  DialogHeadless.displayName = "DialogHeadless";
7880
- var dialogScrimVariants = cva([
7881
- "fixed",
7882
- "inset-0",
7883
- "z-40",
7884
- "bg-scrim",
7885
- "opacity-32",
7886
- "transition-opacity",
7887
- "duration-medium2",
7888
- "ease-standard"
7889
- ]);
8188
+ var dialogScrimVariants = cva(
8189
+ [
8190
+ "fixed",
8191
+ "inset-0",
8192
+ "z-40",
8193
+ // MD3 scrim: bg-scrim at 32% opacity — always set so instant-show works in reduced-motion
8194
+ "bg-scrim/32"
8195
+ ],
8196
+ {
8197
+ variants: {
8198
+ animationState: {
8199
+ entering: ["opacity-0"],
8200
+ visible: ["animate-md-fade-in"],
8201
+ exiting: ["animate-md-fade-out"],
8202
+ exited: ["opacity-0", "pointer-events-none"]
8203
+ }
8204
+ },
8205
+ defaultVariants: {
8206
+ animationState: "entering"
8207
+ }
8208
+ }
8209
+ );
7890
8210
  var dialogPanelVariants = cva(
7891
8211
  [
7892
8212
  // Stacking above scrim
@@ -7896,9 +8216,10 @@ var dialogPanelVariants = cva(
7896
8216
  // Flex column layout for slots
7897
8217
  "flex",
7898
8218
  "flex-col",
7899
- // Transition for animation state changes
7900
- "transition-[opacity,transform]",
7901
- "will-change-[opacity,transform]"
8219
+ // Compositor hint for keyframe animation
8220
+ "will-change-[opacity,transform]",
8221
+ // group scope — lets child slots consume data-with-icon via group-data-[with-icon]/dialog:
8222
+ "group/dialog"
7902
8223
  ],
7903
8224
  {
7904
8225
  variants: {
@@ -7912,11 +8233,11 @@ var dialogPanelVariants = cva(
7912
8233
  "min-w-70",
7913
8234
  "max-w-dialog-max",
7914
8235
  "w-full",
7915
- // Internal spacing
8236
+ // Internal spacing: 24dp padding, headline mb-4, content mb-6, actions pt-3
7916
8237
  "pt-6",
7917
8238
  "pb-3",
7918
8239
  "px-6",
7919
- // Positioned in viewport center
8240
+ // Positioning (centering wrapper handles the viewport centering)
7920
8241
  "relative"
7921
8242
  ],
7922
8243
  fullscreen: [
@@ -7927,7 +8248,6 @@ var dialogPanelVariants = cva(
7927
8248
  "rounded-none",
7928
8249
  // No elevation shadow on fullscreen
7929
8250
  "shadow-none",
7930
- // Positioned to fill portal
7931
8251
  "relative"
7932
8252
  ]
7933
8253
  }
@@ -7937,7 +8257,7 @@ var dialogPanelVariants = cva(
7937
8257
  }
7938
8258
  }
7939
8259
  );
7940
- cva([], {
8260
+ var dialogWrapperVariants = cva([], {
7941
8261
  variants: {
7942
8262
  variant: {
7943
8263
  basic: ["fixed", "inset-0", "z-50", "flex", "items-center", "justify-center", "px-4"],
@@ -7951,9 +8271,13 @@ cva([], {
7951
8271
  var dialogAnimationVariants = cva("", {
7952
8272
  variants: {
7953
8273
  animationState: {
8274
+ // initial mount frame before the animation starts — rendered invisible
7954
8275
  entering: [],
8276
+ // entry animation active
7955
8277
  visible: [],
8278
+ // exit animation active
7956
8279
  exiting: [],
8280
+ // fully dismissed; portal gate will remove the element
7957
8281
  exited: []
7958
8282
  },
7959
8283
  variant: {
@@ -7962,53 +8286,55 @@ var dialogAnimationVariants = cva("", {
7962
8286
  }
7963
8287
  },
7964
8288
  compoundVariants: [
7965
- // Basic: entering — start scaled down + transparent
8289
+ // ── Basic ────────────────────────────────────────────────────────────────
8290
+ // entering: start invisible (animate-md-scale-in keyframe starts from scale(0.85)/opacity:0)
7966
8291
  {
7967
8292
  animationState: "entering",
7968
8293
  variant: "basic",
7969
- className: ["scale-90", "opacity-0"]
8294
+ className: ["opacity-0"]
7970
8295
  },
7971
- // Basic: visible scale to full + fade in
8296
+ // visible: composite scale-in keyframe (expressive-fast-spatial 350ms)
7972
8297
  {
7973
8298
  animationState: "visible",
7974
8299
  variant: "basic",
7975
- className: ["scale-100", "opacity-100", "duration-medium4", "ease-emphasized-decelerate"]
8300
+ className: ["animate-md-scale-in"]
7976
8301
  },
7977
- // Basic: exiting fade out (scale stays at 1)
8302
+ // exiting: composite scale-out keyframe (emphasized-accelerate 200ms)
7978
8303
  {
7979
8304
  animationState: "exiting",
7980
8305
  variant: "basic",
7981
- className: ["scale-100", "opacity-0", "duration-short2", "ease-emphasized-accelerate"]
8306
+ className: ["animate-md-scale-out"]
7982
8307
  },
7983
- // Basic: exited fully transparent
8308
+ // exited: keep hidden until portal gate removes it
7984
8309
  {
7985
8310
  animationState: "exited",
7986
8311
  variant: "basic",
7987
- className: ["scale-100", "opacity-0"]
8312
+ className: ["opacity-0", "pointer-events-none"]
7988
8313
  },
7989
- // Fullscreen: entering — start below viewport + transparent
8314
+ // ── Fullscreen ───────────────────────────────────────────────────────────
8315
+ // entering: start off-screen below (slide-in-bottom starts from translateY(100%)/opacity:0)
7990
8316
  {
7991
8317
  animationState: "entering",
7992
8318
  variant: "fullscreen",
7993
- className: ["translate-y-full", "opacity-0"]
8319
+ className: ["opacity-0"]
7994
8320
  },
7995
- // Fullscreen: visible slide up + fade in
8321
+ // visible: composite slide-in-bottom keyframe (standard-default-spatial 500ms)
7996
8322
  {
7997
8323
  animationState: "visible",
7998
8324
  variant: "fullscreen",
7999
- className: ["translate-y-0", "opacity-100", "duration-medium4", "ease-emphasized-decelerate"]
8325
+ className: ["animate-md-slide-in-bottom"]
8000
8326
  },
8001
- // Fullscreen: exiting slide down + fade out
8327
+ // exiting: composite slide-out-bottom keyframe (emphasized-accelerate 200ms)
8002
8328
  {
8003
8329
  animationState: "exiting",
8004
8330
  variant: "fullscreen",
8005
- className: ["translate-y-full", "opacity-0", "duration-short2", "ease-emphasized-accelerate"]
8331
+ className: ["animate-md-slide-out-bottom"]
8006
8332
  },
8007
- // Fullscreen: exited fully off-screen
8333
+ // exited: keep hidden until portal gate removes it
8008
8334
  {
8009
8335
  animationState: "exited",
8010
8336
  variant: "fullscreen",
8011
- className: ["translate-y-full", "opacity-0"]
8337
+ className: ["opacity-0", "pointer-events-none"]
8012
8338
  }
8013
8339
  ],
8014
8340
  defaultVariants: {
@@ -8016,10 +8342,26 @@ var dialogAnimationVariants = cva("", {
8016
8342
  variant: "basic"
8017
8343
  }
8018
8344
  });
8345
+ cva([
8346
+ // Center the icon in the panel
8347
+ "flex",
8348
+ "items-center",
8349
+ "justify-center",
8350
+ // Bottom margin separating icon from headline
8351
+ "mb-4",
8352
+ // MD3 spec: icon color = secondary
8353
+ "text-secondary",
8354
+ // 24dp icon size (children — typically an SVG — should be 24×24)
8355
+ "size-6"
8356
+ ]);
8019
8357
  var dialogHeadlineVariants = cva(["text-headline-small", "text-on-surface"], {
8020
8358
  variants: {
8021
8359
  variant: {
8022
- basic: ["mb-4"],
8360
+ basic: [
8361
+ "mb-4",
8362
+ // Center headline text when hero icon is present
8363
+ "group-data-[with-icon]/dialog:text-center"
8364
+ ],
8023
8365
  fullscreen: [
8024
8366
  // Top app bar row in fullscreen: flex, items-center, gap
8025
8367
  "flex",
@@ -8044,7 +8386,19 @@ var dialogHeadlineTitleVariants = cva([
8044
8386
  "truncate"
8045
8387
  ]);
8046
8388
  var dialogContentVariants = cva(
8047
- ["text-body-medium", "text-on-surface-variant", "overflow-y-auto", "flex-1"],
8389
+ [
8390
+ "text-body-medium",
8391
+ "text-on-surface-variant",
8392
+ "overflow-y-auto",
8393
+ "flex-1",
8394
+ // Center supporting text when hero icon is present
8395
+ "group-data-[with-icon]/dialog:text-center",
8396
+ // Scroll dividers — activated by DialogContent's scroll handler
8397
+ "data-[scroll-divider-top]:border-t",
8398
+ "data-[scroll-divider-top]:border-outline-variant",
8399
+ "data-[scroll-divider-bottom]:border-b",
8400
+ "data-[scroll-divider-bottom]:border-outline-variant"
8401
+ ],
8048
8402
  {
8049
8403
  variants: {
8050
8404
  variant: {
@@ -8071,11 +8425,31 @@ var Dialog = forwardRef(function Dialog2({
8071
8425
  defaultOpen = false,
8072
8426
  onOpenChange,
8073
8427
  "aria-label": ariaLabel,
8428
+ icon,
8074
8429
  children,
8075
8430
  className
8076
8431
  }, _ref) {
8077
- const panelClassName = cn(dialogPanelVariants({ variant }), className);
8078
- const scrimClass = dialogScrimVariants();
8432
+ const reducedMotion = useReducedMotion();
8433
+ const panelClassName = cn(
8434
+ dialogPanelVariants({ variant }),
8435
+ reducedMotion && "transition-none",
8436
+ className
8437
+ );
8438
+ const wrapperClassName = dialogWrapperVariants({ variant });
8439
+ const getScrimClassName = useCallback(
8440
+ (state) => {
8441
+ if (reducedMotion) return "fixed inset-0 z-40 bg-scrim/32";
8442
+ return dialogScrimVariants({ animationState: state });
8443
+ },
8444
+ [reducedMotion]
8445
+ );
8446
+ const getAnimationClassName = useCallback(
8447
+ (state) => {
8448
+ if (reducedMotion) return "";
8449
+ return dialogAnimationVariants({ animationState: state, variant });
8450
+ },
8451
+ [reducedMotion, variant]
8452
+ );
8079
8453
  return /* @__PURE__ */ jsx(
8080
8454
  DialogHeadless,
8081
8455
  {
@@ -8084,9 +8458,11 @@ var Dialog = forwardRef(function Dialog2({
8084
8458
  ...defaultOpen !== void 0 ? { defaultOpen } : {},
8085
8459
  ...onOpenChange !== void 0 ? { onOpenChange } : {},
8086
8460
  ...ariaLabel ? { "aria-label": ariaLabel } : {},
8461
+ ...icon !== void 0 ? { icon } : {},
8087
8462
  className: panelClassName,
8088
- scrimClassName: scrimClass,
8089
- getAnimationClassName: (state) => dialogAnimationVariants({ animationState: state, variant }),
8463
+ wrapperClassName,
8464
+ getScrimClassName,
8465
+ getAnimationClassName,
8090
8466
  children
8091
8467
  }
8092
8468
  );
@@ -8097,7 +8473,7 @@ var DialogHeadline = forwardRef(
8097
8473
  const { headlineId, variant } = useDialogContext();
8098
8474
  if (variant === "fullscreen") {
8099
8475
  return (
8100
- // Top app bar row for fullscreen variant
8476
+ // Top app bar row for fullscreen variant — always has border-b border-outline-variant
8101
8477
  /* @__PURE__ */ jsxs("div", { className: cn(dialogHeadlineVariants({ variant: "fullscreen" }), className), children: [
8102
8478
  closeButton,
8103
8479
  /* @__PURE__ */ jsx("h2", { id: headlineId, className: dialogHeadlineTitleVariants(), children }),
@@ -8105,21 +8481,72 @@ var DialogHeadline = forwardRef(
8105
8481
  ] })
8106
8482
  );
8107
8483
  }
8108
- return /* @__PURE__ */ jsx(
8109
- "h2",
8110
- {
8111
- ref,
8112
- id: headlineId,
8113
- className: cn(dialogHeadlineVariants({ variant: "basic" }), className),
8114
- children
8115
- }
8484
+ return (
8485
+ // Basic variant: text-headline-small, text-on-surface, mb-4
8486
+ // group-data-[with-icon]/dialog:text-center is applied via the CVA base classes
8487
+ // when the parent panel root has data-with-icon set by DialogHeadless.
8488
+ /* @__PURE__ */ jsx(
8489
+ "h2",
8490
+ {
8491
+ ref,
8492
+ id: headlineId,
8493
+ className: cn(dialogHeadlineVariants({ variant: "basic" }), className),
8494
+ children
8495
+ }
8496
+ )
8116
8497
  );
8117
8498
  }
8118
8499
  );
8119
8500
  DialogHeadline.displayName = "DialogHeadline";
8120
- var DialogContent = forwardRef(function DialogContent2({ children, className }, ref) {
8501
+ var DialogContent = forwardRef(function DialogContent2({ children, className }, forwardedRef) {
8121
8502
  const { contentId, variant } = useDialogContext();
8122
- return /* @__PURE__ */ jsx("div", { ref, id: contentId, className: cn(dialogContentVariants({ variant }), className), children });
8503
+ const internalRef = useRef(null);
8504
+ const setRef = useCallback(
8505
+ (node) => {
8506
+ internalRef.current = node;
8507
+ if (typeof forwardedRef === "function") {
8508
+ forwardedRef(node);
8509
+ } else if (forwardedRef !== null && forwardedRef !== void 0) {
8510
+ forwardedRef.current = node;
8511
+ }
8512
+ },
8513
+ [forwardedRef]
8514
+ );
8515
+ const updateDividers = useCallback(() => {
8516
+ const el = internalRef.current;
8517
+ if (!el) return;
8518
+ const isScrollable = el.scrollHeight > el.clientHeight;
8519
+ if (!isScrollable) {
8520
+ el.removeAttribute("data-scroll-divider-top");
8521
+ el.removeAttribute("data-scroll-divider-bottom");
8522
+ return;
8523
+ }
8524
+ const scrolledFromTop = el.scrollTop > 1;
8525
+ const scrolledToBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
8526
+ if (scrolledFromTop) {
8527
+ el.setAttribute("data-scroll-divider-top", "");
8528
+ } else {
8529
+ el.removeAttribute("data-scroll-divider-top");
8530
+ }
8531
+ if (!scrolledToBottom) {
8532
+ el.setAttribute("data-scroll-divider-bottom", "");
8533
+ } else {
8534
+ el.removeAttribute("data-scroll-divider-bottom");
8535
+ }
8536
+ }, []);
8537
+ useEffect(() => {
8538
+ const el = internalRef.current;
8539
+ if (!el) return;
8540
+ updateDividers();
8541
+ el.addEventListener("scroll", updateDividers, { passive: true });
8542
+ const observer = new ResizeObserver(updateDividers);
8543
+ observer.observe(el);
8544
+ return () => {
8545
+ el.removeEventListener("scroll", updateDividers);
8546
+ observer.disconnect();
8547
+ };
8548
+ }, [updateDividers]);
8549
+ return /* @__PURE__ */ jsx("div", { ref: setRef, id: contentId, className: cn(dialogContentVariants({ variant }), className), children });
8123
8550
  });
8124
8551
  DialogContent.displayName = "DialogContent";
8125
8552
  var DialogActions = forwardRef(function DialogActions2({ children, className }, ref) {