@mario_hahn/jumpy-ui 0.10.0 → 0.12.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/README.md CHANGED
@@ -211,7 +211,7 @@ Jumpy Components als ein gemeinsames UI-Paket.
211
211
  Starting with 0.5.0, Jumpy UI uses the public npmjs registry. No package token is required to install:
212
212
 
213
213
  ```bash
214
- npm install @mario_hahn/jumpy-ui@^0.10.0
214
+ npm install @mario_hahn/jumpy-ui@^0.12.0
215
215
  ```
216
216
 
217
217
  The public npm package uses the `@mario_hahn` scope. Older GitHub Packages releases
@@ -324,6 +324,15 @@ import { JumpAppShell, JumpSideMenu, JumpSideMenuItem, JumpTabBar, JumpTabBarIte
324
324
  - `topBar.leading` defaults to `'menu'` when `navigation` exists; `'back'` calls
325
325
  `onBack`. `topBar.title` falls back to `brand`. `topBar.desktop` keeps the bar on
326
326
  desktop (without the menu button). `topBar={null}` removes it.
327
+ - `topBar.largeTitle` (0.11.0+) renders `title`/`subtitle` as a large page `h1`; on
328
+ mobile the bar stays empty and blends into the page until that heading scrolls under
329
+ it, then shows a compact centered title. Set `--jumpy-app-shell-gutter` to match your
330
+ content padding. Do not render a second page heading yourself.
331
+ - `topBar.actions` (0.12.0+): `[{ label, icon, onClick | href, variant }]`. Mobile shows
332
+ icon-only buttons (`label` becomes the accessible name); with three or more, the first
333
+ stays visible and the rest go into a "More actions" menu. Desktop shows labelled
334
+ buttons beside the large title, otherwise in the top bar. Prefer it over `trailing`
335
+ for ordinary actions; keep `trailing` for custom content such as an avatar.
327
336
  - `JumpTopBar` and `JumpTabBar` also work standalone. Both are sticky and include
328
337
  safe-area insets. `JumpTabBarItem` accepts `as` (e.g. Next `Link`), `href`, `icon`,
329
338
  numeric or custom `badge` and `selected` (`aria-current="page"`).
@@ -9945,7 +9945,8 @@ var providerStyles = `
9945
9945
  }
9946
9946
 
9947
9947
  /* An icon SVG fills the leading slot: otherwise a 20px icon is squeezed to the slot width but keeps its height and drops below the label. */
9948
- [data-jumpy-theme='true'] :is([data-jumpy-component='list-item'], [data-jumpy-component='menu-item'], [data-jumpy-component='side-menu-item']) > [data-jumpy-slot='leading'] > svg {
9948
+ [data-jumpy-theme='true'] :is([data-jumpy-component='list-item'], [data-jumpy-component='menu-item'], [data-jumpy-component='side-menu-item']) > [data-jumpy-slot='leading'] > svg,
9949
+ [data-jumpy-theme='true'] [data-jumpy-component='tab-bar-item'] > [data-jumpy-slot='tab-bar-item-icon'] > svg {
9949
9950
  width: 100%;
9950
9951
  height: 100%;
9951
9952
  flex: none;
@@ -11852,6 +11853,27 @@ var providerStyles = `
11852
11853
  flex: 0 0 auto;
11853
11854
  }
11854
11855
 
11856
+ /* Centered title (iOS navigation bar): equal side columns keep the title centered however many buttons sit left or right.
11857
+ Explicit columns so a hidden menu button does not shift the title. */
11858
+ [data-jumpy-theme='true'] [data-jumpy-component='top-bar'][data-title-align='center'] {
11859
+ display: grid;
11860
+ grid-template-columns: minmax(0, 1fr) minmax(0, max-content) minmax(0, 1fr);
11861
+ }
11862
+
11863
+ [data-jumpy-theme='true'] [data-jumpy-component='top-bar'][data-title-align='center'] > [data-jumpy-slot='top-bar-leading'] {
11864
+ grid-column: 1;
11865
+ justify-self: start;
11866
+ }
11867
+
11868
+ [data-jumpy-theme='true'] [data-jumpy-component='top-bar'][data-title-align='center'] > [data-jumpy-slot='top-bar-title'] {
11869
+ grid-column: 2;
11870
+ }
11871
+
11872
+ [data-jumpy-theme='true'] [data-jumpy-component='top-bar'][data-title-align='center'] > [data-jumpy-slot='top-bar-trailing'] {
11873
+ grid-column: 3;
11874
+ justify-self: end;
11875
+ }
11876
+
11855
11877
  [data-jumpy-theme='true'] [data-jumpy-component='tab-bar'] [data-jumpy-slot='tab-bar-surface'] {
11856
11878
  background-color: var(--jumpy-color-surface-card);
11857
11879
  }
@@ -11869,6 +11891,19 @@ var providerStyles = `
11869
11891
  box-shadow: var(--jumpy-shadow-positive-small);
11870
11892
  }
11871
11893
 
11894
+ [data-jumpy-theme='true'] [data-jumpy-component='top-bar'] > [data-jumpy-slot='top-bar-title'] {
11895
+ transition: opacity var(--jumpy-animation-duration-xsmall) var(--jumpy-animation-easing-drift);
11896
+ }
11897
+
11898
+ /* Large-title mode: until the page title scrolls under it, the bar blends into the page (iOS). */
11899
+ [data-jumpy-theme='true'] [data-jumpy-component='top-bar'][data-large-title='true'][data-title-hidden='true'] {
11900
+ background-color: var(--jumpy-color-surface-canvas);
11901
+ border-bottom-color: transparent;
11902
+ box-shadow: none;
11903
+ -webkit-backdrop-filter: none;
11904
+ backdrop-filter: none;
11905
+ }
11906
+
11872
11907
  [data-jumpy-theme='true'] [data-jumpy-component='tab-bar-item'] {
11873
11908
  transition: background-color var(--jumpy-animation-duration-xsmall) var(--jumpy-animation-easing-drift), color var(--jumpy-animation-duration-xsmall) var(--jumpy-animation-easing-drift);
11874
11909
  -webkit-tap-highlight-color: transparent;
@@ -11887,13 +11922,43 @@ var providerStyles = `
11887
11922
  animation: jumpy-app-shell-drawer-in var(--jumpy-animation-duration-small) var(--jumpy-animation-easing-drift);
