@geomak/ui 7.4.0 → 7.4.1

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.d.cts CHANGED
@@ -2062,43 +2062,54 @@ interface ScalableContainerProps {
2062
2062
  */
2063
2063
  togglePosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
2064
2064
  /**
2065
- * Bounding element the EXPANDED state should overlay. When provided, the
2066
- * expanded content renders into a body portal positioned over this
2067
- * element's rect instead of growing in place letting the container break
2068
- * out of a size-constrained wrapper (e.g. a flex item) whose normal-state
2069
- * sizing it should otherwise respect. Collapsing returns it to normal
2070
- * flow. Omit for the classic expand-in-place behaviour.
2065
+ * Bounding element the expansion is allowed to grow within. When the
2066
+ * container sits in size-constrained flex-item wrappers (where width /
2067
+ * height 100% makes expand-in-place a no-op), providing this ref switches
2068
+ * to PUSH expansion: every flex item between the container and this
2069
+ * element gets its `flex-grow` raised (animated), so this container takes
2070
+ * most of the space while its siblings shrink — but stay visible.
2071
+ * Collapsing restores the wrappers' original sizing. Omit for the classic
2072
+ * expand-in-place behaviour.
2071
2073
  */
2072
2074
  expandContainerRef?: react__default.RefObject<HTMLElement | null>;
2075
+ /**
2076
+ * How dominant the pushed expansion is, as a flex-grow multiplier applied
2077
+ * to the container's wrappers (push mode only). Default `5` — i.e. the
2078
+ * expanded container takes roughly 5 parts for every 1 part a sibling
2079
+ * keeps. Raise for a more fullscreen feel, lower for a gentler split.
2080
+ */
2081
+ expandRatio?: number;
2073
2082
  /** Extra classes merged onto the container root. */
2074
2083
  className?: string;
2075
2084
  }
2076
2085
  /**
2077
- * Container that smoothly expands to fill its parent on click and
2078
- * collapses back to its resting size. Reads like a macOS / Windows
2079
- * window resizing — subtle elevation shift, smooth scale, no flash
2080
- * of colour or harsh background change.
2081
- *
2082
- * **What's different from the previous version**
2083
- * - Animates BOTH width and height (was width-only).
2084
- * - No baked-in background — the container is transparent by default,
2085
- * so it overlays whatever surface the consumer puts behind it.
2086
- * - Shadow lifts on expand (`shadow-md` `shadow-2xl`) like a window
2087
- * being raised. No colour change.
2088
- * - The toggle button is a plain rounded chip with the chevron icon,
2089
- * not the old `IconButton` with the heavy background. Floats over
2090
- * the content via absolute positioning so it doesn't push layout.
2091
- * - Configurable toggle position (default top-right, matching OS
2092
- * close-button convention).
2086
+ * Container that smoothly expands on click and collapses back to its
2087
+ * resting size. Reads like a macOS / Windows window resizing — subtle
2088
+ * elevation shift, smooth scale, no flash of colour or harsh background
2089
+ * change.
2090
+ *
2091
+ * Two expansion modes:
2092
+ * - **In place** (default): animates the container's own width/height to
2093
+ * `expandedWidth`/`expandedHeight`.
2094
+ * - **Push** (`expandContainerRef` set): for containers whose resting size is
2095
+ * owned by flex-item wrappers. Expanding raises `flex-grow` on every flex
2096
+ * item between the container and the bounding element, so the container
2097
+ * grows to dominate the section while sibling containers shrink but remain
2098
+ * visible. Collapsing restores the original layout.
2093
2099
  *
2094
2100
  * @example
2095
2101
  * ```tsx
2096
2102
  * <ScalableContainer width={480} height={300}>
2097
2103
  * <Chart data={metrics} />
2098
2104
  * </ScalableContainer>
2105
+ *
2106
+ * // Push mode inside a flex grid:
2107
+ * const sectionRef = useRef<HTMLDivElement>(null)
2108
+ * <div ref={sectionRef} className="flex flex-col flex-1 min-h-0 gap-2">…
2109
+ * <ScalableContainer width="100%" height="100%" expandContainerRef={sectionRef}>
2099
2110
  * ```
2100
2111
  */
2101
- declare function ScalableContainer({ width, height, expandedWidth, expandedHeight, expanded, onExpandedChange, children, assignClassOnClick, expandIcon, collapseIcon, togglePosition, expandContainerRef, className, }: ScalableContainerProps): react_jsx_runtime.JSX.Element;
2112
+ declare function ScalableContainer({ width, height, expandedWidth, expandedHeight, expanded, onExpandedChange, children, assignClassOnClick, expandIcon, collapseIcon, togglePosition, expandContainerRef, expandRatio, className, }: ScalableContainerProps): react_jsx_runtime.JSX.Element;
2102
2113
 
2103
2114
  interface GridCardItem {
2104
2115
  key: string | number;
package/dist/index.d.ts CHANGED
@@ -2062,43 +2062,54 @@ interface ScalableContainerProps {
2062
2062
  */
2063
2063
  togglePosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
2064
2064
  /**
2065
- * Bounding element the EXPANDED state should overlay. When provided, the
2066
- * expanded content renders into a body portal positioned over this
2067
- * element's rect instead of growing in place letting the container break
2068
- * out of a size-constrained wrapper (e.g. a flex item) whose normal-state
2069
- * sizing it should otherwise respect. Collapsing returns it to normal
2070
- * flow. Omit for the classic expand-in-place behaviour.
2065
+ * Bounding element the expansion is allowed to grow within. When the
2066
+ * container sits in size-constrained flex-item wrappers (where width /
2067
+ * height 100% makes expand-in-place a no-op), providing this ref switches
2068
+ * to PUSH expansion: every flex item between the container and this
2069
+ * element gets its `flex-grow` raised (animated), so this container takes
2070
+ * most of the space while its siblings shrink — but stay visible.
2071
+ * Collapsing restores the wrappers' original sizing. Omit for the classic
2072
+ * expand-in-place behaviour.
2071
2073
  */
2072
2074
  expandContainerRef?: react__default.RefObject<HTMLElement | null>;
2075
+ /**
2076
+ * How dominant the pushed expansion is, as a flex-grow multiplier applied
2077
+ * to the container's wrappers (push mode only). Default `5` — i.e. the
2078
+ * expanded container takes roughly 5 parts for every 1 part a sibling
2079
+ * keeps. Raise for a more fullscreen feel, lower for a gentler split.
2080
+ */
2081
+ expandRatio?: number;
2073
2082
  /** Extra classes merged onto the container root. */
2074
2083
  className?: string;
2075
2084
  }
2076
2085
  /**
2077
- * Container that smoothly expands to fill its parent on click and
2078
- * collapses back to its resting size. Reads like a macOS / Windows
2079
- * window resizing — subtle elevation shift, smooth scale, no flash
2080
- * of colour or harsh background change.
2081
- *
2082
- * **What's different from the previous version**
2083
- * - Animates BOTH width and height (was width-only).
2084
- * - No baked-in background — the container is transparent by default,
2085
- * so it overlays whatever surface the consumer puts behind it.
2086
- * - Shadow lifts on expand (`shadow-md` `shadow-2xl`) like a window
2087
- * being raised. No colour change.
2088
- * - The toggle button is a plain rounded chip with the chevron icon,
2089
- * not the old `IconButton` with the heavy background. Floats over
2090
- * the content via absolute positioning so it doesn't push layout.
2091
- * - Configurable toggle position (default top-right, matching OS
2092
- * close-button convention).
2086
+ * Container that smoothly expands on click and collapses back to its
2087
+ * resting size. Reads like a macOS / Windows window resizing — subtle
2088
+ * elevation shift, smooth scale, no flash of colour or harsh background
2089
+ * change.
2090
+ *
2091
+ * Two expansion modes:
2092
+ * - **In place** (default): animates the container's own width/height to
2093
+ * `expandedWidth`/`expandedHeight`.
2094
+ * - **Push** (`expandContainerRef` set): for containers whose resting size is
2095
+ * owned by flex-item wrappers. Expanding raises `flex-grow` on every flex
2096
+ * item between the container and the bounding element, so the container
2097
+ * grows to dominate the section while sibling containers shrink but remain
2098
+ * visible. Collapsing restores the original layout.
2093
2099
  *
2094
2100
  * @example
2095
2101
  * ```tsx
2096
2102
  * <ScalableContainer width={480} height={300}>
2097
2103
  * <Chart data={metrics} />
2098
2104
  * </ScalableContainer>
2105
+ *
2106
+ * // Push mode inside a flex grid:
2107
+ * const sectionRef = useRef<HTMLDivElement>(null)
2108
+ * <div ref={sectionRef} className="flex flex-col flex-1 min-h-0 gap-2">…
2109
+ * <ScalableContainer width="100%" height="100%" expandContainerRef={sectionRef}>
2099
2110
  * ```
2100
2111
  */
2101
- declare function ScalableContainer({ width, height, expandedWidth, expandedHeight, expanded, onExpandedChange, children, assignClassOnClick, expandIcon, collapseIcon, togglePosition, expandContainerRef, className, }: ScalableContainerProps): react_jsx_runtime.JSX.Element;
2112
+ declare function ScalableContainer({ width, height, expandedWidth, expandedHeight, expanded, onExpandedChange, children, assignClassOnClick, expandIcon, collapseIcon, togglePosition, expandContainerRef, expandRatio, className, }: ScalableContainerProps): react_jsx_runtime.JSX.Element;
2102
2113
 
2103
2114
  interface GridCardItem {
2104
2115
  key: string | number;
package/dist/index.js CHANGED
@@ -4540,10 +4540,6 @@ function List2({
4540
4540
  );
4541
4541
  }) });
4542
4542
  }
4543
- var rectOf = (el) => {
4544
- const r = el.getBoundingClientRect();
4545
- return { left: r.left, top: r.top, width: r.width, height: r.height };
4546
- };
4547
4543
  var TOGGLE_POSITION_CLASS = {
4548
4544
  "top-left": "top-2 left-2",
4549
4545
  "top-right": "top-2 right-2",
@@ -4563,55 +4559,69 @@ function ScalableContainer({
4563
4559
  collapseIcon,
4564
4560
  togglePosition = "top-right",
4565
4561
  expandContainerRef,
4562
+ expandRatio = 5,
4566
4563
  className = ""
4567
4564
  }) {
4568
4565
  const containerRef = useRef(null);
4569
4566
  const [internalScaled, setInternalScaled] = useState(false);
4570
4567
  const isScaled = expanded ?? internalScaled;
4571
4568
  const reduced = useReducedMotion();
4572
- const usePortal = expandContainerRef != null;
4573
- const [overlay, setOverlay] = useState("closed");
4574
- const [fromRect, setFromRect] = useState(null);
4575
- const [targetRect, setTargetRect] = useState(null);
4569
+ const usePush = expandContainerRef != null;
4570
+ const grownRef = useRef([]);
4576
4571
  const prevScaled = useRef(isScaled);
4577
- useEffect(() => {
4578
- if (!usePortal || isScaled === prevScaled.current) return;
4579
- prevScaled.current = isScaled;
4580
- if (isScaled) {
4581
- const src = containerRef.current ? rectOf(containerRef.current) : null;
4582
- const tgt = expandContainerRef.current ? rectOf(expandContainerRef.current) : null;
4583
- if (src && tgt) {
4584
- setFromRect(src);
4585
- setTargetRect(tgt);
4586
- setOverlay("open");
4572
+ const growAncestors = () => {
4573
+ const bound = expandContainerRef?.current;
4574
+ if (!bound || !containerRef.current) return;
4575
+ const grown = [];
4576
+ let el = containerRef.current.parentElement;
4577
+ while (el && el !== bound && bound.contains(el)) {
4578
+ const parent = el.parentElement;
4579
+ if (parent && getComputedStyle(parent).display.includes("flex")) {
4580
+ grown.push({
4581
+ el,
4582
+ prev: {
4583
+ flexGrow: el.style.flexGrow,
4584
+ flexBasis: el.style.flexBasis,
4585
+ transition: el.style.transition
4586
+ }
4587
+ });
4588
+ const grow = `flex-grow ${reduced ? 0 : 0.32}s cubic-bezier(0.16, 1, 0.3, 1), flex-basis ${reduced ? 0 : 0.32}s cubic-bezier(0.16, 1, 0.3, 1)`;
4589
+ el.style.transition = el.style.transition ? `${el.style.transition}, ${grow}` : grow;
4590
+ el.style.flexBasis = "0%";
4591
+ el.style.flexGrow = String(expandRatio);
4587
4592
  }
4588
- } else if (containerRef.current) {
4589
- setTargetRect(rectOf(containerRef.current));
4590
- setOverlay("closing");
4593
+ el = parent;
4591
4594
  }
4592
- }, [isScaled, usePortal, expandContainerRef]);
4593
- useEffect(() => {
4594
- if (overlay !== "closing") return;
4595
- const t = window.setTimeout(() => setOverlay("closed"), reduced ? 0 : 360);
4596
- return () => window.clearTimeout(t);
4597
- }, [overlay, reduced]);
4595
+ grownRef.current = grown;
4596
+ };
4597
+ const restoreAncestors = () => {
4598
+ for (const { el, prev } of grownRef.current) {
4599
+ el.style.flexGrow = prev.flexGrow;
4600
+ el.style.flexBasis = prev.flexBasis;
4601
+ window.setTimeout(() => {
4602
+ el.style.transition = prev.transition;
4603
+ }, reduced ? 0 : 360);
4604
+ }
4605
+ grownRef.current = [];
4606
+ };
4598
4607
  useEffect(() => {
4599
- if (overlay !== "open" || !expandContainerRef?.current) return;
4600
- const update = () => {
4601
- if (expandContainerRef.current) setTargetRect(rectOf(expandContainerRef.current));
4602
- };
4603
- window.addEventListener("resize", update);
4604
- window.addEventListener("scroll", update, true);
4605
- return () => {
4606
- window.removeEventListener("resize", update);
4607
- window.removeEventListener("scroll", update, true);
4608
- };
4609
- }, [overlay, expandContainerRef]);
4608
+ if (!usePush || isScaled === prevScaled.current) return;
4609
+ prevScaled.current = isScaled;
4610
+ if (isScaled) growAncestors();
4611
+ else restoreAncestors();
4612
+ }, [isScaled, usePush]);
4613
+ useEffect(() => () => {
4614
+ for (const { el, prev } of grownRef.current) {
4615
+ el.style.flexGrow = prev.flexGrow;
4616
+ el.style.flexBasis = prev.flexBasis;
4617
+ el.style.transition = prev.transition;
4618
+ }
4619
+ }, []);
4610
4620
  const onToggle = () => {
4611
4621
  const next = !isScaled;
4612
4622
  if (expanded === void 0) setInternalScaled(next);
4613
4623
  onExpandedChange?.(next);
4614
- if (next && !usePortal) {
4624
+ if (next && !usePush) {
4615
4625
  window.setTimeout(
4616
4626
  () => containerRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" }),
4617
4627
  reduced ? 0 : 340
@@ -4619,79 +4629,52 @@ function ScalableContainer({
4619
4629
  }
4620
4630
  };
4621
4631
  const wrapperClass = isScaled ? assignClassOnClick : void 0;
4622
- const overlayActive = usePortal && overlay !== "closed";
4623
- const toggleButton = (scaled) => /* @__PURE__ */ jsx(Tooltip, { placement: "bottom", title: scaled ? "Collapse" : "Expand", children: /* @__PURE__ */ jsx(
4624
- "button",
4632
+ return /* @__PURE__ */ jsxs(
4633
+ motion.div,
4625
4634
  {
4626
- type: "button",
4627
- onClick: onToggle,
4628
- "aria-label": scaled ? "Collapse container" : "Expand container",
4629
- "aria-expanded": scaled,
4630
- className: [
4631
- "absolute z-10",
4632
- TOGGLE_POSITION_CLASS[togglePosition],
4633
- "w-7 h-7 inline-flex items-center justify-center",
4634
- "rounded-md bg-surface/80 backdrop-blur-sm border border-border",
4635
- "text-foreground-secondary hover:text-foreground hover:bg-surface",
4636
- "shadow-sm transition-colors duration-150",
4637
- "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
4638
- ].join(" "),
4639
- children: scaled ? collapseIcon ?? /* @__PURE__ */ jsx(CollapseIcon, {}) : expandIcon ?? /* @__PURE__ */ jsx(ExpandIcon, {})
4640
- }
4641
- ) });
4642
- return /* @__PURE__ */ jsxs(Fragment, { children: [
4643
- /* @__PURE__ */ jsxs(
4644
- motion.div,
4645
- {
4646
- ref: containerRef,
4647
- animate: {
4648
- // Breakout mode never grows in place — the in-flow box stays
4649
- // at its resting size and acts as the collapse target.
4650
- width: isScaled && !usePortal ? expandedWidth : width,
4651
- height: isScaled && !usePortal ? expandedHeight : height
4652
- },
4653
- transition: reduced ? { duration: 0 } : {
4654
- width: { type: "tween", duration: 0.32, ease: [0.16, 1, 0.3, 1] },
4655
- height: { type: "tween", duration: 0.32, ease: [0.16, 1, 0.3, 1] }
4656
- },
4657
- className: cx(
4658
- "relative rounded-lg overflow-hidden",
4659
- // OS-window aesthetic: subtle elevation at rest, lifted shadow
4660
- // when expanded. No background colour change.
4661
- isScaled && !usePortal ? "shadow-2xl" : "shadow-md",
4662
- "transition-shadow duration-300",
4663
- className
4664
- ),
4665
- children: [
4666
- !overlayActive && toggleButton(isScaled),
4667
- !overlayActive && /* @__PURE__ */ jsx("div", { className: wrapperClass, children })
4668
- ]
4669
- }
4670
- ),
4671
- overlayActive && fromRect && targetRect && createPortal(
4672
- /* @__PURE__ */ jsxs(
4673
- motion.div,
4674
- {
4675
- initial: { ...fromRect },
4676
- animate: { ...targetRect },
4677
- transition: reduced ? { duration: 0 } : { type: "tween", duration: 0.32, ease: [0.16, 1, 0.3, 1] },
4678
- onAnimationComplete: () => {
4679
- if (overlay === "closing") setOverlay("closed");
4680
- },
4681
- style: { position: "fixed" },
4682
- className: cx(
4683
- "z-dropdown rounded-lg overflow-hidden bg-surface shadow-2xl",
4684
- className
4685
- ),
4686
- children: [
4687
- toggleButton(isScaled),
4688
- /* @__PURE__ */ jsx("div", { className: cx("h-full w-full", wrapperClass), children })
4689
- ]
4690
- }
4635
+ ref: containerRef,
4636
+ animate: {
4637
+ // Push mode keeps the container filling its (now growing)
4638
+ // wrapper — the wrapper's flex-grow does the work.
4639
+ width: isScaled && !usePush ? expandedWidth : width,
4640
+ height: isScaled && !usePush ? expandedHeight : height
4641
+ },
4642
+ transition: reduced ? { duration: 0 } : {
4643
+ width: { type: "tween", duration: 0.32, ease: [0.16, 1, 0.3, 1] },
4644
+ height: { type: "tween", duration: 0.32, ease: [0.16, 1, 0.3, 1] }
4645
+ },
4646
+ className: cx(
4647
+ "relative rounded-lg overflow-hidden",
4648
+ // OS-window aesthetic: subtle elevation at rest, lifted shadow
4649
+ // when expanded. No background colour change.
4650
+ isScaled ? "shadow-2xl" : "shadow-md",
4651
+ "transition-shadow duration-300",
4652
+ className
4691
4653
  ),
4692
- document.body
4693
- )
4694
- ] });
4654
+ children: [
4655
+ /* @__PURE__ */ jsx(Tooltip, { placement: "bottom", title: isScaled ? "Collapse" : "Expand", children: /* @__PURE__ */ jsx(
4656
+ "button",
4657
+ {
4658
+ type: "button",
4659
+ onClick: onToggle,
4660
+ "aria-label": isScaled ? "Collapse container" : "Expand container",
4661
+ "aria-expanded": isScaled,
4662
+ className: [
4663
+ "absolute z-10",
4664
+ TOGGLE_POSITION_CLASS[togglePosition],
4665
+ "w-7 h-7 inline-flex items-center justify-center",
4666
+ "rounded-md bg-surface/80 backdrop-blur-sm border border-border",
4667
+ "text-foreground-secondary hover:text-foreground hover:bg-surface",
4668
+ "shadow-sm transition-colors duration-150",
4669
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
4670
+ ].join(" "),
4671
+ children: isScaled ? collapseIcon ?? /* @__PURE__ */ jsx(CollapseIcon, {}) : expandIcon ?? /* @__PURE__ */ jsx(ExpandIcon, {})
4672
+ }
4673
+ ) }),
4674
+ /* @__PURE__ */ jsx("div", { className: cx("h-full w-full", wrapperClass), children })
4675
+ ]
4676
+ }
4677
+ );
4695
4678
  }
4696
4679
  function CollapseIcon() {
4697
4680
  return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "w-4 h-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 9L4 4M9 9V4M9 9H4M15 9L20 4M15 9V4M15 9H20M9 15L4 20M9 15V20M9 15H4M15 15L20 20M15 15V20M15 15H20" }) });