concertina 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
@@ -26,7 +26,7 @@ It's not a state problem. It's a **structure problem.** The box changed size bec
26
26
 
27
27
  Don't swap structures. Swap what's inside them.
28
28
 
29
- Concertina gives you three high-level components: **Bellows**, **Hum**, and **Ensemble**. They handle the math so you can focus on the music. CSS is auto-injected on first render. No manual imports needed.
29
+ Concertina gives you four high-level components: **Bellows**, **Hum**, **Overture**, and **Ensemble**. They handle the math so you can focus on the music. CSS is auto-injected on first render. No manual imports needed.
30
30
 
31
31
  ```bash
32
32
  npm install concertina
@@ -225,6 +225,34 @@ Named after musical **vamping** — repeating a pattern while waiting for a cue.
225
225
 
226
226
  ---
227
227
 
228
+ ## Overture: temporal stability for arbitrary content
229
+
230
+ A card, table, or page loads from an API. You want shimmer bones during loading, a smooth fade-out when data arrives, and the container must never collapse during the swap. You don't have a flat list — you have complex, nested JSX.
231
+
232
+ Overture composes `Vamp` (ambient loading context) + `Gigbag` (size ratchet) + `useWarmupExit` (exit transition) into a single wrapper. Write one JSX tree for both states. Nested `<Hum>` instances read loading state from the Vamp context automatically.
233
+
234
+ ```tsx
235
+ import { Overture, Hum } from "concertina";
236
+
237
+ <Overture loading={isLoading} exitDuration={150}>
238
+ <h2><Hum className="text-xl font-bold">{user?.name}</Hum></h2>
239
+ <p><Hum className="text-sm text-stone">{user?.email}</Hum></p>
240
+ <Button><Hum>Edit Profile</Hum></Button>
241
+ </Overture>
242
+ ```
243
+
244
+ During loading, every Hum renders a shimmer sized to its ghost children. When loading finishes, the shimmers fade out, real content appears, and the Gigbag ratchet prevents any height collapse.
245
+
246
+ #### Overture props
247
+
248
+ | Prop | Type | Default | Description |
249
+ |------|------|---------|-------------|
250
+ | `loading` | `boolean` | | Show shimmer (true) or content (false) |
251
+ | `exitDuration` | `number` | | Exit animation duration in ms (match `--concertina-close-duration`) |
252
+ | `as` | `ElementType` | `"div"` | HTML element to render |
253
+
254
+ ---
255
+
228
256
  ## Ensemble: temporal stability for collections
229
257
 
230
258
  A list loads from an API. You want shimmer rows while loading, then a smooth transition to real items, and the container must never collapse during the swap.
@@ -408,6 +436,7 @@ Scrolls an element to the top of its nearest scrollable ancestor. Only touches `
408
436
  | Two variants swap in one slot | Bellows + Slot |
409
437
  | Line of text loading from API | Hum |
410
438
  | Many Hum instances share one loading state | Vamp + Hum |
439
+ | Card/table/page loading from API | Overture + Hum |
411
440
  | List loading from API | Ensemble |
412
441
  | Spinner replaced by loaded content | Gigbag + Warmup |
413
442
  | Accordion/table shimmer rows | Stub data + WarmupLine (wrapper-once pattern) |
package/dist/index.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  Header: () => Header,
41
41
  Hum: () => Hum,
42
42
  Item: () => Item2,
43
+ Overture: () => Overture,
43
44
  Root: () => Root3,
44
45
  Slot: () => Slot,
45
46
  StableCollection: () => Ensemble,
@@ -1466,8 +1467,8 @@ var Hum = (0, import_react14.forwardRef)(
1466
1467
  }
1467
1468
  );
1468
1469
 
1469
- // src/components/ensemble.tsx
1470
- var import_react19 = require("react");
1470
+ // src/components/overture.tsx
1471
+ var import_react18 = require("react");
1471
1472
 
1472
1473
  // src/components/gigbag.tsx
1473
1474
  var import_react16 = require("react");
@@ -1546,38 +1547,12 @@ var Gigbag = (0, import_react16.forwardRef)(
1546
1547
  }