11888
11923
  }
11889
11924
 
11925
+ [data-jumpy-theme='true'] [data-jumpy-component='app-shell-drawer'][data-state='closing'] {
11926
+ animation: jumpy-app-shell-drawer-out var(--jumpy-animation-duration-small) var(--jumpy-animation-easing-release) forwards;
11927
+ pointer-events: none;
11928
+ }
11929
+
11930
+ [data-jumpy-theme='true'] [data-jumpy-component='app-shell-drawer-overlay'] {
11931
+ animation: jumpy-app-shell-overlay-in var(--jumpy-animation-duration-small) var(--jumpy-animation-easing-drift);
11932
+ }
11933
+
11934
+ [data-jumpy-theme='true'] [data-jumpy-component='app-shell-drawer-overlay'][data-state='closing'] {
11935
+ animation: jumpy-app-shell-overlay-out var(--jumpy-animation-duration-small) var(--jumpy-animation-easing-release) forwards;
11936
+ pointer-events: none;
11937
+ }
11938
+
11890
11939
  @keyframes jumpy-app-shell-drawer-in {
11891
11940
  from { transform: translateX(-100%); }
11892
11941
  to { transform: translateX(0); }
11893
11942
  }
11894
11943
 
11944
+ @keyframes jumpy-app-shell-drawer-out {
11945
+ from { transform: translateX(0); }
11946
+ to { transform: translateX(-100%); }
11947
+ }
11948
+
11949
+ @keyframes jumpy-app-shell-overlay-in {
11950
+ from { opacity: 0; }
11951
+ to { opacity: 1; }
11952
+ }
11953
+
11954
+ @keyframes jumpy-app-shell-overlay-out {
11955
+ from { opacity: 1; }
11956
+ to { opacity: 0; }
11957
+ }
11958
+
11895
11959
  @media (prefers-reduced-motion: reduce) {
11896
- [data-jumpy-theme='true'] [data-jumpy-component='app-shell-drawer'] {
11960
+ [data-jumpy-theme='true'] [data-jumpy-component='app-shell-drawer'],
11961
+ [data-jumpy-theme='true'] [data-jumpy-component='app-shell-drawer-overlay'] {
11897
11962
  animation: none;
11898
11963
  }
11899
11964
  }
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  textStyleValue,
29
29
  themeToCSSVariables,
30
30
  useJumpyTheme
31
- } from "./chunk-LORNGSIB.js";
31
+ } from "./chunk-3PHOJLCH.js";
32
32
  import {
33
33
  cn
34
34
  } from "./chunk-3DBKYKP2.js";
