@helloimjolopez-pathway/pathway-components 0.12.0 → 0.13.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.
@@ -60,6 +60,7 @@ import { Spinner } from "../spinner/spinner.jsx";
60
60
  import { Badge } from "../badge/badge.jsx";
61
61
  import { Avatar, initialsFrom } from "../avatar/avatar.jsx";
62
62
  import { Button } from "../button/button.jsx";
63
+ import { Tooltip } from "../tooltip/tooltip.jsx";
63
64
 
64
65
  // ─── TOKEN HELPERS ────────────────────────────────────────────────────────────
65
66
  // SC = semantic-color (themes/*.css), SL = semantic-layout-units (layout.css),
@@ -191,6 +192,12 @@ export const TYPE = {
191
192
  header: { fontSize: ST("font-size-xs"), lineHeight: ST("line-height-xs-single"), fontWeight: ST("weight-semibold"), letterSpacing: ST("letter-spacing-wide") },
192
193
  };
193
194
 
195
+ /* The BottomSheet's tokens and measurements (bottom-sheet.jsx T and L), named
196
+ here rather than imported because BottomSheet imports this module. */
197
+ const SHEET_T = { surface: SC("fill-surface-elevated"), overlay: SC("scrim-subtle"), handle: SC("fill-static-neutral-strong") };
198
+ const SHEET_L = { radius: SL("cornerradius-xlarge"), pillRadius: SL("cornerradius-full"),
199
+ maxWidth: 640, topClearance: 56, handleZone: 20, pillWidth: 32, pillHeight: 4 };
200
+
194
201
  const ITEM = ".pw-menu-item";
195
202
  const live = (s) => `${ITEM}:not([data-disabled]):is(${s})`;
196
203
  const HOVER = "[data-hovered],[data-focused],[data-open],[data-force=Hover],[data-force=Focus]";