1547
1548
  );
1548
1549
 
1549
- // src/components/warmup.tsx
1550
- var import_react17 = require("react");
1551
- var import_jsx_runtime17 = require("react/jsx-runtime");
1552
- var Warmup = (0, import_react17.forwardRef)(
1553
- function Warmup2({ rows, columns, as: Tag = "div", className, children, ...props }, ref) {
1554
- (0, import_react17.useInsertionEffect)(injectStyles, []);
1555
- const merged = className ? `concertina-warmup ${className}` : "concertina-warmup";
1556
- const count2 = columns ? rows * columns : rows;
1557
- const cells = Array.from({ length: count2 }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "concertina-warmup-bone", children: [
1558
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "concertina-warmup-line" }),
1559
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "concertina-warmup-line" })
1560
- ] }, i));
1561
- const gridStyle = columns ? { gridTemplateColumns: `repeat(${columns}, auto)`, gridTemplateAreas: `'${"chamber ".repeat(columns).trim()}'` } : { gridTemplateAreas: "'chamber'" };
1562
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1563
- Tag,
1564
- {
1565
- ref,
1566
- className: merged,
1567
- style: gridStyle,
1568
- ...props,
1569
- children: cells
1570
- }
1571
- );
1572
- }
1573
- );
1574
-
1575
1550
  // src/primitives/use-warmup-exit.ts
1576
- var import_react18 = require("react");
1551
+ var import_react17 = require("react");
1577
1552
  function useWarmupExit(loading, duration) {
1578
- const [exiting, setExiting] = (0, import_react18.useState)(false);
1579
- const prevLoading = (0, import_react18.useRef)(loading);
1580
- (0, import_react18.useEffect)(() => {
1553
+ const [exiting, setExiting] = (0, import_react17.useState)(false);
1554
+ const prevLoading = (0, import_react17.useRef)(loading);
1555
+ (0, import_react17.useEffect)(() => {
1581
1556
  if (prevLoading.current && !loading) {
1582
1557
  setExiting(true);
1583
1558
  const id = setTimeout(() => setExiting(false), duration);
@@ -1594,8 +1569,48 @@ function useWarmupExit(loading, duration) {
1594
1569
  };
1595
1570
  }
1596
1571
 
1572
+ // src/components/overture.tsx
1573
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1574
+ var Overture = (0, import_react18.forwardRef)(
1575
+ function Overture2({ loading, exitDuration, as: Tag = "div", className, children, ...props }, ref) {
1576
+ (0, import_react18.useInsertionEffect)(injectStyles, []);
1577
+ const { showWarmup, exiting } = useWarmupExit(loading, exitDuration);
1578
+ const merged = exiting ? className ? `concertina-warmup-exiting ${className}` : "concertina-warmup-exiting" : className;
1579
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Gigbag, { ref, axis: "height", as: Tag, className: merged, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Vamp, { loading: showWarmup, children }) });
1580
+ }
1581
+ );
1582
+
1597
1583
  // src/components/ensemble.tsx
1584
+ var import_react20 = require("react");
1585
+
1586
+ // src/components/warmup.tsx
1587
+ var import_react19 = require("react");
1598
1588
  var import_jsx_runtime18 = require("react/jsx-runtime");
1589
+ var Warmup = (0, import_react19.forwardRef)(
1590
+ function Warmup2({ rows, columns, as: Tag = "div", className, children, ...props }, ref) {
1591
+ (0, import_react19.useInsertionEffect)(injectStyles, []);
1592
+ const merged = className ? `concertina-warmup ${className}` : "concertina-warmup";
1593
+ const count2 = columns ? rows * columns : rows;
1594
+ const cells = Array.from({ length: count2 }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "concertina-warmup-bone", children: [
1595
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "concertina-warmup-line" }),
1596
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "concertina-warmup-line" })
1597
+ ] }, i));
1598
+ const gridStyle = columns ? { gridTemplateColumns: `repeat(${columns}, auto)`, gridTemplateAreas: `'${"chamber ".repeat(columns).trim()}'` } : { gridTemplateAreas: "'chamber'" };
1599
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1600
+ Tag,
1601
+ {
1602
+ ref,
1603
+ className: merged,
1604
+ style: gridStyle,
1605
+ ...props,
1606
+ children: cells
1607
+ }
1608
+ );
1609
+ }
1610
+ );
1611
+
1612
+ // src/components/ensemble.tsx
1613
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1599
1614
  function EnsembleInner({
1600
1615
  items,
1601
1616
  loading,
@@ -1606,33 +1621,33 @@ function EnsembleInner({
1606
1621
  className,
1607
1622
  ...props
1608
1623
  }, ref) {
1609
- (0, import_react19.useInsertionEffect)(injectStyles, []);
1624
+ (0, import_react20.useInsertionEffect)(injectStyles, []);
1610
1625
  const { showWarmup, exiting } = useWarmupExit(loading, exitDuration);
1611
1626
  const warmupClass = exiting ? className ? `concertina-warmup-exiting ${className}` : "concertina-warmup-exiting" : className;
1612
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Gigbag, { ref, axis: "height", as: Tag, ...props, children: showWarmup ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Warmup, { rows: stubCount, className: warmupClass }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Tag, { className, children: items.map(renderItem) }) });
1627
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Gigbag, { ref, axis: "height", as: Tag, ...props, children: showWarmup ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Warmup, { rows: stubCount, className: warmupClass }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Tag, { className, children: items.map(renderItem) }) });
1613
1628
  }