@@ -1642,7 +1642,7 @@ function JumpPopover({ open, defaultOpen = false, onOpenChange, trigger, childre
1642
1642
  visible ? /* @__PURE__ */ jsx2("div", { role: "dialog", "data-jumpy-component": "popover", style: { position: "absolute", top: `calc(100% + ${spacingValue(theme, JumpySpacing.smallXS)}px)`, left: 0, zIndex: 30, minWidth: 220, borderRadius: radiusValue(theme, JumpyRadius.large), border: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid ${theme.color.borderSubtle}`, backgroundColor: theme.color.surfaceCard, color: theme.color.textPrimary, boxShadow: theme.shadow.positiveMedium, padding: spacingValue(theme, JumpySpacing.large) }, children }) : null
1643
1643
  ] });
1644
1644
  }
1645
- function JumpMenu({ trigger, open, defaultOpen = false, onOpenChange, header, footer, children, className = "", placement = "bottom-start", closeOnSelect = true, ...props }) {
1645
+ function JumpMenu({ trigger, triggerProps = {}, open, defaultOpen = false, onOpenChange, header, footer, children, className = "", placement = "bottom-start", closeOnSelect = true, ...props }) {
1646
1646
  const theme = useJumpyTheme();
1647
1647
  const [internalOpen, setInternalOpen] = useState2(defaultOpen);
1648
1648
  const isOpen = open ?? internalOpen;
@@ -1748,7 +1748,7 @@ function JumpMenu({ trigger, open, defaultOpen = false, onOpenChange, header, fo
1748
1748
  refs.domReference.current?.focus?.();
1749
1749
  }
1750
1750
  return /* @__PURE__ */ jsxs2("span", { "data-jumpy-component": "menu-root", className: cn(className), onPointerDown: markInteractionInside, onFocus: markInteractionInside, style: { display: "inline-flex", ...props.style || {} }, children: [
1751
- /* @__PURE__ */ jsx2("span", { ref: refs.setReference, "aria-haspopup": "menu", "aria-expanded": isOpen, tabIndex: 0, onClick: () => setOpen(!isOpen), onKeyDown: handleTriggerKeyDown, children: trigger }),
1751
+ /* @__PURE__ */ jsx2("span", { ...triggerProps, ref: refs.setReference, "aria-haspopup": "menu", "aria-expanded": isOpen, tabIndex: 0, onClick: () => setOpen(!isOpen), onKeyDown: handleTriggerKeyDown, children: trigger }),
1752
1752
  isOpen ? /* @__PURE__ */ jsx2(JumpyPortal, { children: /* @__PURE__ */ jsxs2(
1753
1753
  "div",
1754
1754
  {
@@ -2412,11 +2412,12 @@ function JumpyNavigationIcon({ type }) {
2412
2412
  const paths = {
2413
2413
  menu: "M4 6h16M4 12h16M4 18h16",
2414
2414
  close: "M6 6l12 12M18 6 6 18",
2415
- back: "M15 18l-6-6 6-6"
2415
+ back: "M15 18l-6-6 6-6",
2416
+ more: "M5 12h.01M12 12h.01M19 12h.01"
2416
2417
  };
2417
- return /* @__PURE__ */ jsx2("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", "data-jumpy-stroke-icon": "true", style: { display: "block" }, children: /* @__PURE__ */ jsx2("path", { d: paths[type] }) });
2418
+ return /* @__PURE__ */ jsx2("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: type === "more" ? 3 : 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", "data-jumpy-stroke-icon": "true", style: { display: "block" }, children: /* @__PURE__ */ jsx2("path", { d: paths[type] }) });
2418
2419
  }
2419
- function JumpTopBar({ leading, onMenu, onBack, menuOpen = false, title, subtitle, trailing, sticky = true, ariaLabels = {}, className = "", ...props }) {
2420
+ function JumpTopBar({ leading, onMenu, onBack, menuOpen = false, title, subtitle, trailing, sticky = true, titleAlign = "start", titleHidden = false, ariaLabels = {}, className = "", ...props }) {
2420
2421
  const theme = useJumpyTheme();
2421
2422
  const [scrolled, setScrolled] = useState2(false);
2422
2423
  const titleStyle = textStyleValue(theme, JumpyTextStyle.bodyLargeEmphasized);
@@ -2437,6 +2438,8 @@ function JumpTopBar({ leading, onMenu, onBack, menuOpen = false, title, subtitle
2437
2438
  "data-jumpy-component": "top-bar",
2438
2439
  "data-sticky": sticky ? "true" : "false",
2439
2440
  "data-scrolled": scrolled ? "true" : "false",
2441
+ "data-title-align": titleAlign,
2442
+ "data-title-hidden": titleHidden ? "true" : "false",
2440
2443
  className: cn(className),
2441
2444
  style: {
2442
2445
  boxSizing: "border-box",
@@ -2453,11 +2456,18 @@ function JumpTopBar({ leading, onMenu, onBack, menuOpen = false, title, subtitle
2453
2456
  },
2454
2457
  children: [
2455
2458
  leadingNode ? /* @__PURE__ */ jsx2("span", { "data-jumpy-slot": "top-bar-leading", "data-leading": leadingType, style: { marginLeft: -spacingValue(theme, JumpySpacing.smallXS) }, children: leadingNode }) : null,
2456
- /* @__PURE__ */ jsxs2("span", { "data-jumpy-slot": "top-bar-title", style: { display: "flex", flexDirection: "column", flex: "1 1 auto", minWidth: 0 }, children: [
2457
- title ? /* @__PURE__ */ jsx2("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: titleStyle.fontSize, lineHeight: titleStyle.lineHeight, fontWeight: titleStyle.fontWeight }, children: title }) : null,
2458
- subtitle ? /* @__PURE__ */ jsx2("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--jumpy-color-text-secondary)", fontSize: subtitleStyle.fontSize, lineHeight: subtitleStyle.lineHeight }, children: subtitle }) : null
2459
+ /* @__PURE__ */ jsxs2("span", { "data-jumpy-slot": "top-bar-title", "aria-hidden": titleHidden || void 0, style: titleAlign === "center" ? {
2460
+ display: "flex",
2461
+ flexDirection: "column",
2462
+ alignItems: "center",
2463
+ textAlign: "center",
2464
+ minWidth: 0,
2465
+ opacity: titleHidden ? 0 : 1
2466
+ } : { display: "flex", flexDirection: "column", flex: "1 1 auto", minWidth: 0, opacity: titleHidden ? 0 : 1 }, children: [
2467
+ title ? /* @__PURE__ */ jsx2("span", { style: { maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: titleStyle.fontSize, lineHeight: titleStyle.lineHeight, fontWeight: titleStyle.fontWeight }, children: title }) : null,
2468
+ subtitle ? /* @__PURE__ */ jsx2("span", { style: { maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--jumpy-color-text-secondary)", fontSize: subtitleStyle.fontSize, lineHeight: subtitleStyle.lineHeight }, children: subtitle }) : null
2459
2469
  ] }),
2460
- trailing ? /* @__PURE__ */ jsx2("span", { "data-jumpy-slot": "top-bar-trailing", style: { display: "inline-flex", flex: "0 0 auto", alignItems: "center", gap: spacingValue(theme, JumpySpacing.smallXS) }, children: trailing }) : null
2470
+ trailing ? /* @__PURE__ */ jsx2("span", { "data-jumpy-slot": "top-bar-trailing", style: { display: "inline-flex", flex: "0 0 auto", marginLeft: "auto", alignItems: "center", gap: spacingValue(theme, JumpySpacing.smallXS) }, children: trailing }) : null
2461
2471
  ]
2462
2472
  }
2463
2473
  );
@@ -2504,7 +2514,7 @@ function JumpTabBar({ children, className = "", "aria-label": ariaLabel = "Main
2504
2514
  function JumpTabBarItem({ as, icon, badge, selected = false, children, className = "", ...props }) {
2505
2515
  const theme = useJumpyTheme();
2506
2516
  const Component = as || (props.href ? "a" : "button");
2507
- const labelStyle = textStyleValue(theme, JumpyTextStyle.labelSmallEmphasized);
2517
+ const labelStyle = textStyleValue(theme, JumpyTextStyle.labelSmall);
2508
2518
  const badgeNode = typeof badge === "number" ? badge > 0 ? /* @__PURE__ */ jsx2(JumpBadge, { size: "small", status: "active", "aria-label": void 0, role: "presentation", "aria-live": "off", children: badge > 99 ? "99+" : badge }) : null : badge;
2509
2519
  return /* @__PURE__ */ jsxs2(
2510
2520
  Component,
@@ -2539,7 +2549,7 @@ function JumpTabBarItem({ as, icon, badge, selected = false, children, className
2539
2549
  ...props.style || {}
2540
2550
  },
2541
2551
  children: [
2542
- icon ? /* @__PURE__ */ jsxs2("span", { "data-jumpy-slot": "tab-bar-item-icon", "aria-hidden": "true", style: { position: "relative", display: "inline-flex", width: sizeValue(theme, JumpySize.small2XS), height: sizeValue(theme, JumpySize.small2XS) }, children: [
2552
+ icon ? /* @__PURE__ */ jsxs2("span", { "data-jumpy-slot": "tab-bar-item-icon", "aria-hidden": "true", style: { position: "relative", display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0, width: sizeValue(theme, JumpySize.smallXS), height: sizeValue(theme, JumpySize.smallXS) }, children: [
2543
2553
  icon,
2544
2554
  badgeNode ? /* @__PURE__ */ jsx2("span", { "data-jumpy-slot": "tab-bar-item-badge", style: { position: "absolute", top: -spacingValue(theme, JumpySpacing.smallXS), left: "50%", marginLeft: spacingValue(theme, JumpySpacing.small3XS), display: "inline-flex" }, children: badgeNode }) : null
2545
2555
  ] }) : null,
@@ -2548,6 +2558,28 @@ function JumpTabBarItem({ as, icon, badge, selected = false, children, className
2548
2558
  }
2549
2559
  );
2550
2560
  }
2561
+ function JumpyAppShellActions({ actions, variant, ariaLabels }) {
2562
+ const theme = useJumpyTheme();
2563
+ if (!actions.length) return null;
2564
+ const gap = spacingValue(theme, JumpySpacing.smallXS);
2565
+ if (variant === "full") {
2566
+ return /* @__PURE__ */ jsx2("span", { "data-jumpy-slot": "app-shell-actions", "data-variant": "full", style: { alignItems: "center", gap, flexWrap: "wrap", justifyContent: "flex-end" }, children: actions.map((action, index) => /* @__PURE__ */ jsx2(JumpButton, { as: action.href ? "a" : void 0, href: action.href, variant: action.variant || "secondary", leadingIcon: action.icon, disabled: action.disabled, onClick: action.onClick, children: action.label }, action.id ?? index)) });
2567
+ }
2568
+ const visible = actions.length > 2 ? actions.slice(0, 1) : actions;
2569
+ const overflow = actions.length > 2 ? actions.slice(1) : [];
2570
+ return /* @__PURE__ */ jsxs2("span", { "data-jumpy-slot": "app-shell-actions", "data-variant": "compact", style: { alignItems: "center", gap }, children: [
2571
+ visible.map((action, index) => /* @__PURE__ */ jsx2(JumpIconButton, { as: action.href ? "a" : void 0, href: action.href, variant: action.variant === "primary" ? "primary" : "borderless", "aria-label": action.label, disabled: action.disabled, onClick: action.onClick, children: action.icon }, action.id ?? index)),
2572
+ overflow.length ? /* @__PURE__ */ jsx2(
2573
+ JumpMenu,
2574
+ {
2575
+ placement: "bottom-end",
2576
+ triggerProps: { role: "button", "aria-label": ariaLabels.moreActions || "More actions", "data-jumpy-slot": "app-shell-actions-more" },
2577
+ trigger: /* @__PURE__ */ jsx2(JumpIconButton, { as: "span", variant: "borderless", tabIndex: -1, "aria-hidden": "true", children: /* @__PURE__ */ jsx2(JumpyNavigationIcon, { type: "more" }) }),
2578
+ children: overflow.map((action, index) => /* @__PURE__ */ jsx2(JumpMenuItem, { as: action.href ? "a" : void 0, href: action.href, leading: action.icon, disabled: action.disabled, onClick: action.onClick, children: action.label }, action.id ?? index))
2579
+ }
2580
+ ) : null
2581
+ ] });
2582
+ }
2551
2583
  function JumpAppShell({ brand, navigation, sidebarFooter, topBar, tabBar, hideTabBar = false, breakpoint = 1024, sidebarWidth = 256, navigationOpen, defaultNavigationOpen = false, onNavigationOpenChange, ariaLabels = {}, children, className = "", ...props }) {
2552
2584
  const theme = useJumpyTheme();
2553
2585
  const shellId = useId();
@@ -2555,8 +2587,15 @@ function JumpAppShell({ brand, navigation, sidebarFooter, topBar, tabBar, hideTa
2555
2587
  const isOpen = Boolean(navigation) && (navigationOpen ?? internalOpen);
2556
2588
  const swipeStart = useRef2(null);
2557
2589
  const rootRef = useRef2(null);
2558
- const wasOpen = useRef2(isOpen);
2559
- const { desktop: topBarOnDesktop = false, leading, ...topBarProps } = topBar || {};
2590
+ const [drawerMounted, setDrawerMounted] = useState2(isOpen);
2591
+ const showDrawer = isOpen || drawerMounted;
2592
+ const closing = drawerMounted && !isOpen;
2593
+ const wasShown = useRef2(showDrawer);
2594
+ const { desktop: topBarDesktop = false, leading, largeTitle = false, actions = [], ...topBarProps } = topBar || {};
2595
+ const showLargeTitle = largeTitle && Boolean(topBarProps.title);
2596
+ const topBarOnDesktop = topBarDesktop || actions.length > 0 && !showLargeTitle;
2597
+ const largeTitleRef = useRef2(null);
2598
+ const [largeTitleCovered, setLargeTitleCovered] = useState2(false);
2560
2599
  const resolvedLeading = leading ?? (navigation ? "menu" : void 0);
2561
2600
  const shell = `[data-jumpy-app-shell="${shellId}"]`;
2562
2601
  const width = typeof sidebarWidth === "number" ? `${sidebarWidth}px` : sidebarWidth;
@@ -2575,15 +2614,37 @@ function JumpAppShell({ brand, navigation, sidebarFooter, topBar, tabBar, hideTa
2575
2614
  return () => desktop.removeEventListener("change", closeOnDesktop);
2576
2615
  });
2577
2616
  useEffect2(() => {
2578
- const closed = wasOpen.current && !isOpen;
2579
- wasOpen.current = isOpen;
2617
+ if (isOpen) {
2618
+ setDrawerMounted(true);
2619
+ return void 0;
2620
+ }
2621
+ if (!drawerMounted) return void 0;
2622
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
2623
+ setDrawerMounted(false);
2624
+ return void 0;
2625
+ }
2626
+ const timer = window.setTimeout(() => setDrawerMounted(false), animationDurationValue(theme, JumpyAnimationDuration.small) * 1e3 + 100);
2627
+ return () => window.clearTimeout(timer);
2628
+ }, [isOpen, drawerMounted]);
2629
+ useEffect2(() => {
2630
+ const closed = wasShown.current && !showDrawer;
2631
+ wasShown.current = showDrawer;
2580
2632
  if (!closed) return void 0;
2581
2633
  const frame = window.requestAnimationFrame(() => {
2582
2634
  if (document.activeElement && document.activeElement !== document.body) return;
2583
2635
  rootRef.current?.querySelector(':scope > [data-jumpy-slot="app-shell-body"] > [data-jumpy-component="top-bar"] > [data-leading="menu"] button')?.focus();
2584
2636
  });
2585
2637
  return () => window.cancelAnimationFrame(frame);
2586
- }, [isOpen]);
2638
+ }, [showDrawer]);
2639
+ useEffect2(() => {
2640
+ const element = largeTitleRef.current;
2641
+ if (!showLargeTitle || !element || typeof IntersectionObserver === "undefined") return void 0;
2642
+ const bar = rootRef.current?.querySelector(':scope > [data-jumpy-slot="app-shell-body"] > [data-jumpy-component="top-bar"]');
2643
+ const barHeight = bar && bar.getBoundingClientRect().height > 0 ? bar.getBoundingClientRect().height : 0;
2644
+ const observer = new IntersectionObserver(([entry]) => setLargeTitleCovered(!entry.isIntersecting), { rootMargin: `-${Math.round(barHeight)}px 0px 0px 0px` });
2645
+ observer.observe(element);
2646
+ return () => observer.disconnect();
2647
+ }, [showLargeTitle, topBarProps.title]);
2587
2648
  function closeOnNavigate(event) {
2588
2649
  const item = event.target.closest?.('a[href], [data-jumpy-component="side-menu-item"]');
2589
2650
  if (!item || item.hasAttribute("aria-expanded") || item.getAttribute("aria-disabled") === "true" || item.disabled) return;
@@ -2592,9 +2653,13 @@ function JumpAppShell({ brand, navigation, sidebarFooter, topBar, tabBar, hideTa
2592
2653
  const shellStyles = `
2593
2654
  ${shell} { grid-template-columns: minmax(0, 1fr); }
2594
2655
  ${shell} > [data-jumpy-slot='app-shell-sidebar'] { display: none; }
2656
+ ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='compact'] { display: inline-flex; }
2657
+ ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='full'] { display: none; }
2595
2658
  @media (min-width: ${breakpoint}px) {
2596
2659
  ${shell} { grid-template-columns: ${width} minmax(0, 1fr); }
2597
2660
  ${shell} > [data-jumpy-slot='app-shell-sidebar'] { display: flex; }
2661
+ ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='compact'] { display: none; }
2662
+ ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='full'] { display: inline-flex; }
2598
2663
  ${shell} > [data-jumpy-slot='app-shell-body'] > [data-jumpy-component='tab-bar'] { display: none; }
2599
2664
  ${shell} > [data-jumpy-slot='app-shell-body'] > [data-jumpy-component='top-bar'][data-mobile-only='true'] { display: none; }
2600
2665
  ${shell} > [data-jumpy-slot='app-shell-body'] > [data-jumpy-component='top-bar'] > [data-leading='menu'] { display: none; }
@@ -2608,6 +2673,15 @@ ${shell} > [data-jumpy-slot='app-shell-sidebar'] { display: none; }
2608
2673
  menuOpen: isOpen,
2609
2674
  onMenu: () => setOpen(true),
2610
2675
  title: topBarProps.title ?? brand,
2676
+ subtitle: showLargeTitle ? void 0 : topBarProps.subtitle,
2677
+ titleAlign: showLargeTitle ? "center" : topBarProps.titleAlign,
2678
+ titleHidden: showLargeTitle ? !largeTitleCovered : topBarProps.titleHidden,
2679
+ "data-large-title": showLargeTitle ? "true" : void 0,
2680
+ trailing: actions.length || topBarProps.trailing ? /* @__PURE__ */ jsxs2(Fragment, { children: [
2681
+ /* @__PURE__ */ jsx2(JumpyAppShellActions, { actions, variant: "compact", ariaLabels }),
2682
+ showLargeTitle ? null : /* @__PURE__ */ jsx2(JumpyAppShellActions, { actions, variant: "full", ariaLabels }),
2683
+ topBarProps.trailing
2684
+ ] }) : void 0,
2611
2685
  ariaLabels: { menu: ariaLabels.openNavigation, back: ariaLabels.back, ...topBarProps.ariaLabels || {} }
2612
2686
  }
2613
2687
  );