@@ -206,6 +213,9 @@ const CSS = `
206
213
  background:${T.panel};border:${L.panelBorder} solid ${T.panelBorder};border-radius:${L.panelRadius};box-shadow:${L.shadow};
207
214
  font-family:${ST("family-brand")},sans-serif}
208
215
  .pw-menu-panel[data-searchable]{height:${L.searchHeight}px}
216
+ /* React Aria stacks the panel at 100000 inline; a truncated label's Tooltip
217
+ (portalled to the body at 1000) has to sit above it. */
218
+ body:has(.pw-menu-panel) .pw-tooltip{z-index:100001}
209
219
  .pw-menu-panel[data-entering]{animation:pw-menu-in ${M("duration-4")} ${M("easing-decelerate")} both}
210
220
  .pw-menu-panel[data-placement=top][data-entering]{animation-name:pw-menu-in-up}
211
221
  .pw-menu-panel[data-exiting]{animation:pw-menu-out ${M("duration-2")} ${M("easing-standard")} both}
@@ -304,7 +314,26 @@ ${ITEM}:is([data-selected],[data-current]) .pw-menu-item__stripe{background:${T.
304
314
  border:0;border-radius:${L.itemRadius};background:${T.avatarFill};color:${T.avatarText};font-size:${ST("font-size-xxs")};font-weight:${ST("weight-semibold")}}
305
315
  .pw-menu-trigger :is(.pw-menu-trigger__icon,.pw-menu-trigger__button,.pw-menu-trigger__avatar):focus-visible{outline:${L.focusWidth} solid ${T.focusRing}}
306
316
 
317
+ /* smallScreen="sheet" (G4, pending the overlay family decision): below 768 the
318
+ panel opens as a bottom sheet from the BottomSheet tokens, the way the
319
+ approved demo draws it. The scrim is a spread shadow, so a press outside
320
+ still reaches the page and closes the menu. */
321
+ @media (max-width: 767px){
322
+ .pw-menu-panel[data-small-screen=sheet]{position:fixed !important;top:auto !important;bottom:0 !important;left:0 !important;right:0 !important;
323
+ width:auto;max-width:${SHEET_L.maxWidth}px;margin:0 auto;max-height:calc(var(--visual-viewport-height,100vh) - ${SHEET_L.topClearance}px);
324
+ padding-top:${SHEET_L.handleZone}px;padding-bottom:env(safe-area-inset-bottom,0);
325
+ background:${SHEET_T.surface};border-bottom:0;border-radius:${SHEET_L.radius} ${SHEET_L.radius} 0 0;box-shadow:0 0 0 100vmax ${SHEET_T.overlay}}
326
+ .pw-menu-panel[data-small-screen=sheet]::before{content:"";position:absolute;top:${(SHEET_L.handleZone - SHEET_L.pillHeight) / 2}px;left:50%;
327
+ width:${SHEET_L.pillWidth}px;height:${SHEET_L.pillHeight}px;margin-left:-${SHEET_L.pillWidth / 2}px;border-radius:${SHEET_L.pillRadius};background:${SHEET_T.handle}}
328
+ .pw-menu-panel[data-small-screen=sheet][data-entering],.pw-menu-panel[data-small-screen=sheet][data-placement=top][data-entering]{animation:pw-menu-sheet-in ${M("duration-5")} ${M("easing-emphasized")} both}
329
+ .pw-menu-panel[data-small-screen=sheet][data-exiting]{animation:pw-menu-sheet-out ${M("duration-4")} ${M("easing-accelerate")} both}
330
+ @keyframes pw-menu-sheet-in{from{transform:translateY(100%)}to{transform:none}}
331
+ @keyframes pw-menu-sheet-out{from{transform:none}to{transform:translateY(100%)}}
332
+ }
333
+
307
334
  @media (prefers-reduced-motion: reduce){
335
+ .pw-menu-panel[data-small-screen=sheet][data-entering],.pw-menu-panel[data-small-screen=sheet][data-placement=top][data-entering]{animation:pw-menu-fade ${M("duration-2")} ${M("easing-standard")} both}
336
+ .pw-menu-panel[data-small-screen=sheet][data-exiting]{animation:pw-menu-out ${M("duration-2")} ${M("easing-standard")} both}
308
337
  .pw-menu-panel[data-entering],.pw-menu-panel[data-placement=top][data-entering]{animation:pw-menu-fade ${M("duration-2")} ${M("easing-standard")} both}
309
338
  .pw-menu-panel[data-exiting]{animation:pw-menu-out ${M("duration-2")} ${M("easing-standard")} both}
310
339
  @keyframes pw-menu-fade{from{opacity:0}to{opacity:1}}
@@ -325,8 +354,9 @@ function useStyles() {
325
354
 
326
355
  /* Picks are seen before the panel goes: the row shows its new state for
327
356
  Duration/3, then the panel fades (approved demo). */
328
- const pickDelay = () => (typeof document !== "undefined"
329
- && parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--motion-duration-3"))) || 200;
357
+ const motionMs = (name, fallback) => (typeof document !== "undefined"
358
+ && parseFloat(getComputedStyle(document.documentElement).getPropertyValue(name))) || fallback;
359
+ const pickDelay = () => motionMs("--motion-duration-3", 200);
330
360
 
331
361
  // ─── CONTEXTS ─────────────────────────────────────────────────────────────────
332
362
 
@@ -379,6 +409,32 @@ function Trailing({ trailing, hasSubmenu }) {
379
409
  return null;
380
410
  }
381
411
 
412
+ /* A label cut off by the panel width shows the full text in the real Tooltip:
413
+ on hover after Duration/4, on keyboard focus at once (approved demo). The
414
+ item's accessible name already carries the full label, so the tooltip is
415
+ visual only and leaves the item's ARIA alone. */
416
+ function ItemLabel({ label, s }) {
417
+ const ref = useRef(null);
418
+ const [cut, setCut] = useState(false);
419
+ const [hoverOpen, setHoverOpen] = useState(false);
420
+ const engaged = !!(s.isHovered || s.isFocusVisible);
421
+ useEffect(() => {
422
+ const el = ref.current;
423
+ setCut(!!el && el.scrollWidth > el.clientWidth + 1);
424
+ }, [engaged, label]);
425
+ useEffect(() => {
426
+ if (!s.isHovered) { setHoverOpen(false); return undefined; }
427
+ const t = setTimeout(() => setHoverOpen(true), motionMs("--motion-duration-4", 300));
428
+ return () => clearTimeout(t);
429
+ }, [s.isHovered]);
430
+ const open = cut && typeof label === "string" && (!!s.isFocusVisible || hoverOpen);
431
+ return (
432
+ <Tooltip content={typeof label === "string" ? label : undefined} open={open} manageAria={false} align="start">
433
+ <span ref={ref} className="pw-menu-item__label">{label}</span>
434
+ </Tooltip>
435
+ );
436
+ }
437
+
382
438
  /* The one item anatomy: [stripe] [indicator + visual] [label, description or
383
439
  reason] [trailing]. The indicator is the real Checkbox or Radio, visual
384
440
  only: the item is the control and owns the role, the state and the target. */
@@ -413,10 +469,10 @@ function ItemContent({ p, s }) {
413
469
  <span className="pw-menu-item__text" data-inline={inline || undefined}>
414
470
  {p.badges && p.badges.length ? (
415
471
  <span className="pw-menu-item__label-row">
416
- <span className="pw-menu-item__label">{p.label}</span>
472
+ <ItemLabel label={p.label} s={s} />
417
473
  {p.badges.map((b) => <Badge key={b.label} label={b.label} status={b.status || "Neutral"} size="Medium" badgeStyle="Outline & Fill" />)}
418
474
  </span>
419
- ) : <span className="pw-menu-item__label">{p.label}</span>}
475
+ ) : <ItemLabel label={p.label} s={s} />}
420
476
  {inline ? <span className="pw-menu-item__desc">{p.inlineText}</span>
421
477
  : p.description ? <span className="pw-menu-item__desc">{p.description}</span> : null}
422
478
  {p.reason && (
@@ -634,6 +690,7 @@ export function PopoverMenuList({
634
690
  * @param {"none"|"single"|"multiple"} selectionMode, selectedKeys, onSelectionChange, onAction
635
691
  * @param {ReactNode} header, footer, emptyState See PopoverMenuList
636
692
  * @param {boolean} searchable The panel opens at the fixed 360 height and keeps it while the list filters
693
+ * @param {"float"|"sheet"} smallScreen Below 768: float from the trigger (default), or open as a bottom sheet. Pending decision G4
637
694
  */
638
695
  export function PopoverMenu({
639
696
  type = "icon",
@@ -657,6 +714,7 @@ export function PopoverMenu({
657
714
  footer,
658
715
  emptyState,
659
716
  searchable = false,
717
+ smallScreen = "float",
660
718
  maxHeight,
661
719
  className = "",
662
720
  style,
@@ -736,7 +794,8 @@ export function PopoverMenu({
736
794
  [RootMenuTriggerStateContext, state],
737
795
  [PopoverContext, { trigger: "MenuTrigger", triggerRef: wrap, placement: `bottom ${align}` }],
738
796
  ]}>
739
- <Popover className="pw-menu-panel" offset={L.offset} data-searchable={searchable || undefined}>
797
+ <Popover className="pw-menu-panel" offset={L.offset} data-searchable={searchable || undefined}
798
+ data-small-screen={smallScreen === "sheet" ? "sheet" : undefined}>
740
799
  {list}
741
800
  </Popover>
742
801
  </Provider>