1614
- var Ensemble = (0, import_react19.forwardRef)(EnsembleInner);
1629
+ var Ensemble = (0, import_react20.forwardRef)(EnsembleInner);
1615
1630
 
1616
1631
  // src/components/warmup-line.tsx
1617
- var import_react20 = require("react");
1618
- var import_jsx_runtime19 = require("react/jsx-runtime");
1619
- var WarmupLine = (0, import_react20.forwardRef)(
1632
+ var import_react21 = require("react");
1633
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1634
+ var WarmupLine = (0, import_react21.forwardRef)(
1620
1635
  function WarmupLine2({ as: Tag = "div", className, ...props }, ref) {
1621
- (0, import_react20.useInsertionEffect)(injectStyles, []);
1636
+ (0, import_react21.useInsertionEffect)(injectStyles, []);
1622
1637
  const merged = className ? `concertina-warmup-line ${className}` : "concertina-warmup-line";
1623
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Tag, { ref, className: merged, ...props });
1638
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Tag, { ref, className: merged, ...props });
1624
1639
  }
1625
1640
  );
1626
1641
 
1627
1642
  // src/components/glide.tsx
1628
- var import_react22 = require("react");
1643
+ var import_react23 = require("react");
1629
1644
 
1630
1645
  // src/primitives/use-presence.ts
1631
- var import_react21 = require("react");
1646
+ var import_react22 = require("react");
1632
1647
  function usePresence2(show) {
1633
- const [mounted, setMounted] = (0, import_react21.useState)(show);
1634
- const [phase, setPhase] = (0, import_react21.useState)(show ? "entered" : "exiting");
1635
- (0, import_react21.useEffect)(() => {
1648
+ const [mounted, setMounted] = (0, import_react22.useState)(show);
1649
+ const [phase, setPhase] = (0, import_react22.useState)(show ? "entered" : "exiting");
1650
+ (0, import_react22.useEffect)(() => {
1636
1651
  if (show) {
1637
1652
  setMounted(true);
1638
1653
  setPhase("entering");
@@ -1640,7 +1655,7 @@ function usePresence2(show) {
1640
1655
  setPhase("exiting");
1641
1656
  }
1642
1657
  }, [show]);
1643
- const onAnimationEnd = (0, import_react21.useCallback)(
1658
+ const onAnimationEnd = (0, import_react22.useCallback)(
1644
1659
  (e) => {
1645
1660
  if (e.target !== e.currentTarget) return;
1646
1661
  if (phase === "entering") setPhase("entered");
@@ -1652,15 +1667,15 @@ function usePresence2(show) {
1652
1667
  }
1653
1668
 
1654
1669
  // src/components/glide.tsx
1655
- var import_jsx_runtime20 = require("react/jsx-runtime");
1656
- var Glide = (0, import_react22.forwardRef)(
1670
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1671
+ var Glide = (0, import_react23.forwardRef)(
1657
1672
  function Glide2({ show, as: Tag = "div", className, children, ...props }, ref) {
1658
- (0, import_react22.useInsertionEffect)(injectStyles, []);
1673
+ (0, import_react23.useInsertionEffect)(injectStyles, []);
1659
1674
  const { mounted, phase, onAnimationEnd } = usePresence2(show);
1660
1675
  if (!mounted) return null;
1661
1676
  const phaseClass = phase === "entering" ? "concertina-glide-entering" : phase === "exiting" ? "concertina-glide-exiting" : "";
1662
1677
  const merged = ["concertina-glide", phaseClass, className].filter(Boolean).join(" ");
1663
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1678
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1664
1679
  Tag,
1665
1680
  {
1666
1681
  ref,
@@ -1674,12 +1689,12 @@ var Glide = (0, import_react22.forwardRef)(
1674
1689
  );
1675
1690
 
1676
1691
  // src/primitives/use-size.ts
1677
- var import_react23 = require("react");
1692
+ var import_react24 = require("react");
1678
1693
  var NO_OBSERVATION = { width: Number.NaN, height: Number.NaN };
1679
1694
  function useSize() {
1680
- const [size, setSize] = (0, import_react23.useState)(NO_OBSERVATION);
1681
- const observerRef = (0, import_react23.useRef)(null);
1682
- const ref = (0, import_react23.useCallback)((el) => {
1695
+ const [size, setSize] = (0, import_react24.useState)(NO_OBSERVATION);
1696
+ const observerRef = (0, import_react24.useRef)(null);
1697
+ const ref = (0, import_react24.useCallback)((el) => {
1683
1698
  if (observerRef.current) {
1684
1699
  observerRef.current.disconnect();
1685
1700
  observerRef.current = null;
@@ -1708,12 +1723,12 @@ function useSize() {
1708
1723
  }
1709
1724
 
1710
1725
  // src/accordion/use-concertina.ts
1711
- var import_react24 = require("react");
1726
+ var import_react25 = require("react");
1712
1727
  function useConcertina() {
1713
- const [value, setValue] = (0, import_react24.useState)("");
1714
- const [switching, setSwitching] = (0, import_react24.useState)(false);
1715
- const itemRefs = (0, import_react24.useRef)({});
1716
- const onValueChange = (0, import_react24.useCallback)(
1728
+ const [value, setValue] = (0, import_react25.useState)("");
1729
+ const [switching, setSwitching] = (0, import_react25.useState)(false);
1730
+ const itemRefs = (0, import_react25.useRef)({});
1731
+ const onValueChange = (0, import_react25.useCallback)(
1717
1732
  (newValue) => {
1718
1733
  if (!newValue) {
1719
1734
  setSwitching(false);
@@ -1725,14 +1740,14 @@ function useConcertina() {
1725
1740
  },
1726
1741
  [value]
1727
1742
  );
1728
- (0, import_react24.useLayoutEffect)(() => {
1743
+ (0, import_react25.useLayoutEffect)(() => {
1729
1744
  if (!value) return;
1730
1745
  pinToScrollTop(itemRefs.current[value]);
1731
1746
  }, [value]);
1732
- (0, import_react24.useEffect)(() => {
1747
+ (0, import_react25.useEffect)(() => {
1733
1748
  if (switching) setSwitching(false);
1734
1749
  }, [switching]);
1735
- const getItemRef = (0, import_react24.useCallback)(
1750
+ const getItemRef = (0, import_react25.useCallback)(
1736
1751
  (id) => (el) => {
1737
1752
  itemRefs.current[id] = el;
1738
1753
  },
@@ -1757,6 +1772,7 @@ function useConcertina() {
1757
1772
  Header,
1758
1773
  Hum,
1759
1774
  Item,
1775
+ Overture,
1760
1776
  Root,
1761
1777
  Slot,
1762
1778
  StableCollection,
package/dist/index.d.cts CHANGED
@@ -89,6 +89,34 @@ interface VampProps {
89
89
  */
90
90
  declare function Vamp({ loading, children }: VampProps): react_jsx_runtime.JSX.Element;
91
91
 
92
+ interface OvertureProps extends HTMLAttributes<HTMLElement> {
93
+ /** Whether data is loading. Sets Vamp context for all nested Hum instances. */
94
+ loading: boolean;
95
+ /** Exit animation duration in ms. Must match CSS --concertina-close-duration. */
96
+ exitDuration: number;
97
+ /** HTML element to render. Default: "div". */
98
+ as?: ElementType;
99
+ }
100
+ /**
101
+ * Loading-aware subtree wrapper — the opening act before the real content.
102
+ *
103
+ * Composes three behaviors into one component:
104
+ * - **Vamp** context: every nested `<Hum>` reads loading state automatically.
105
+ * - **Gigbag** ratchet: container never shrinks during the shimmer-to-content swap.
106
+ * - **Exit transition**: applies `concertina-warmup-exiting` class during the
107
+ * fade-out so shimmer lines animate before real content mounts.
108
+ *
109
+ * Write one JSX tree for both states. Hum instances handle the visual toggle.
110
+ *
111
+ * ```tsx
112
+ * <Overture loading={isLoading} exitDuration={150}>
113
+ * <h2><Hum className="text-xl">{user?.name}</Hum></h2>
114
+ * <p><Hum className="text-sm">{user?.bio}</Hum></p>
115
+ * </Overture>
116
+ * ```
117
+ */
118
+ declare const Overture: react.ForwardRefExoticComponent<OvertureProps & react.RefAttributes<HTMLElement>>;
119
+
92
120
  interface EnsembleProps<T> extends Omit<HTMLAttributes<HTMLElement>, "children"> {
93
121
  /** Data items to render. */
94
122
  items: T[];
@@ -316,4 +344,4 @@ declare function useWarmupExit(loading: boolean, duration: number): {
316
344
  exiting: boolean;
317
345
  };
318
346
 
319
- export { type Axis, Bellows, type BellowsProps, Ensemble, type EnsembleProps, Gigbag, type GigbagProps, Glide, type GlideProps, Hum, type HumProps, type Phase, type Size, Slot, type SlotProps, Ensemble as StableCollection, type EnsembleProps as StableCollectionProps, Bellows as StableSlot, type BellowsProps as StableSlotProps, Hum as StableText, type HumProps as StableTextProps, type UsePresenceReturn, Vamp, VampContext, type VampProps, Warmup, WarmupLine, type WarmupLineProps, type WarmupProps, pinToScrollTop, usePresence, useScrollPin, useSize, useStableSlot, useTransitionLock, useVamp, useWarmupExit };
347
+ export { type Axis, Bellows, type BellowsProps, Ensemble, type EnsembleProps, Gigbag, type GigbagProps, Glide, type GlideProps, Hum, type HumProps, Overture, type OvertureProps, type Phase, type Size, Slot, type SlotProps, Ensemble as StableCollection, type EnsembleProps as StableCollectionProps, Bellows as StableSlot, type BellowsProps as StableSlotProps, Hum as StableText, type HumProps as StableTextProps, type UsePresenceReturn, Vamp, VampContext, type VampProps, Warmup, WarmupLine, type WarmupLineProps, type WarmupProps, pinToScrollTop, usePresence, useScrollPin, useSize, useStableSlot, useTransitionLock, useVamp, useWarmupExit };
package/dist/index.d.ts CHANGED
@@ -89,6 +89,34 @@ interface VampProps {
89
89
  */
90
90
  declare function Vamp({ loading, children }: VampProps): react_jsx_runtime.JSX.Element;
91
91
 
92
+ interface OvertureProps extends HTMLAttributes<HTMLElement> {
93
+ /** Whether data is loading. Sets Vamp context for all nested Hum instances. */
94
+ loading: boolean;
95
+ /** Exit animation duration in ms. Must match CSS --concertina-close-duration. */
96
+ exitDuration: number;
97
+ /** HTML element to render. Default: "div". */
98
+ as?: ElementType;
99
+ }
100
+ /**
101
+ * Loading-aware subtree wrapper — the opening act before the real content.
102
+ *
103
+ * Composes three behaviors into one component:
104
+ * - **Vamp** context: every nested `<Hum>` reads loading state automatically.
105
+ * - **Gigbag** ratchet: container never shrinks during the shimmer-to-content swap.
106
+ * - **Exit transition**: applies `concertina-warmup-exiting` class during the
107
+ * fade-out so shimmer lines animate before real content mounts.
108
+ *
109
+ * Write one JSX tree for both states. Hum instances handle the visual toggle.
110
+ *
111
+ * ```tsx
112
+ * <Overture loading={isLoading} exitDuration={150}>
113
+ * <h2><Hum className="text-xl">{user?.name}</Hum></h2>
114
+ * <p><Hum className="text-sm">{user?.bio}</Hum></p>
115
+ * </Overture>
116
+ * ```
117
+ */
118
+ declare const Overture: react.ForwardRefExoticComponent<OvertureProps & react.RefAttributes<HTMLElement>>;
119
+
92
120
  interface EnsembleProps<T> extends Omit<HTMLAttributes<HTMLElement>, "children"> {
93
121
  /** Data items to render. */
94
122
  items: T[];
@@ -316,4 +344,4 @@ declare function useWarmupExit(loading: boolean, duration: number): {
316
344
  exiting: boolean;
317
345
  };
318
346
 
319
- export { type Axis, Bellows, type BellowsProps, Ensemble, type EnsembleProps, Gigbag, type GigbagProps, Glide, type GlideProps, Hum, type HumProps, type Phase, type Size, Slot, type SlotProps, Ensemble as StableCollection, type EnsembleProps as StableCollectionProps, Bellows as StableSlot, type BellowsProps as StableSlotProps, Hum as StableText, type HumProps as StableTextProps, type UsePresenceReturn, Vamp, VampContext, type VampProps, Warmup, WarmupLine, type WarmupLineProps, type WarmupProps, pinToScrollTop, usePresence, useScrollPin, useSize, useStableSlot, useTransitionLock, useVamp, useWarmupExit };
347
+ export { type Axis, Bellows, type BellowsProps, Ensemble, type EnsembleProps, Gigbag, type GigbagProps, Glide, type GlideProps, Hum, type HumProps, Overture, type OvertureProps, type Phase, type Size, Slot, type SlotProps, Ensemble as StableCollection, type EnsembleProps as StableCollectionProps, Bellows as StableSlot, type BellowsProps as StableSlotProps, Hum as StableText, type HumProps as StableTextProps, type UsePresenceReturn, Vamp, VampContext, type VampProps, Warmup, WarmupLine, type WarmupLineProps, type WarmupProps, pinToScrollTop, usePresence, useScrollPin, useSize, useStableSlot, useTransitionLock, useVamp, useWarmupExit };
package/dist/index.js CHANGED
@@ -92,10 +92,10 @@ var Hum = forwardRef3(
92
92
  }
93
93
  );
94
94
 
95
- // src/components/ensemble.tsx
95
+ // src/components/overture.tsx
96
96
  import {
97
- forwardRef as forwardRef6,
98
- useInsertionEffect as useInsertionEffect6
97
+ forwardRef as forwardRef5,
98
+ useInsertionEffect as useInsertionEffect5
99
99
  } from "react";
100
100
 
101
101
  // src/components/gigbag.tsx
@@ -175,32 +175,6 @@ var Gigbag = forwardRef4(
175
175
  }
176
176
  );
177
177
 
178
- // src/components/warmup.tsx
179
- import { forwardRef as forwardRef5, useInsertionEffect as useInsertionEffect5 } from "react";
180
- import { jsx as jsx6, jsxs } from "react/jsx-runtime";
181
- var Warmup = forwardRef5(
182
- function Warmup2({ rows, columns, as: Tag = "div", className, children, ...props }, ref) {
183
- useInsertionEffect5(injectStyles, []);
184
- const merged = className ? `concertina-warmup ${className}` : "concertina-warmup";
185
- const count = columns ? rows * columns : rows;
186
- const cells = Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsxs("div", { className: "concertina-warmup-bone", children: [
187
- /* @__PURE__ */ jsx6("div", { className: "concertina-warmup-line" }),
188
- /* @__PURE__ */ jsx6("div", { className: "concertina-warmup-line" })
189
- ] }, i));
190
- const gridStyle = columns ? { gridTemplateColumns: `repeat(${columns}, auto)`, gridTemplateAreas: `'${"chamber ".repeat(columns).trim()}'` } : { gridTemplateAreas: "'chamber'" };
191
- return /* @__PURE__ */ jsx6(
192
- Tag,
193
- {
194
- ref,
195
- className: merged,
196
- style: gridStyle,
197
- ...props,
198
- children: cells
199
- }
200
- );
201
- }
202
- );
203
-
204
178
  // src/primitives/use-warmup-exit.ts
205
179
  import { useState as useState2, useEffect, useRef as useRef2 } from "react";
206
180
  function useWarmupExit(loading, duration) {
@@ -223,8 +197,51 @@ function useWarmupExit(loading, duration) {
223
197
  };
224
198
  }
225
199
 
200
+ // src/components/overture.tsx
201
+ import { jsx as jsx6 } from "react/jsx-runtime";
202
+ var Overture = forwardRef5(
203
+ function Overture2({ loading, exitDuration, as: Tag = "div", className, children, ...props }, ref) {
204
+ useInsertionEffect5(injectStyles, []);
205
+ const { showWarmup, exiting } = useWarmupExit(loading, exitDuration);
206
+ const merged = exiting ? className ? `concertina-warmup-exiting ${className}` : "concertina-warmup-exiting" : className;
207
+ return /* @__PURE__ */ jsx6(Gigbag, { ref, axis: "height", as: Tag, className: merged, ...props, children: /* @__PURE__ */ jsx6(Vamp, { loading: showWarmup, children }) });
208
+ }
209
+ );
210
+
211
+ // src/components/ensemble.tsx
212
+ import {
213
+ forwardRef as forwardRef7,
214
+ useInsertionEffect as useInsertionEffect7
215
+ } from "react";
216
+
217
+ // src/components/warmup.tsx
218
+ import { forwardRef as forwardRef6, useInsertionEffect as useInsertionEffect6 } from "react";
219
+ import { jsx as jsx7, jsxs } from "react/jsx-runtime";
220
+ var Warmup = forwardRef6(
221
+ function Warmup2({ rows, columns, as: Tag = "div", className, children, ...props }, ref) {
222
+ useInsertionEffect6(injectStyles, []);
223
+ const merged = className ? `concertina-warmup ${className}` : "concertina-warmup";
224
+ const count = columns ? rows * columns : rows;
225
+ const cells = Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsxs("div", { className: "concertina-warmup-bone", children: [
226
+ /* @__PURE__ */ jsx7("div", { className: "concertina-warmup-line" }),
227
+ /* @__PURE__ */ jsx7("div", { className: "concertina-warmup-line" })
228
+ ] }, i));
229
+ const gridStyle = columns ? { gridTemplateColumns: `repeat(${columns}, auto)`, gridTemplateAreas: `'${"chamber ".repeat(columns).trim()}'` } : { gridTemplateAreas: "'chamber'" };
230
+ return /* @__PURE__ */ jsx7(
231
+ Tag,
232
+ {
233
+ ref,
234
+ className: merged,
235
+ style: gridStyle,
236
+ ...props,
237
+ children: cells
238
+ }
239
+ );
240
+ }
241
+ );
242
+
226
243
  // src/components/ensemble.tsx
227
- import { jsx as jsx7 } from "react/jsx-runtime";
244
+ import { jsx as jsx8 } from "react/jsx-runtime";
228
245
  function EnsembleInner({
229
246
  items,
230
247
  loading,
@@ -235,28 +252,28 @@ function EnsembleInner({
235
252
  className,
236
253
  ...props
237
254
  }, ref) {
238
- useInsertionEffect6(injectStyles, []);
255
+ useInsertionEffect7(injectStyles, []);
239
256
  const { showWarmup, exiting } = useWarmupExit(loading, exitDuration);
240
257
  const warmupClass = exiting ? className ? `concertina-warmup-exiting ${className}` : "concertina-warmup-exiting" : className;
241
- return /* @__PURE__ */ jsx7(Gigbag, { ref, axis: "height", as: Tag, ...props, children: showWarmup ? /* @__PURE__ */ jsx7(Warmup, { rows: stubCount, className: warmupClass }) : /* @__PURE__ */ jsx7(Tag, { className, children: items.map(renderItem) }) });
258
+ return /* @__PURE__ */ jsx8(Gigbag, { ref, axis: "height", as: Tag, ...props, children: showWarmup ? /* @__PURE__ */ jsx8(Warmup, { rows: stubCount, className: warmupClass }) : /* @__PURE__ */ jsx8(Tag, { className, children: items.map(renderItem) }) });
242
259
  }
243
- var Ensemble = forwardRef6(EnsembleInner);
260
+ var Ensemble = forwardRef7(EnsembleInner);
244
261
 
245
262
  // src/components/warmup-line.tsx
246
- import { forwardRef as forwardRef7, useInsertionEffect as useInsertionEffect7 } from "react";
247
- import { jsx as jsx8 } from "react/jsx-runtime";
248
- var WarmupLine = forwardRef7(
263
+ import { forwardRef as forwardRef8, useInsertionEffect as useInsertionEffect8 } from "react";
264
+ import { jsx as jsx9 } from "react/jsx-runtime";
265
+ var WarmupLine = forwardRef8(
249
266
  function WarmupLine2({ as: Tag = "div", className, ...props }, ref) {
250
- useInsertionEffect7(injectStyles, []);
267
+ useInsertionEffect8(injectStyles, []);
251
268
  const merged = className ? `concertina-warmup-line ${className}` : "concertina-warmup-line";
252
- return /* @__PURE__ */ jsx8(Tag, { ref, className: merged, ...props });
269
+ return /* @__PURE__ */ jsx9(Tag, { ref, className: merged, ...props });
253
270
  }
254
271
  );
255
272
 
256
273
  // src/components/glide.tsx
257
274
  import {
258
- forwardRef as forwardRef8,
259
- useInsertionEffect as useInsertionEffect8
275
+ forwardRef as forwardRef9,
276
+ useInsertionEffect as useInsertionEffect9
260
277
  } from "react";
261
278
 
262
279
  // src/primitives/use-presence.ts
@@ -288,15 +305,15 @@ function usePresence(show) {
288
305
  }
289
306
 
290
307
  // src/components/glide.tsx
291
- import { jsx as jsx9 } from "react/jsx-runtime";
292
- var Glide = forwardRef8(
308
+ import { jsx as jsx10 } from "react/jsx-runtime";
309
+ var Glide = forwardRef9(
293
310
  function Glide2({ show, as: Tag = "div", className, children, ...props }, ref) {
294
- useInsertionEffect8(injectStyles, []);
311
+ useInsertionEffect9(injectStyles, []);
295
312
  const { mounted, phase, onAnimationEnd } = usePresence(show);
296
313
  if (!mounted) return null;
297
314
  const phaseClass = phase === "entering" ? "concertina-glide-entering" : phase === "exiting" ? "concertina-glide-exiting" : "";
298
315
  const merged = ["concertina-glide", phaseClass, className].filter(Boolean).join(" ");
299
- return /* @__PURE__ */ jsx9(
316
+ return /* @__PURE__ */ jsx10(
300
317
  Tag,
301
318
  {
302
319
  ref,
@@ -353,6 +370,7 @@ export {
353
370
  Header,
354
371
  Hum,
355
372
  Item,
373
+ Overture,
356
374
  Root,
357
375
  Slot,
358
376
  Ensemble as StableCollection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concertina",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "React toolkit for layout stability.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",