@@ -2641,29 +2715,42 @@ ${shell} > [data-jumpy-slot='app-shell-sidebar'] { display: none; }
2641
2715
  top: 0,
2642
2716
  height: "100dvh",
2643
2717
  flexDirection: "column",
2644
- overflowY: "auto",
2718
+ overflow: "hidden",
2645
2719
  borderRight: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid var(--jumpy-color-border-subtle)`,
2646
2720
  backgroundColor: "var(--jumpy-color-surface-card)"
2647
2721
  },
2648
2722
  children: [
2649
- brand ? /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-brand", style: { padding: spacingValue(theme, JumpySpacing.large2XL), borderBottom: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid var(--jumpy-color-border-subtle)` }, children: brand }) : null,
2650
- navigation ? /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-navigation", style: { flex: 1, padding: spacingValue(theme, JumpySpacing.large) }, children: navigation }) : null,
2651
- sidebarFooter ? /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-sidebar-footer", style: { padding: spacingValue(theme, JumpySpacing.large), borderTop: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid var(--jumpy-color-border-subtle)` }, children: sidebarFooter }) : null
2723
+ brand ? /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-brand", style: { flex: "0 0 auto", padding: spacingValue(theme, JumpySpacing.large2XL), borderBottom: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid var(--jumpy-color-border-subtle)` }, children: brand }) : null,
2724
+ navigation ? /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-navigation", style: { flex: "1 1 auto", minHeight: 0, overflowY: "auto", overscrollBehavior: "contain", padding: spacingValue(theme, JumpySpacing.large) }, children: navigation }) : null,
2725
+ sidebarFooter ? /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-sidebar-footer", style: { flex: "0 0 auto", padding: spacingValue(theme, JumpySpacing.large), borderTop: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid var(--jumpy-color-border-subtle)` }, children: sidebarFooter }) : null
2652
2726
  ]
