@mario_hahn/jumpy-ui 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.
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.12.0
214
+ npm install @mario_hahn/jumpy-ui@^0.13.0
215
215
  ```
216
216
 
217
217
  The public npm package uses the `@mario_hahn` scope. Older GitHub Packages releases
@@ -333,6 +333,11 @@ import { JumpAppShell, JumpSideMenu, JumpSideMenuItem, JumpTabBar, JumpTabBarIte
333
333
  stays visible and the rest go into a "More actions" menu. Desktop shows labelled
334
334
  buttons beside the large title, otherwise in the top bar. Prefer it over `trailing`
335
335
  for ordinary actions; keep `trailing` for custom content such as an avatar.
336
+ - `topBar.back` (0.13.0+): `{ label, href, as: Link }` for detail pages ("up" to the
337
+ parent). Mobile: "‹ label" replaces the menu button; desktop: link above the large
338
+ title or in the top bar. Returns through history when the previous entry is that
339
+ parent, otherwise follows `href` (deep links stay in the app). Keep the browser's own
340
+ back (button, Safari edge swipe) working: every page change needs a history entry.
336
341
  - `JumpTopBar` and `JumpTabBar` also work standalone. Both are sticky and include
337
342
  safe-area insets. `JumpTabBarItem` accepts `as` (e.g. Next `Link`), `href`, `icon`,
338
343
  numeric or custom `badge` and `selected` (`aria-current="page"`).
@@ -11904,6 +11904,25 @@ var providerStyles = `
11904
11904
  backdrop-filter: none;
11905
11905
  }
11906
11906
 
11907
+ [data-jumpy-theme='true'] [data-jumpy-slot='app-shell-back'] {
11908
+ display: inline-flex;
11909
+ -webkit-tap-highlight-color: transparent;
11910
+ }
11911
+
11912
+ /* Only the label truncates; the chevron keeps its size. */
11913
+ [data-jumpy-theme='true'] [data-jumpy-slot='app-shell-back'] > svg {
11914
+ flex: none;
11915
+ }
11916
+
11917
+ [data-jumpy-theme='true'] [data-jumpy-slot='app-shell-back']:hover {
11918
+ color: var(--jumpy-interactive-active-regular-hover-text);
11919
+ }
11920
+
11921
+ [data-jumpy-theme='true'] [data-jumpy-slot='app-shell-back']:focus-visible {
11922
+ outline: var(--jumpy-spacing-outline-medium) solid var(--jumpy-interactive-inactive-regular-focus-focus-ring);
11923
+ outline-offset: var(--jumpy-spacing-outline-small);
11924
+ }
11925
+
11907
11926
  [data-jumpy-theme='true'] [data-jumpy-component='tab-bar-item'] {
11908
11927
  transition: background-color var(--jumpy-animation-duration-xsmall) var(--jumpy-animation-easing-drift), color var(--jumpy-animation-duration-xsmall) var(--jumpy-animation-easing-drift);
11909
11928
  -webkit-tap-highlight-color: transparent;
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  textStyleValue,
29
29
  themeToCSSVariables,
30
30
  useJumpyTheme
31
- } from "./chunk-3PHOJLCH.js";
31
+ } from "./chunk-2I6OOQNV.js";
32
32
  import {
33
33
  cn
34
34
  } from "./chunk-3DBKYKP2.js";
@@ -2408,14 +2408,14 @@ function JumpSheet({ visible = false, onVisibleChange, position = "right", title
2408
2408
  footer ? /* @__PURE__ */ jsx2("div", { style: { padding: spacingValue(theme, JumpySpacing.large), borderTop: `${borderWidthValue(theme, JumpyBorderWidth.small)}px solid ${theme.color.borderSubtle}` }, children: footer }) : null
2409
2409
  ] }) });
2410
2410
  }
2411
- function JumpyNavigationIcon({ type }) {
2411
+ function JumpyNavigationIcon({ type, size: size2 = 24 }) {
2412
2412
  const paths = {
2413
2413
  menu: "M4 6h16M4 12h16M4 18h16",
2414
2414
  close: "M6 6l12 12M18 6 6 18",
2415
2415
  back: "M15 18l-6-6 6-6",
2416
2416
  more: "M5 12h.01M12 12h.01M19 12h.01"
2417
2417
  };
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
+ return /* @__PURE__ */ jsx2("svg", { width: size2, height: size2, 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] }) });
2419
2419
  }
2420
2420
  function JumpTopBar({ leading, onMenu, onBack, menuOpen = false, title, subtitle, trailing, sticky = true, titleAlign = "start", titleHidden = false, ariaLabels = {}, className = "", ...props }) {
2421
2421
  const theme = useJumpyTheme();
@@ -2455,7 +2455,7 @@ function JumpTopBar({ leading, onMenu, onBack, menuOpen = false, title, subtitle
2455
2455
  ...props.style || {}
2456
2456
  },
2457
2457
  children: [
2458
- leadingNode ? /* @__PURE__ */ jsx2("span", { "data-jumpy-slot": "top-bar-leading", "data-leading": leadingType, style: { marginLeft: -spacingValue(theme, JumpySpacing.smallXS) }, children: leadingNode }) : null,
2458
+ leadingNode ? /* @__PURE__ */ jsx2("span", { "data-jumpy-slot": "top-bar-leading", "data-leading": leadingType, style: { minWidth: 0, maxWidth: "100%", marginLeft: -spacingValue(theme, JumpySpacing.smallXS) }, children: leadingNode }) : null,
2459
2459
  /* @__PURE__ */ jsxs2("span", { "data-jumpy-slot": "top-bar-title", "aria-hidden": titleHidden || void 0, style: titleAlign === "center" ? {
2460
2460
  display: "flex",
2461
2461
  flexDirection: "column",
@@ -2558,6 +2558,71 @@ function JumpTabBarItem({ as, icon, badge, selected = false, children, className
2558
2558
  }
2559
2559
  );
2560
2560
  }
2561
+ var jumpyLocations = { current: null, previous: null };
2562
+ function jumpyNormalizeUrl(url) {
2563
+ const resolved = new URL(url, window.location.href);
2564
+ const path = resolved.pathname.length > 1 ? resolved.pathname.replace(/\/+$/, "") : resolved.pathname;
2565
+ return `${resolved.origin}${path}${resolved.search === "?" ? "" : resolved.search}`;
2566
+ }
2567
+ function jumpyPreviousEntryIs(href) {
2568
+ const target = jumpyNormalizeUrl(href);
2569
+ const navigation = window.navigation;
2570
+ if (navigation && typeof navigation.entries === "function" && navigation.currentEntry) {
2571
+ if (!navigation.canGoBack) return false;
2572
+ const previous = navigation.entries()[navigation.currentEntry.index - 1];
2573
+ return Boolean(previous?.url) && jumpyNormalizeUrl(previous.url) === target;
2574
+ }
2575
+ return Boolean(jumpyLocations.previous) && jumpyNormalizeUrl(jumpyLocations.previous) === target && window.history.length > 1;
2576
+ }
2577
+ function JumpyAppShellBack({ back, variant, ariaLabels }) {
2578
+ const theme = useJumpyTheme();
2579
+ const Component = back.as || (back.href ? "a" : "button");
2580
+ const textStyle = textStyleValue(theme, variant === "inline" ? JumpyTextStyle.bodySmallEmphasized : JumpyTextStyle.bodyMedium);
2581
+ const accessibleName = ariaLabels.backTo ? ariaLabels.backTo(back.label) : `Back to ${back.label}`;
2582
+ function handleClick(event) {
2583
+ back.onClick?.(event);
2584
+ if (event.defaultPrevented || !back.href || back.history === false) return;
2585
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
2586
+ if (!jumpyPreviousEntryIs(back.href)) return;
2587
+ event.preventDefault();
2588
+ window.history.back();
2589
+ }
2590
+ return /* @__PURE__ */ jsxs2(
2591
+ Component,
2592
+ {
2593
+ href: back.href,
2594
+ type: Component === "button" ? "button" : void 0,
2595
+ "aria-label": accessibleName,
2596
+ "data-jumpy-slot": "app-shell-back",
2597
+ "data-variant": variant,
2598
+ onClick: handleClick,
2599
+ style: {
2600
+ boxSizing: "border-box",
2601
+ alignItems: "center",
2602
+ gap: spacingValue(theme, JumpySpacing.small4XS),
2603
+ maxWidth: "100%",
2604
+ minWidth: 0,
2605
+ minHeight: variant === "inline" ? void 0 : sizeValue(theme, JumpySize.largeXL),
2606
+ margin: variant === "inline" ? `0 0 ${spacingValue(theme, JumpySpacing.small3XS)}px ${-spacingValue(theme, JumpySpacing.small3XS)}px` : 0,
2607
+ padding: variant === "inline" ? 0 : `0 ${spacingValue(theme, JumpySpacing.smallXS)}px 0 0`,
2608
+ border: 0,
2609
+ borderRadius: radiusValue(theme, JumpyRadius.small),
2610
+ background: "transparent",
2611
+ color: "var(--jumpy-interactive-active-regular-default-text)",
2612
+ textDecoration: "none",
2613
+ cursor: "pointer",
2614
+ fontFamily: theme.fontFamily.familyBase,
2615
+ fontSize: textStyle.fontSize,
2616
+ lineHeight: textStyle.lineHeight,
2617
+ fontWeight: textStyle.fontWeight
2618
+ },
2619
+ children: [
2620
+ /* @__PURE__ */ jsx2(JumpyNavigationIcon, { type: "back", size: variant === "inline" ? 18 : 24 }),
2621
+ /* @__PURE__ */ jsx2("span", { style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: back.label })
2622
+ ]
2623
+ }
2624
+ );
2625
+ }
2561
2626
  function JumpyAppShellActions({ actions, variant, ariaLabels }) {
2562
2627
  const theme = useJumpyTheme();
2563
2628
  if (!actions.length) return null;
@@ -2591,12 +2656,19 @@ function JumpAppShell({ brand, navigation, sidebarFooter, topBar, tabBar, hideTa
2591
2656
  const showDrawer = isOpen || drawerMounted;
2592
2657
  const closing = drawerMounted && !isOpen;
2593
2658
  const wasShown = useRef2(showDrawer);
2594
- const { desktop: topBarDesktop = false, leading, largeTitle = false, actions = [], ...topBarProps } = topBar || {};
2659
+ const { desktop: topBarDesktop = false, leading, largeTitle = false, actions = [], back, ...topBarProps } = topBar || {};
2595
2660
  const showLargeTitle = largeTitle && Boolean(topBarProps.title);
2596
- const topBarOnDesktop = topBarDesktop || actions.length > 0 && !showLargeTitle;
2661
+ const hasBack = Boolean(back?.label);
2662
+ const topBarOnDesktop = topBarDesktop || !showLargeTitle && (actions.length > 0 || hasBack);
2597
2663
  const largeTitleRef = useRef2(null);
2598
2664
  const [largeTitleCovered, setLargeTitleCovered] = useState2(false);
2599
- const resolvedLeading = leading ?? (navigation ? "menu" : void 0);
2665
+ const resolvedLeading = hasBack ? /* @__PURE__ */ jsx2(JumpyAppShellBack, { back, variant: "bar", ariaLabels }) : leading ?? (navigation ? "menu" : void 0);
2666
+ useEffect2(() => {
2667
+ const href = window.location.href;
2668
+ if (jumpyLocations.current === href) return;
2669
+ jumpyLocations.previous = jumpyLocations.current;
2670
+ jumpyLocations.current = href;
2671
+ });
2600
2672
  const shell = `[data-jumpy-app-shell="${shellId}"]`;
2601
2673
  const width = typeof sidebarWidth === "number" ? `${sidebarWidth}px` : sidebarWidth;
2602
2674
  function setOpen(next) {
@@ -2655,11 +2727,13 @@ ${shell} { grid-template-columns: minmax(0, 1fr); }
2655
2727
  ${shell} > [data-jumpy-slot='app-shell-sidebar'] { display: none; }
2656
2728
  ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='compact'] { display: inline-flex; }
2657
2729
  ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='full'] { display: none; }
2730
+ ${shell} [data-jumpy-slot='app-shell-back'][data-variant='inline'] { display: none; }
2658
2731
  @media (min-width: ${breakpoint}px) {
2659
2732
  ${shell} { grid-template-columns: ${width} minmax(0, 1fr); }
2660
2733
  ${shell} > [data-jumpy-slot='app-shell-sidebar'] { display: flex; }
2661
2734
  ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='compact'] { display: none; }
2662
2735
  ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='full'] { display: inline-flex; }
2736
+ ${shell} [data-jumpy-slot='app-shell-back'][data-variant='inline'] { display: flex; width: fit-content; }
2663
2737
  ${shell} > [data-jumpy-slot='app-shell-body'] > [data-jumpy-component='tab-bar'] { display: none; }
2664
2738
  ${shell} > [data-jumpy-slot='app-shell-body'] > [data-jumpy-component='top-bar'][data-mobile-only='true'] { display: none; }
2665
2739
  ${shell} > [data-jumpy-slot='app-shell-body'] > [data-jumpy-component='top-bar'] > [data-leading='menu'] { display: none; }
@@ -2674,7 +2748,7 @@ ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='full'] { display: n
2674
2748
  onMenu: () => setOpen(true),
2675
2749
  title: topBarProps.title ?? brand,
2676
2750
  subtitle: showLargeTitle ? void 0 : topBarProps.subtitle,
2677
- titleAlign: showLargeTitle ? "center" : topBarProps.titleAlign,
2751
+ titleAlign: showLargeTitle || hasBack ? "center" : topBarProps.titleAlign,
2678
2752
  titleHidden: showLargeTitle ? !largeTitleCovered : topBarProps.titleHidden,
2679
2753
  "data-large-title": showLargeTitle ? "true" : void 0,
2680
2754
  trailing: actions.length || topBarProps.trailing ? /* @__PURE__ */ jsxs2(Fragment, { children: [
@@ -2731,6 +2805,7 @@ ${shell} [data-jumpy-slot='app-shell-actions'][data-variant='full'] { display: n
2731
2805
  /* @__PURE__ */ jsxs2("main", { "data-jumpy-slot": "app-shell-content", style: { flex: 1, minWidth: 0 }, children: [
2732
2806
  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
2807
  /* @__PURE__ */ jsxs2("div", { style: { minWidth: 0 }, children: [
2808
+ hasBack ? /* @__PURE__ */ jsx2(JumpyAppShellBack, { back, variant: "inline", ariaLabels }) : null,
2734
2809
  /* @__PURE__ */ jsx2("h1", { ref: largeTitleRef, style: { margin: 0, ...textStyleValue(theme, JumpyTextStyle.displaySmallEmphasized), overflowWrap: "anywhere" }, children: topBarProps.title }),
2735
2810
  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
2811
  ] }),
package/dist/theme.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  textStyleValue,
29
29
  themeToCSSVariables,
30
30
  useJumpyTheme
31
- } from "./chunk-3PHOJLCH.js";
31
+ } from "./chunk-2I6OOQNV.js";
32
32
  export {
33
33
  JumpyAnimation,
34
34
  JumpyAnimationDelay,
package/index.d.ts CHANGED
@@ -117,8 +117,9 @@ export declare function JumpTabBar(props: HTMLAttributes<HTMLElement> & { childr
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
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
120
+ export type JumpAppShellBack = { label: string; href?: string; as?: ElementType; onClick?: (event: import('react').MouseEvent<HTMLElement>) => void; history?: boolean }
121
+ export type JumpAppShellTopBar = Omit<JumpTopBarProps, 'onMenu' | 'menuOpen'> & { desktop?: boolean; largeTitle?: boolean; actions?: JumpAppShellAction[]; back?: JumpAppShellBack }
122
+ 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; backTo?: (label: string) => string; moreActions?: string }; children?: ReactNode }): ReactNode
122
123
  export declare function JumpSectionHeader(props: HTMLAttributes<HTMLElement> & { as?: ElementTag; variant?: 'regular' | 'prominent'; description?: ReactNode; children: ReactNode }): ReactNode
123
124
  export declare function JumpSectionFooter(props: HTMLAttributes<HTMLElement> & { as?: ElementTag; children: ReactNode }): ReactNode
124
125
  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.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {