@geomak/ui 7.4.0 → 7.4.2

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,55 @@ 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 `3` — i.e. the
2078
+ * expanded container takes roughly 3 parts for every 1 part a sibling
2079
+ * keeps, leaving the siblings enough room to stay legible. Raise for a
2080
+ * more fullscreen feel, lower for a gentler split.
2081
+ */
2082
+ expandRatio?: number;
2073
2083
  /** Extra classes merged onto the container root. */
2074
2084
  className?: string;
2075
2085
  }
2076
2086
  /**
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).
2087
+ * Container that smoothly expands on click and collapses back to its
2088
+ * resting size. Reads like a macOS / Windows window resizing — subtle
2089
+ * elevation shift, smooth scale, no flash of colour or harsh background
2090
+ * change.
2091
+ *
2092
+ * Two expansion modes:
2093
+ * - **In place** (default): animates the container's own width/height to
2094
+ * `expandedWidth`/`expandedHeight`.
2095
+ * - **Push** (`expandContainerRef` set): for containers whose resting size is
2096
+ * owned by flex-item wrappers. Expanding raises `flex-grow` on every flex
2097
+ * item between the container and the bounding element, so the container
2098
+ * grows to dominate the section while sibling containers shrink but remain
2099
+ * visible. Collapsing restores the original layout.
2093
2100
  *
2094
2101
  * @example
2095
2102
  * ```tsx
2096
2103
  * <ScalableContainer width={480} height={300}>
2097
2104
  * <Chart data={metrics} />
2098
2105
  * </ScalableContainer>
2106
+ *
2107
+ * // Push mode inside a flex grid:
2108
+ * const sectionRef = useRef<HTMLDivElement>(null)
2109
+ * <div ref={sectionRef} className="flex flex-col flex-1 min-h-0 gap-2">…
2110
+ * <ScalableContainer width="100%" height="100%" expandContainerRef={sectionRef}>
2099
2111
  * ```
2100
2112
  */
2101
- declare function ScalableContainer({ width, height, expandedWidth, expandedHeight, expanded, onExpandedChange, children, assignClassOnClick, expandIcon, collapseIcon, togglePosition, expandContainerRef, className, }: ScalableContainerProps): react_jsx_runtime.JSX.Element;
2113
+ declare function ScalableContainer({ width, height, expandedWidth, expandedHeight, expanded, onExpandedChange, children, assignClassOnClick, expandIcon, collapseIcon, togglePosition, expandContainerRef, expandRatio, className, }: ScalableContainerProps): react_jsx_runtime.JSX.Element;
2102
2114
 
2103
2115
  interface GridCardItem {
2104
2116
  key: string | number;
package/dist/index.d.ts CHANGED
@@ -2062,43 +2062,55 @@ 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 `3` — i.e. the
2078
+ * expanded container takes roughly 3 parts for every 1 part a sibling
2079
+ * keeps, leaving the siblings enough room to stay legible. Raise for a
2080
+ * more fullscreen feel, lower for a gentler split.
2081
+ */
2082
+ expandRatio?: number;
2073
2083
  /** Extra classes merged onto the container root. */
2074
2084
  className?: string;
2075
2085
  }
2076
2086
  /**
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).
2087
+ * Container that smoothly expands on click and collapses back to its
2088
+ * resting size. Reads like a macOS / Windows window resizing — subtle
2089
+ * elevation shift, smooth scale, no flash of colour or harsh background
2090
+ * change.
2091
+ *
2092
+ * Two expansion modes:
2093
+ * - **In place** (default): animates the container's own width/height to
2094
+ * `expandedWidth`/`expandedHeight`.
2095
+ * - **Push** (`expandContainerRef` set): for containers whose resting size is
2096
+ * owned by flex-item wrappers. Expanding raises `flex-grow` on every flex
2097
+ * item between the container and the bounding element, so the container
2098
+ * grows to dominate the section while sibling containers shrink but remain
2099
+ * visible. Collapsing restores the original layout.
2093
2100
  *
2094
2101
  * @example
2095
2102
  * ```tsx
2096
2103
  * <ScalableContainer width={480} height={300}>
2097
2104
  * <Chart data={metrics} />
2098
2105
  * </ScalableContainer>
2106
+ *
2107
+ * // Push mode inside a flex grid:
2108
+ * const sectionRef = useRef<HTMLDivElement>(null)
2109
+ * <div ref={sectionRef} className="flex flex-col flex-1 min-h-0 gap-2">…
2110
+ * <ScalableContainer width="100%" height="100%" expandContainerRef={sectionRef}>
2099
2111
  * ```
2100
2112
  */
2101
- declare function ScalableContainer({ width, height, expandedWidth, expandedHeight, expanded, onExpandedChange, children, assignClassOnClick, expandIcon, collapseIcon, togglePosition, expandContainerRef, className, }: ScalableContainerProps): react_jsx_runtime.JSX.Element;
2113
+ declare function ScalableContainer({ width, height, expandedWidth, expandedHeight, expanded, onExpandedChange, children, assignClassOnClick, expandIcon, collapseIcon, togglePosition, expandContainerRef, expandRatio, className, }: ScalableContainerProps): react_jsx_runtime.JSX.Element;
2102
2114
 
2103
2115
  interface GridCardItem {
2104
2116
  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,79 @@ function ScalableContainer({
4563
4559
  collapseIcon,
4564
4560
  togglePosition = "top-right",
4565
4561
  expandContainerRef,
4562
+ expandRatio = 3,
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 kickResizeDuringTransition = () => {
4573
+ if (typeof window === "undefined") return;
4574
+ const kick = () => window.dispatchEvent(new Event("resize"));
4575
+ const interval = window.setInterval(kick, 80);
4576
+ window.setTimeout(() => {
4577
+ window.clearInterval(interval);
4578
+ kick();
4579
+ }, reduced ? 0 : 400);
4580
+ };
4581
+ const growAncestors = () => {
4582
+ const bound = expandContainerRef?.current;
4583
+ if (!bound || !containerRef.current) return;
4584
+ const grown = [];
4585
+ let el = containerRef.current.parentElement;
4586
+ while (el && el !== bound && bound.contains(el)) {
4587
+ const parent = el.parentElement;
4588
+ if (parent && getComputedStyle(parent).display.includes("flex")) {
4589
+ grown.push({
4590
+ el,
4591
+ prev: {
4592
+ flexGrow: el.style.flexGrow,
4593
+ flexBasis: el.style.flexBasis,
4594
+ transition: el.style.transition
4595
+ }
4596
+ });
4597
+ 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)`;
4598
+ el.style.transition = el.style.transition ? `${el.style.transition}, ${grow}` : grow;
4599
+ el.style.flexBasis = "0%";
4600
+ el.style.flexGrow = String(expandRatio);
4587
4601
  }
4588
- } else if (containerRef.current) {
4589
- setTargetRect(rectOf(containerRef.current));
4590
- setOverlay("closing");
4602
+ el = parent;
4591
4603
  }
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]);
4604
+ grownRef.current = grown;
4605
+ };
4606
+ const restoreAncestors = () => {
4607
+ for (const { el, prev } of grownRef.current) {
4608
+ el.style.flexGrow = prev.flexGrow;
4609
+ el.style.flexBasis = prev.flexBasis;
4610
+ window.setTimeout(() => {
4611
+ el.style.transition = prev.transition;
4612
+ }, reduced ? 0 : 360);
4613
+ }
4614
+ grownRef.current = [];
4615
+ };
4598
4616
  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]);
4617
+ if (!usePush || isScaled === prevScaled.current) return;
4618
+ prevScaled.current = isScaled;
4619
+ if (isScaled) growAncestors();
4620
+ else restoreAncestors();
4621
+ kickResizeDuringTransition();
4622
+ }, [isScaled, usePush]);
4623
+ useEffect(() => () => {
4624
+ for (const { el, prev } of grownRef.current) {
4625
+ el.style.flexGrow = prev.flexGrow;
4626
+ el.style.flexBasis = prev.flexBasis;
4627
+ el.style.transition = prev.transition;
4628
+ }
4629
+ }, []);
4610
4630
  const onToggle = () => {
4611
4631
  const next = !isScaled;
4612
4632
  if (expanded === void 0) setInternalScaled(next);
4613
4633
  onExpandedChange?.(next);
4614
- if (next && !usePortal) {
4634
+ if (next && !usePush) {
4615
4635
  window.setTimeout(
4616
4636
  () => containerRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" }),
4617
4637
  reduced ? 0 : 340
@@ -4619,79 +4639,56 @@ function ScalableContainer({
4619
4639
  }
4620
4640
  };
4621
4641
  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",
4642
+ return /* @__PURE__ */ jsxs(
4643
+ motion.div,
4625
4644
  {
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
- }
4645
+ ref: containerRef,
4646
+ style: {
4647
+ width: isScaled && !usePush ? expandedWidth : width,
4648
+ height: isScaled && !usePush ? expandedHeight : height
4649
+ },
4650
+ animate: {
4651
+ // Push mode keeps the container filling its (now growing)
4652
+ // wrapper — the wrapper's flex-grow does the work.
4653
+ width: isScaled && !usePush ? expandedWidth : width,
4654
+ height: isScaled && !usePush ? expandedHeight : height
4655
+ },
4656
+ transition: reduced ? { duration: 0 } : {
4657
+ width: { type: "tween", duration: 0.32, ease: [0.16, 1, 0.3, 1] },
4658
+ height: { type: "tween", duration: 0.32, ease: [0.16, 1, 0.3, 1] }
4659
+ },
4660
+ className: cx(
4661
+ "relative rounded-lg overflow-hidden",
4662
+ // OS-window aesthetic: subtle elevation at rest, lifted shadow
4663
+ // when expanded. No background colour change.
4664
+ isScaled ? "shadow-2xl" : "shadow-md",
4665
+ "transition-shadow duration-300",
4666
+ className
4691
4667
  ),
4692
- document.body
4693
- )
4694
- ] });
4668
+ children: [
4669
+ /* @__PURE__ */ jsx(Tooltip, { placement: "bottom", title: isScaled ? "Collapse" : "Expand", children: /* @__PURE__ */ jsx(
4670
+ "button",
4671
+ {
4672
+ type: "button",
4673
+ onClick: onToggle,
4674
+ "aria-label": isScaled ? "Collapse container" : "Expand container",
4675
+ "aria-expanded": isScaled,
4676
+ className: [
4677
+ "absolute z-10",
4678
+ TOGGLE_POSITION_CLASS[togglePosition],
4679
+ "w-7 h-7 inline-flex items-center justify-center",
4680
+ "rounded-md bg-surface/80 backdrop-blur-sm border border-border",
4681
+ "text-foreground-secondary hover:text-foreground hover:bg-surface",
4682
+ "shadow-sm transition-colors duration-150",
4683
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
4684
+ ].join(" "),
4685
+ children: isScaled ? collapseIcon ?? /* @__PURE__ */ jsx(CollapseIcon, {}) : expandIcon ?? /* @__PURE__ */ jsx(ExpandIcon, {})
4686
+ }
4687
+ ) }),
4688
+ /* @__PURE__ */ jsx("div", { className: cx("h-full w-full", wrapperClass), children })
4689
+ ]
4690
+ }
4691
+ );
4695
4692
  }
4696
4693
  function CollapseIcon() {
4697
4694
  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" }) });