2653
2727
  }
2654
2728
  ),
2655
2729
  /* @__PURE__ */ jsxs2("div", { "data-jumpy-slot": "app-shell-body", style: { display: "flex", flexDirection: "column", minWidth: 0, minHeight: "100dvh" }, children: [
2656
2730
  topBar !== null ? topBarNode : null,
2657
- /* @__PURE__ */ jsx2("main", { "data-jumpy-slot": "app-shell-content", style: { flex: 1, minWidth: 0 }, children }),
2731
+ /* @__PURE__ */ jsxs2("main", { "data-jumpy-slot": "app-shell-content", style: { flex: 1, minWidth: 0 }, children: [
2732
+ showLargeTitle ? /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-large-title", style: { padding: `${spacingValue(theme, JumpySpacing.smallXS)}px calc(var(--jumpy-app-shell-gutter, ${spacingValue(theme, JumpySpacing.large)}px) + env(safe-area-inset-right, 0px)) 0 calc(var(--jumpy-app-shell-gutter, ${spacingValue(theme, JumpySpacing.large)}px) + env(safe-area-inset-left, 0px))` }, children: /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: spacingValue(theme, JumpySpacing.large), flexWrap: "wrap" }, children: [
2733
+ /* @__PURE__ */ jsxs2("div", { style: { minWidth: 0 }, children: [
2734
+ /* @__PURE__ */ jsx2("h1", { ref: largeTitleRef, style: { margin: 0, ...textStyleValue(theme, JumpyTextStyle.displaySmallEmphasized), overflowWrap: "anywhere" }, children: topBarProps.title }),
2735
+ topBarProps.subtitle ? /* @__PURE__ */ jsx2("p", { style: { margin: `${spacingValue(theme, JumpySpacing.small4XS)}px 0 0`, color: "var(--jumpy-color-text-secondary)", ...textStyleValue(theme, JumpyTextStyle.bodyMedium) }, children: topBarProps.subtitle }) : null
2736
+ ] }),
2737
+ /* @__PURE__ */ jsx2(JumpyAppShellActions, { actions, variant: "full", ariaLabels })
2738
+ ] }) }) : null,
2739
+ children
2740
+ ] }),
2658
2741
  tabBar && !hideTabBar ? tabBar : null
2659
2742
  ] }),
2660
- isOpen ? /* @__PURE__ */ jsx2(JumpyModalLayer, { onVisibleChange: setOpen, "data-jumpy-component": "app-shell-drawer-overlay", style: { position: "fixed", inset: 0, zIndex: 60, backgroundColor: "var(--jumpy-color-surface-overlay)" }, children: /* @__PURE__ */ jsxs2(
2743
+ showDrawer ? /* @__PURE__ */ jsx2(JumpyModalLayer, { onVisibleChange: setOpen, "data-jumpy-component": "app-shell-drawer-overlay", "data-state": closing ? "closing" : "open", style: { position: "fixed", inset: 0, zIndex: 60, backgroundColor: "var(--jumpy-color-surface-overlay)" }, children: /* @__PURE__ */ jsxs2(
2661
2744
  "div",
2662
2745
  {
2663
2746
  role: "dialog",
2664
2747
  "aria-modal": "true",
2665
2748
  "aria-label": ariaLabels.navigation || "Navigation",
2666
2749
  "data-jumpy-component": "app-shell-drawer",
2750
+ "data-state": closing ? "closing" : "open",
2751
+ onAnimationEnd: (event) => {
2752
+ if (closing && event.target === event.currentTarget) setDrawerMounted(false);
2753
+ },
2667
2754
  onClick: closeOnNavigate,
2668
2755
  onTouchStart: (event) => {
2669
2756
  const touch = event.touches[0];
@@ -2686,20 +2773,25 @@ ${shell} > [data-jumpy-slot='app-shell-sidebar'] { display: none; }
2686
2773
  flexDirection: "column",
2687
2774
  width: "100vw",
2688
2775
  height: "100dvh",
2689
- overflowY: "auto",
2776
+ overflow: "hidden",
2690
2777
  outline: "none",
2691
2778
  backgroundColor: "var(--jumpy-color-surface-card)",
2692
2779
  color: "var(--jumpy-color-text-primary)",
2693
- fontFamily: theme.fontFamily.familyBase,
2694
- paddingBottom: "env(safe-area-inset-bottom, 0px)"
2780
+ fontFamily: theme.fontFamily.familyBase
2695
2781
  },
2696
2782
  children: [
2697
- /* @__PURE__ */ jsxs2("div", { "data-jumpy-slot": "app-shell-drawer-header", style: { boxSizing: "border-box", display: "grid", gridTemplateColumns: "auto minmax(0, 1fr)", alignItems: "center", gap: spacingValue(theme, JumpySpacing.medium), minHeight: `calc(${sizeValue(theme, JumpySize.large2XL)}px + env(safe-area-inset-top, 0px))`, padding: `calc(${spacingValue(theme, JumpySpacing.smallXS)}px + env(safe-area-inset-top, 0px)) calc(${spacingValue(theme, JumpySpacing.large)}px + env(safe-area-inset-right, 0px)) ${spacingValue(theme, JumpySpacing.smallXS)}px calc(${spacingValue(theme, JumpySpacing.large)}px + env(safe-area-inset-left, 0px))`, borderBottom: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid var(--jumpy-color-border-subtle)` }, children: [
2698
- /* @__PURE__ */ jsx2("span", { style: { display: "inline-flex", marginLeft: -spacingValue(theme, JumpySpacing.smallXS) }, children: /* @__PURE__ */ jsx2(JumpIconButton, { variant: "borderless", "aria-label": ariaLabels.closeNavigation || "Close navigation", onClick: () => setOpen(false), children: /* @__PURE__ */ jsx2(JumpyNavigationIcon, { type: "close" }) }) }),
2699
- /* @__PURE__ */ jsx2("span", { style: { minWidth: 0 }, children: brand })
2700
- ] }),
2701
- /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-drawer-navigation", style: { flex: 1, padding: spacingValue(theme, JumpySpacing.large) }, children: navigation }),
2702
- sidebarFooter ? /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-drawer-footer", style: { padding: spacingValue(theme, JumpySpacing.large), borderTop: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid var(--jumpy-color-border-subtle)` }, children: sidebarFooter }) : null
2783
+ /* @__PURE__ */ jsx2(
2784
+ JumpTopBar,
2785
+ {
2786
+ "data-jumpy-slot": "app-shell-drawer-header",
2787
+ sticky: false,
2788
+ style: { flex: "0 0 auto" },
2789
+ leading: /* @__PURE__ */ jsx2(JumpIconButton, { variant: "borderless", "aria-label": ariaLabels.closeNavigation || "Close navigation", onClick: () => setOpen(false), children: /* @__PURE__ */ jsx2(JumpyNavigationIcon, { type: "close" }) }),
2790
+ title: brand
2791
+ }
2792
+ ),
2793
+ /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-drawer-navigation", style: { flex: "1 1 auto", minHeight: 0, overflowY: "auto", overscrollBehavior: "contain", padding: spacingValue(theme, JumpySpacing.large) }, children: navigation }),
2794
+ sidebarFooter ? /* @__PURE__ */ jsx2("div", { "data-jumpy-slot": "app-shell-drawer-footer", style: { flex: "0 0 auto", padding: `${spacingValue(theme, JumpySpacing.large)}px calc(${spacingValue(theme, JumpySpacing.large)}px + env(safe-area-inset-right, 0px)) calc(${spacingValue(theme, JumpySpacing.large)}px + env(safe-area-inset-bottom, 0px)) calc(${spacingValue(theme, JumpySpacing.large)}px + env(safe-area-inset-left, 0px))`, borderTop: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid var(--jumpy-color-border-subtle)`, backgroundColor: "var(--jumpy-color-surface-card)" }, children: sidebarFooter }) : null
2703
2795
  ]
2704
2796
  }
2705
2797
  ) }) : null
package/dist/theme.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  textStyleValue,
29
29
  themeToCSSVariables,
30
30
  useJumpyTheme
31
- } from "./chunk-LORNGSIB.js";
31
+ } from "./chunk-3PHOJLCH.js";
32
32
  export {
33
33
  JumpyAnimation,
34
34
  JumpyAnimationDelay,
package/index.d.ts CHANGED
@@ -71,7 +71,7 @@ export declare function JumpSkeletonLoader(props: HTMLAttributes<HTMLElement> &
71
71
  export declare const JumpSearchField: import('react').ForwardRefExoticComponent<Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> & { label?: string } & import('react').RefAttributes<HTMLInputElement>>
72
72
  export declare function JumpTooltip(props: HTMLAttributes<HTMLSpanElement> & { content: ReactNode; children: ReactNode }): ReactNode
73
73
  export declare function JumpPopover(props: HTMLAttributes<HTMLSpanElement> & { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; trigger: ReactNode; children: ReactNode }): ReactNode
74
- export declare function JumpMenu(props: HTMLAttributes<HTMLDivElement> & { trigger: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; closeOnSelect?: boolean; header?: ReactNode; footer?: ReactNode; placement?: 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end'; children: ReactNode }): ReactNode
74
+ export declare function JumpMenu(props: HTMLAttributes<HTMLDivElement> & { trigger: ReactNode; triggerProps?: HTMLAttributes<HTMLSpanElement> & Record<string, unknown>; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; closeOnSelect?: boolean; header?: ReactNode; footer?: ReactNode; placement?: 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end'; children: ReactNode }): ReactNode
75
75
  export declare function JumpMenuItem(props: HTMLAttributes<HTMLElement> & { as?: ElementTag; children: ReactNode; selected?: boolean; disabled?: boolean; readOnly?: boolean; focusedView?: boolean; leading?: ReactNode; trailing?: ReactNode; metadata?: ReactNode; description?: ReactNode; slots?: JumpListItemSlots; nestedMenu?: ReactNode }): ReactNode
76
76
  export type JumpToggleButtonValue = string | number
77
77
  export declare function JumpToggleButton(props: Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> & { as?: ElementTag; selected?: boolean; value?: JumpToggleButtonValue; onChange?: (selected: boolean) => void; leading?: ReactNode | ((state: { selected: boolean }) => ReactNode); children?: ReactNode }): ReactNode
@@ -111,13 +111,14 @@ export declare function JumpTableCell(props: Omit<TdHTMLAttributes<HTMLTableCell
111
111
  export declare function JumpTableHeaderCell(props: Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> & { as?: ElementTag; ref?: Ref<HTMLTableCellElement>; selected?: boolean; disabled?: boolean; align?: JumpTableAlign; variant?: 'default' | 'control'; padding?: 'default' | 'none' | 'checkbox'; showHorizontalBorder?: boolean; pinned?: JumpTablePinned; pinnedEdge?: boolean; sortable?: boolean; sortOrder?: JumpTableSortOrder; onSortChange?: (sortOrder: JumpTableSortOrder) => void; ariaLabels?: { sort?: string | ((sortOrder: JumpTableSortOrder) => string) }; leading?: ReactNode; trailing?: ReactNode; slots?: { leading?: ReactNode; trailing?: ReactNode }; innerClasses?: JumpTableCellInnerClasses; children?: ReactNode }): ReactNode
112
112
  export declare function JumpSheet(props: HTMLAttributes<HTMLElement> & { visible?: boolean; onVisibleChange?: (visible: boolean) => void; position?: 'top' | 'right' | 'bottom' | 'left'; title?: string; subtitle?: string; footer?: ReactNode; children?: ReactNode }): ReactNode
113
113
  export type JumpTopBarLeading = 'menu' | 'back' | ReactNode
114
- export type JumpTopBarProps = Omit<HTMLAttributes<HTMLElement>, 'title'> & { leading?: JumpTopBarLeading; onMenu?: () => void; onBack?: () => void; menuOpen?: boolean; title?: ReactNode; subtitle?: ReactNode; trailing?: ReactNode; sticky?: boolean; ariaLabels?: { menu?: string; back?: string } }
114
+ export type JumpTopBarProps = Omit<HTMLAttributes<HTMLElement>, 'title'> & { leading?: JumpTopBarLeading; onMenu?: () => void; onBack?: () => void; menuOpen?: boolean; title?: ReactNode; subtitle?: ReactNode; trailing?: ReactNode; sticky?: boolean; titleAlign?: 'start' | 'center'; titleHidden?: boolean; ariaLabels?: { menu?: string; back?: string } }
115
115
  export declare function JumpTopBar(props: JumpTopBarProps): ReactNode
116
116
  export declare function JumpTabBar(props: HTMLAttributes<HTMLElement> & { children: ReactNode }): ReactNode
117
117
  export type JumpTabBarItemProps = { as?: ElementType; href?: string; icon?: ReactNode; badge?: ReactNode | number; selected?: boolean; children: ReactNode; className?: string; style?: CSSProperties } & Record<string, unknown>
118
118
  export declare function JumpTabBarItem(props: JumpTabBarItemProps): ReactNode
119
- export type JumpAppShellTopBar = Omit<JumpTopBarProps, 'onMenu' | 'menuOpen'> & { desktop?: boolean }
120
- export declare function JumpAppShell(props: HTMLAttributes<HTMLDivElement> & { brand?: ReactNode; navigation?: ReactNode; sidebarFooter?: ReactNode; topBar?: JumpAppShellTopBar | null; tabBar?: ReactNode; hideTabBar?: boolean; breakpoint?: number; sidebarWidth?: number | string; navigationOpen?: boolean; defaultNavigationOpen?: boolean; onNavigationOpenChange?: (open: boolean) => void; ariaLabels?: { navigation?: string; openNavigation?: string; closeNavigation?: string; back?: string }; children?: ReactNode }): ReactNode
119
+ export type JumpAppShellAction = { id?: string; label: string; icon: ReactNode; onClick?: (event: import('react').MouseEvent<HTMLElement>) => void; href?: string; variant?: 'primary' | 'secondary' | 'tertiary' | 'borderless'; disabled?: boolean }
120
+ export type JumpAppShellTopBar = Omit<JumpTopBarProps, 'onMenu' | 'menuOpen'> & { desktop?: boolean; largeTitle?: boolean; actions?: JumpAppShellAction[] }
121
+ export declare function JumpAppShell(props: HTMLAttributes<HTMLDivElement> & { brand?: ReactNode; navigation?: ReactNode; sidebarFooter?: ReactNode; topBar?: JumpAppShellTopBar | null; tabBar?: ReactNode; hideTabBar?: boolean; breakpoint?: number; sidebarWidth?: number | string; navigationOpen?: boolean; defaultNavigationOpen?: boolean; onNavigationOpenChange?: (open: boolean) => void; ariaLabels?: { navigation?: string; openNavigation?: string; closeNavigation?: string; back?: string; moreActions?: string }; children?: ReactNode }): ReactNode
121
122
  export declare function JumpSectionHeader(props: HTMLAttributes<HTMLElement> & { as?: ElementTag; variant?: 'regular' | 'prominent'; description?: ReactNode; children: ReactNode }): ReactNode
122
123
  export declare function JumpSectionFooter(props: HTMLAttributes<HTMLElement> & { as?: ElementTag; children: ReactNode }): ReactNode
123
124
  export declare function JumpTag(props: HTMLAttributes<HTMLSpanElement> & { size?: 'small' | 'medium'; status?: 'regular' | 'positive' | 'warning' | 'alarm'; disabled?: boolean; children: ReactNode }): ReactNode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mario_hahn/jumpy-ui",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {