concertina 0.11.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
@@ -188,18 +188,71 @@ import { Hum } from "concertina";
188
188
 
189
189
  The `className` is passed through to the shimmer so `1lh` inherits the correct font metrics. The shimmer is exactly as tall as the text it replaces because `1lh` resolves to the element's computed line-height. Not font-size, not a token, not a guess.
190
190
 
191
+ #### Vamp: ambient loading for entire subtrees
192
+
193
+ When many Hum instances share the same loading state (e.g. every cell in a table), threading `loading` to each one is boilerplate. Wrap the subtree in `<Vamp>` and every nested `<Hum>` picks it up automatically.
194
+
195
+ ```tsx
196
+ import { Vamp, Hum } from "concertina";
197
+
198
+ <Vamp loading={isLoading}>
199
+ <h2><Hum className="text-xl font-bold">{user?.name}</Hum></h2>
200
+ <p><Hum className="text-sm text-stone">{user?.email}</Hum></p>
201
+ <p><Hum className="text-sm">{user?.bio}</Hum></p>
202
+ </Vamp>
203
+ ```
204
+
205
+ No `loading` prop on any Hum. They all read from Vamp. An explicit `loading` prop on any individual Hum still overrides context.
206
+
207
+ Named after musical **vamping** — repeating a pattern while waiting for a cue.
208
+
191
209
  #### Hum props
192
210
 
193
211
  | Prop | Type | Default | Description |
194
212
  |------|------|---------|-------------|
195
- | `loading` | `boolean` | | Show shimmer (true) or children (false) |
213
+ | `loading` | `boolean` | Vamp context | Show shimmer (true) or children (false). Falls back to nearest `<Vamp>` when omitted. |
196
214
  | `as` | `ElementType` | `"span"` | HTML element to render |
197
215
  | `className` | `string` | | Applied to both shimmer and content states |
198
216
 
217
+ #### Vamp props
218
+
219
+ | Prop | Type | Description |
220
+ |------|------|-------------|
221
+ | `loading` | `boolean` | Whether the subtree is in a loading/warmup state |
222
+ | `children` | `ReactNode` | Content to wrap |
223
+
199
224
  > `StableText` is an alias for `Hum`.
200
225
 
201
226
  ---
202
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
+
203
256
  ## Ensemble: temporal stability for collections
204
257
 
205
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.
@@ -382,6 +435,8 @@ Scrolls an element to the top of its nearest scrollable ancestor. Only touches `
382
435
  |---------|------|
383
436
  | Two variants swap in one slot | Bellows + Slot |
384
437
  | Line of text loading from API | Hum |
438
+ | Many Hum instances share one loading state | Vamp + Hum |
439
+ | Card/table/page loading from API | Overture + Hum |
385
440
  | List loading from API | Ensemble |
386
441
  | Spinner replaced by loaded content | Gigbag + Warmup |
387
442
  | Accordion/table shimmer rows | Stub data + WarmupLine (wrapper-once pattern) |
@@ -1229,12 +1229,13 @@ var styles_default = `/* concertina \u2014 Radix Accordion expand/collapse with
1229
1229
  }
1230
1230
 
1231
1231
  /* Inactive Slot hiding \u2014 belt and suspenders.
1232
- Inline style sets visibility: hidden + opacity: 0 on the Slot div.
1233
- CSS backup catches edge cases where inline styles are overridden.
1232
+ Primary: inline style on the Slot element (visibility: hidden + opacity: 0).
1233
+ Inline styles can't be overridden by any CSS cascade \u2014 this is the
1234
+ bulletproof layer.
1235
+ Backup: CSS rules below catch edge cases (e.g. if inline styles are
1236
+ stripped by a framework or test harness).
1234
1237
  transition: none on descendants prevents children with transition-all
1235
- from animating the inherited visibility change (CSS visibility is
1236
- inherited; transition-all transitions it over the child's duration
1237
- instead of hiding instantly). */
1238
+ from animating the inherited visibility change. */
1238
1239
  .concertina-stable-slot > [inert] {
1239
1240
  visibility: hidden;
1240
1241
  opacity: 0;
package/dist/accordion.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  Trigger2,
9
9
  useConcertina,
10
10
  useExpanded
11
- } from "./chunk-4DDADLSW.js";
11
+ } from "./chunk-OGJMPKZX.js";
12
12
  export {
13
13
  ConcertinaContext,
14
14
  ConcertinaStore,
@@ -1190,12 +1190,13 @@ var styles_default = `/* concertina \u2014 Radix Accordion expand/collapse with
1190
1190
  }
1191
1191
 
1192
1192
  /* Inactive Slot hiding \u2014 belt and suspenders.
1193
- Inline style sets visibility: hidden + opacity: 0 on the Slot div.
1194
- CSS backup catches edge cases where inline styles are overridden.
1193
+ Primary: inline style on the Slot element (visibility: hidden + opacity: 0).
1194
+ Inline styles can't be overridden by any CSS cascade \u2014 this is the
1195
+ bulletproof layer.
1196
+ Backup: CSS rules below catch edge cases (e.g. if inline styles are
1197
+ stripped by a framework or test harness).
1195
1198
  transition: none on descendants prevents children with transition-all
1196
- from animating the inherited visibility change (CSS visibility is
1197
- inherited; transition-all transitions it over the child's duration
1198
- instead of hiding instantly). */
1199
+ from animating the inherited visibility change. */
1199
1200
  .concertina-stable-slot > [inert] {
1200
1201
  visibility: hidden;
1201
1202
  opacity: 0;
package/dist/index.cjs CHANGED
@@ -40,12 +40,15 @@ __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,
46
47
  StableSlot: () => Bellows,
47
48
  StableText: () => Hum,
48
49
  Trigger: () => Trigger2,
50
+ Vamp: () => Vamp,
51
+ VampContext: () => VampContext,
49
52
  Warmup: () => Warmup,
50
53
  WarmupLine: () => WarmupLine,
51
54
  pinToScrollTop: () => pinToScrollTop,
@@ -56,6 +59,7 @@ __export(index_exports, {
56
59
  useSize: () => useSize,
57
60
  useStableSlot: () => useStableSlot,
58
61
  useTransitionLock: () => useTransitionLock,
62
+ useVamp: () => useVamp,
59
63
  useWarmupExit: () => useWarmupExit
60
64
  });
61
65
  module.exports = __toCommonJS(index_exports);
@@ -1247,12 +1251,13 @@ var styles_default = `/* concertina \u2014 Radix Accordion expand/collapse with
1247
1251
  }
1248
1252
 
1249
1253
  /* Inactive Slot hiding \u2014 belt and suspenders.
1250
- Inline style sets visibility: hidden + opacity: 0 on the Slot div.
1251
- CSS backup catches edge cases where inline styles are overridden.
1254
+ Primary: inline style on the Slot element (visibility: hidden + opacity: 0).
1255
+ Inline styles can't be overridden by any CSS cascade \u2014 this is the
1256
+ bulletproof layer.
1257
+ Backup: CSS rules below catch edge cases (e.g. if inline styles are
1258
+ stripped by a framework or test harness).
1252
1259
  transition: none on descendants prevents children with transition-all
1253
- from animating the inherited visibility change (CSS visibility is
1254
- inherited; transition-all transitions it over the child's duration
1255
- instead of hiding instantly). */
1260
+ from animating the inherited visibility change. */
1256
1261
  .concertina-stable-slot > [inert] {
1257
1262
  visibility: hidden;
1258
1263
  opacity: 0;
@@ -1412,18 +1417,20 @@ var Bellows = (0, import_react11.forwardRef)(
1412
1417
  // src/components/slot.tsx
1413
1418
  var import_react12 = require("react");
1414
1419
  var import_jsx_runtime13 = require("react/jsx-runtime");
1420
+ var HIDDEN_STYLE = { visibility: "hidden", opacity: 0 };
1415
1421
  var Slot = (0, import_react12.forwardRef)(
1416
1422
  function Slot2({ active, note, as: Tag = "div", style, children, ...props }, ref) {
1417
1423
  (0, import_react12.useInsertionEffect)(injectStyles, []);
1418
1424
  (0, import_react12.useContext)(AxisContext);
1419
1425
  const activeNote = (0, import_react12.useContext)(ActiveNoteContext);
1420
- const isActive = active ?? (note != null && activeNote != null ? note === activeNote : true);
1426
+ const isActive = active ?? (note != null ? note === activeNote : true);
1427
+ const merged = isActive ? style : style ? { ...style, ...HIDDEN_STYLE } : HIDDEN_STYLE;
1421
1428
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1422
1429
  Tag,
1423
1430
  {
1424
1431
  ref,
1425
1432
  inert: !isActive || void 0,
1426
- style,
1433
+ style: merged,
1427
1434
  ...props,
1428
1435
  children
1429
1436
  }
@@ -1432,34 +1439,49 @@ var Slot = (0, import_react12.forwardRef)(
1432
1439
  );
1433
1440
 
1434
1441
  // src/components/hum.tsx
1442
+ var import_react14 = require("react");
1443
+
1444
+ // src/components/vamp.tsx
1435
1445
  var import_react13 = require("react");
1436
1446
  var import_jsx_runtime14 = require("react/jsx-runtime");
1437
- var Hum = (0, import_react13.forwardRef)(
1447
+ var VampContext = (0, import_react13.createContext)(false);
1448
+ function useVamp() {
1449
+ return (0, import_react13.useContext)(VampContext);
1450
+ }
1451
+ function Vamp({ loading, children }) {
1452
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(VampContext.Provider, { value: loading, children });
1453
+ }
1454
+
1455
+ // src/components/hum.tsx
1456
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1457
+ var Hum = (0, import_react14.forwardRef)(
1438
1458
  function Hum2({ loading, as: Tag = "span", className, children, ...props }, ref) {
1439
- (0, import_react13.useInsertionEffect)(injectStyles, []);
1440
- if (loading) {
1459
+ (0, import_react14.useInsertionEffect)(injectStyles, []);
1460
+ const vampLoading = useVamp();
1461
+ const isLoading = loading ?? vampLoading;
1462
+ if (isLoading) {
1441
1463
  const merged = className ? `concertina-warmup-line ${className}` : "concertina-warmup-line";
1442
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Tag, { ref, className: merged, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Tag, { inert: true, children }) });
1464
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Tag, { ref, className: merged, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Tag, { inert: true, children }) });
1443
1465
  }
1444
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Tag, { ref, className, ...props, children });
1466
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Tag, { ref, className, ...props, children });
1445
1467
  }
1446
1468
  );
1447
1469
 
1448
- // src/components/ensemble.tsx
1470
+ // src/components/overture.tsx
1449
1471
  var import_react18 = require("react");
1450
1472
 
1451
1473
  // src/components/gigbag.tsx
1452
- var import_react15 = require("react");
1474
+ var import_react16 = require("react");
1453
1475
 
1454
1476
  // src/primitives/use-stable-slot.ts
1455
- var import_react14 = require("react");
1477
+ var import_react15 = require("react");
1456
1478
  var RATCHET_FLOOR = -Infinity;
1457
1479
  function useStableSlot(options = {}) {
1458
1480
  const { axis = "both" } = options;
1459
- const [style, setStyle] = (0, import_react14.useState)({});
1460
- const maxRef = (0, import_react14.useRef)({ w: RATCHET_FLOOR, h: RATCHET_FLOOR });
1461
- const observerRef = (0, import_react14.useRef)(null);
1462
- const ref = (0, import_react14.useCallback)(
1481
+ const [style, setStyle] = (0, import_react15.useState)({});
1482
+ const maxRef = (0, import_react15.useRef)({ w: RATCHET_FLOOR, h: RATCHET_FLOOR });
1483
+ const observerRef = (0, import_react15.useRef)(null);
1484
+ const ref = (0, import_react15.useCallback)(
1463
1485
  (el) => {
1464
1486
  if (observerRef.current) {
1465
1487
  observerRef.current.disconnect();
@@ -1506,13 +1528,13 @@ function useStableSlot(options = {}) {
1506
1528
  }
1507
1529
 
1508
1530
  // src/components/gigbag.tsx
1509
- var import_jsx_runtime15 = require("react/jsx-runtime");
1510
- var Gigbag = (0, import_react15.forwardRef)(
1531
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1532
+ var Gigbag = (0, import_react16.forwardRef)(
1511
1533
  function Gigbag2({ axis = "height", as: Tag = "div", className, style, children, ...props }, fwdRef) {
1512
- (0, import_react15.useInsertionEffect)(injectStyles, []);
1534
+ (0, import_react16.useInsertionEffect)(injectStyles, []);
1513
1535
  const { ref: ratchetRef, style: ratchetStyle } = useStableSlot({ axis });
1514
1536
  const merged = className ? `concertina-gigbag ${className}` : "concertina-gigbag";
1515
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1537
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1516
1538
  Tag,
1517
1539
  {
1518
1540
  ref: mergeRefs(ratchetRef, fwdRef),
@@ -1525,32 +1547,6 @@ var Gigbag = (0, import_react15.forwardRef)(
1525
1547
  }
1526
1548
  );
1527
1549
 
1528
- // src/components/warmup.tsx
1529
- var import_react16 = require("react");
1530
- var import_jsx_runtime16 = require("react/jsx-runtime");
1531
- var Warmup = (0, import_react16.forwardRef)(
1532
- function Warmup2({ rows, columns, as: Tag = "div", className, children, ...props }, ref) {
1533
- (0, import_react16.useInsertionEffect)(injectStyles, []);
1534
- const merged = className ? `concertina-warmup ${className}` : "concertina-warmup";
1535
- const count2 = columns ? rows * columns : rows;
1536
- const cells = Array.from({ length: count2 }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "concertina-warmup-bone", children: [
1537
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "concertina-warmup-line" }),
1538
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "concertina-warmup-line" })
1539
- ] }, i));
1540
- const gridStyle = columns ? { gridTemplateColumns: `repeat(${columns}, auto)`, gridTemplateAreas: `'${"chamber ".repeat(columns).trim()}'` } : { gridTemplateAreas: "'chamber'" };
1541
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1542
- Tag,
1543
- {
1544
- ref,
1545
- className: merged,
1546
- style: gridStyle,
1547
- ...props,
1548
- children: cells
1549
- }
1550
- );
1551
- }
1552
- );
1553
-
1554
1550
  // src/primitives/use-warmup-exit.ts
1555
1551
  var import_react17 = require("react");
1556
1552
  function useWarmupExit(loading, duration) {
@@ -1573,8 +1569,48 @@ function useWarmupExit(loading, duration) {
1573
1569
  };
1574
1570
  }
1575
1571
 
1576
- // src/components/ensemble.tsx
1572
+ // src/components/overture.tsx
1577
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
+
1583
+ // src/components/ensemble.tsx
1584
+ var import_react20 = require("react");
1585
+
1586
+ // src/components/warmup.tsx
1587
+ var import_react19 = require("react");
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");
1578
1614
  function EnsembleInner({
1579
1615
  items,
1580
1616
  loading,
@@ -1585,33 +1621,33 @@ function EnsembleInner({
1585
1621
  className,
1586
1622
  ...props
1587
1623
  }, ref) {
1588
- (0, import_react18.useInsertionEffect)(injectStyles, []);
1624
+ (0, import_react20.useInsertionEffect)(injectStyles, []);
1589
1625
  const { showWarmup, exiting } = useWarmupExit(loading, exitDuration);
1590
1626
  const warmupClass = exiting ? className ? `concertina-warmup-exiting ${className}` : "concertina-warmup-exiting" : className;
1591
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Gigbag, { ref, axis: "height", as: Tag, ...props, children: showWarmup ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Warmup, { rows: stubCount, className: warmupClass }) : /* @__PURE__ */ (0, import_jsx_runtime17.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) }) });
1592
1628
  }
1593
- var Ensemble = (0, import_react18.forwardRef)(EnsembleInner);
1629
+ var Ensemble = (0, import_react20.forwardRef)(EnsembleInner);
1594
1630
 
1595
1631
  // src/components/warmup-line.tsx
1596
- var import_react19 = require("react");
1597
- var import_jsx_runtime18 = require("react/jsx-runtime");
1598
- var WarmupLine = (0, import_react19.forwardRef)(
1632
+ var import_react21 = require("react");
1633
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1634
+ var WarmupLine = (0, import_react21.forwardRef)(
1599
1635
  function WarmupLine2({ as: Tag = "div", className, ...props }, ref) {
1600
- (0, import_react19.useInsertionEffect)(injectStyles, []);
1636
+ (0, import_react21.useInsertionEffect)(injectStyles, []);
1601
1637
  const merged = className ? `concertina-warmup-line ${className}` : "concertina-warmup-line";
1602
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Tag, { ref, className: merged, ...props });
1638
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Tag, { ref, className: merged, ...props });
1603
1639
  }
1604
1640
  );
1605
1641
 
1606
1642
  // src/components/glide.tsx
1607
- var import_react21 = require("react");
1643
+ var import_react23 = require("react");
1608
1644
 
1609
1645
  // src/primitives/use-presence.ts
1610
- var import_react20 = require("react");
1646
+ var import_react22 = require("react");
1611
1647
  function usePresence2(show) {
1612
- const [mounted, setMounted] = (0, import_react20.useState)(show);
1613
- const [phase, setPhase] = (0, import_react20.useState)(show ? "entered" : "exiting");
1614
- (0, import_react20.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)(() => {
1615
1651
  if (show) {
1616
1652
  setMounted(true);
1617
1653
  setPhase("entering");
@@ -1619,7 +1655,7 @@ function usePresence2(show) {
1619
1655
  setPhase("exiting");
1620
1656
  }
1621
1657
  }, [show]);
1622
- const onAnimationEnd = (0, import_react20.useCallback)(
1658
+ const onAnimationEnd = (0, import_react22.useCallback)(
1623
1659
  (e) => {
1624
1660
  if (e.target !== e.currentTarget) return;
1625
1661
  if (phase === "entering") setPhase("entered");
@@ -1631,15 +1667,15 @@ function usePresence2(show) {
1631
1667
  }
1632
1668
 
1633
1669
  // src/components/glide.tsx
1634
- var import_jsx_runtime19 = require("react/jsx-runtime");
1635
- var Glide = (0, import_react21.forwardRef)(
1670
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1671
+ var Glide = (0, import_react23.forwardRef)(
1636
1672
  function Glide2({ show, as: Tag = "div", className, children, ...props }, ref) {
1637
- (0, import_react21.useInsertionEffect)(injectStyles, []);
1673
+ (0, import_react23.useInsertionEffect)(injectStyles, []);
1638
1674
  const { mounted, phase, onAnimationEnd } = usePresence2(show);
1639
1675
  if (!mounted) return null;
1640
1676
  const phaseClass = phase === "entering" ? "concertina-glide-entering" : phase === "exiting" ? "concertina-glide-exiting" : "";
1641
1677
  const merged = ["concertina-glide", phaseClass, className].filter(Boolean).join(" ");
1642
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1678
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1643
1679
  Tag,
1644
1680
  {
1645
1681
  ref,
@@ -1653,12 +1689,12 @@ var Glide = (0, import_react21.forwardRef)(
1653
1689
  );
1654
1690
 
1655
1691
  // src/primitives/use-size.ts
1656
- var import_react22 = require("react");
1692
+ var import_react24 = require("react");
1657
1693
  var NO_OBSERVATION = { width: Number.NaN, height: Number.NaN };
1658
1694
  function useSize() {
1659
- const [size, setSize] = (0, import_react22.useState)(NO_OBSERVATION);
1660
- const observerRef = (0, import_react22.useRef)(null);
1661
- const ref = (0, import_react22.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) => {
1662
1698
  if (observerRef.current) {
1663
1699
  observerRef.current.disconnect();
1664
1700
  observerRef.current = null;
@@ -1687,12 +1723,12 @@ function useSize() {
1687
1723
  }
1688
1724
 
1689
1725
  // src/accordion/use-concertina.ts
1690
- var import_react23 = require("react");
1726
+ var import_react25 = require("react");
1691
1727
  function useConcertina() {
1692
- const [value, setValue] = (0, import_react23.useState)("");
1693
- const [switching, setSwitching] = (0, import_react23.useState)(false);
1694
- const itemRefs = (0, import_react23.useRef)({});
1695
- const onValueChange = (0, import_react23.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)(
1696
1732
  (newValue) => {
1697
1733
  if (!newValue) {
1698
1734
  setSwitching(false);
@@ -1704,14 +1740,14 @@ function useConcertina() {
1704
1740
  },
1705
1741
  [value]
1706
1742
  );
1707
- (0, import_react23.useLayoutEffect)(() => {
1743
+ (0, import_react25.useLayoutEffect)(() => {
1708
1744
  if (!value) return;
1709
1745
  pinToScrollTop(itemRefs.current[value]);
1710
1746
  }, [value]);
1711
- (0, import_react23.useEffect)(() => {
1747
+ (0, import_react25.useEffect)(() => {
1712
1748
  if (switching) setSwitching(false);
1713
1749
  }, [switching]);
1714
- const getItemRef = (0, import_react23.useCallback)(
1750
+ const getItemRef = (0, import_react25.useCallback)(
1715
1751
  (id) => (el) => {
1716
1752
  itemRefs.current[id] = el;
1717
1753
  },
@@ -1736,12 +1772,15 @@ function useConcertina() {
1736
1772
  Header,
1737
1773
  Hum,
1738
1774
  Item,
1775
+ Overture,
1739
1776
  Root,
1740
1777
  Slot,
1741
1778
  StableCollection,
1742
1779
  StableSlot,
1743
1780
  StableText,
1744
1781
  Trigger,
1782
+ Vamp,
1783
+ VampContext,
1745
1784
  Warmup,
1746
1785
  WarmupLine,
1747
1786
  pinToScrollTop,
@@ -1752,5 +1791,6 @@ function useConcertina() {
1752
1791
  useSize,
1753
1792
  useStableSlot,
1754
1793
  useTransitionLock,
1794
+ useVamp,
1755
1795
  useWarmupExit
1756
1796
  });
package/dist/index.d.cts CHANGED
@@ -2,6 +2,7 @@ export { ConcertinaContext, ConcertinaRootProps, ConcertinaStore, Content, Item,
2
2
  export { Header, Trigger } from '@radix-ui/react-accordion';
3
3
  import * as react from 'react';
4
4
  import { HTMLAttributes, ElementType, ReactNode, Ref, ReactElement, AnimationEvent, DependencyList, CSSProperties } from 'react';
5
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
6
 
6
7
  type Axis = "width" | "height" | "both";
7
8
  interface BellowsProps extends HTMLAttributes<HTMLElement> {
@@ -34,15 +35,18 @@ interface SlotProps extends HTMLAttributes<HTMLElement> {
34
35
  * All slots overlap via CSS grid. Inactive slots are hidden
35
36
  * but still contribute to grid cell sizing.
36
37
  *
37
- * Inactive hiding is handled entirely by CSS via the [inert] attribute:
38
- * .concertina-stable-slot > [inert] { visibility: hidden; opacity: 0; }
39
- * No inline style overrides needed injectStyles guarantees the rules exist.
38
+ * Inactive hiding uses inline styles (can't be overridden by CSS cascade)
39
+ * plus the [inert] attribute for accessibility (non-focusable, non-interactive).
40
+ * CSS `.concertina-stable-slot > [inert]` serves as a backup.
40
41
  */
41
42
  declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<HTMLElement>>;
42
43
 
43
44
  interface HumProps extends HTMLAttributes<HTMLElement> {
44
- /** Whether data is loading. Shows shimmer when true, children when false. */
45
- loading: boolean;
45
+ /**
46
+ * Whether data is loading. Shows shimmer when true, children when false.
47
+ * When omitted, falls back to the nearest `<Vamp>` ancestor's loading state.
48
+ */
49
+ loading?: boolean;
46
50
  /** HTML element to render. Default: "span". */
47
51
  as?: ElementType;
48
52
  }
@@ -56,9 +60,63 @@ interface HumProps extends HTMLAttributes<HTMLElement> {
56
60
  *
57
61
  * The className is passed through so `1lh` inherits the correct font
58
62
  * metrics from the consuming context.
63
+ *
64
+ * When no explicit `loading` prop is provided, Hum reads from the
65
+ * nearest `<Vamp>` ancestor. This lets a single provider control
66
+ * shimmer state for an entire subtree.
59
67
  */
60
68
  declare const Hum: react.ForwardRefExoticComponent<HumProps & react.RefAttributes<HTMLElement>>;
61
69
 
70
+ /** Context carrying the ambient loading state set by Vamp. */
71
+ declare const VampContext: react.Context<boolean>;
72
+ /**
73
+ * Read the nearest Vamp's loading state.
74
+ * Returns `false` when no Vamp ancestor exists.
75
+ */
76
+ declare function useVamp(): boolean;
77
+ interface VampProps {
78
+ /** Whether the subtree is in a loading/warmup state. */
79
+ loading: boolean;
80
+ children: ReactNode;
81
+ }
82
+ /**
83
+ * Ambient loading provider — musical "vamping" (repeating a pattern
84
+ * while waiting for a cue).
85
+ *
86
+ * Wrapping a subtree in `<Vamp loading>` lets every nested `<Hum>`
87
+ * pick up the loading state automatically, without threading an
88
+ * explicit `loading` prop through every cell.
89
+ */
90
+ declare function Vamp({ loading, children }: VampProps): react_jsx_runtime.JSX.Element;
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
+
62
120
  interface EnsembleProps<T> extends Omit<HTMLAttributes<HTMLElement>, "children"> {
63
121
  /** Data items to render. */
64
122
  items: T[];
@@ -286,4 +344,4 @@ declare function useWarmupExit(loading: boolean, duration: number): {
286
344
  exiting: boolean;
287
345
  };
288
346
 
289
- 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, Warmup, WarmupLine, type WarmupLineProps, type WarmupProps, pinToScrollTop, usePresence, useScrollPin, useSize, useStableSlot, useTransitionLock, 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
@@ -2,6 +2,7 @@ export { ConcertinaContext, ConcertinaRootProps, ConcertinaStore, Content, Item,
2
2
  export { Header, Trigger } from '@radix-ui/react-accordion';
3
3
  import * as react from 'react';
4
4
  import { HTMLAttributes, ElementType, ReactNode, Ref, ReactElement, AnimationEvent, DependencyList, CSSProperties } from 'react';
5
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
6
 
6
7
  type Axis = "width" | "height" | "both";
7
8
  interface BellowsProps extends HTMLAttributes<HTMLElement> {
@@ -34,15 +35,18 @@ interface SlotProps extends HTMLAttributes<HTMLElement> {
34
35
  * All slots overlap via CSS grid. Inactive slots are hidden
35
36
  * but still contribute to grid cell sizing.
36
37
  *
37
- * Inactive hiding is handled entirely by CSS via the [inert] attribute:
38
- * .concertina-stable-slot > [inert] { visibility: hidden; opacity: 0; }
39
- * No inline style overrides needed injectStyles guarantees the rules exist.
38
+ * Inactive hiding uses inline styles (can't be overridden by CSS cascade)
39
+ * plus the [inert] attribute for accessibility (non-focusable, non-interactive).
40
+ * CSS `.concertina-stable-slot > [inert]` serves as a backup.
40
41
  */
41
42
  declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<HTMLElement>>;
42
43
 
43
44
  interface HumProps extends HTMLAttributes<HTMLElement> {
44
- /** Whether data is loading. Shows shimmer when true, children when false. */
45
- loading: boolean;
45
+ /**
46
+ * Whether data is loading. Shows shimmer when true, children when false.
47
+ * When omitted, falls back to the nearest `<Vamp>` ancestor's loading state.
48
+ */
49
+ loading?: boolean;
46
50
  /** HTML element to render. Default: "span". */
47
51
  as?: ElementType;
48
52
  }
@@ -56,9 +60,63 @@ interface HumProps extends HTMLAttributes<HTMLElement> {
56
60
  *
57
61
  * The className is passed through so `1lh` inherits the correct font
58
62
  * metrics from the consuming context.
63
+ *
64
+ * When no explicit `loading` prop is provided, Hum reads from the
65
+ * nearest `<Vamp>` ancestor. This lets a single provider control
66
+ * shimmer state for an entire subtree.
59
67
  */
60
68
  declare const Hum: react.ForwardRefExoticComponent<HumProps & react.RefAttributes<HTMLElement>>;
61
69
 
70
+ /** Context carrying the ambient loading state set by Vamp. */
71
+ declare const VampContext: react.Context<boolean>;
72
+ /**
73
+ * Read the nearest Vamp's loading state.
74
+ * Returns `false` when no Vamp ancestor exists.
75
+ */
76
+ declare function useVamp(): boolean;
77
+ interface VampProps {
78
+ /** Whether the subtree is in a loading/warmup state. */
79
+ loading: boolean;
80
+ children: ReactNode;
81
+ }
82
+ /**
83
+ * Ambient loading provider — musical "vamping" (repeating a pattern
84
+ * while waiting for a cue).
85
+ *
86
+ * Wrapping a subtree in `<Vamp loading>` lets every nested `<Hum>`
87
+ * pick up the loading state automatically, without threading an
88
+ * explicit `loading` prop through every cell.
89
+ */
90
+ declare function Vamp({ loading, children }: VampProps): react_jsx_runtime.JSX.Element;
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
+
62
120
  interface EnsembleProps<T> extends Omit<HTMLAttributes<HTMLElement>, "children"> {
63
121
  /** Data items to render. */
64
122
  items: T[];
@@ -286,4 +344,4 @@ declare function useWarmupExit(loading: boolean, duration: number): {
286
344
  exiting: boolean;
287
345
  };
288
346
 
289
- 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, Warmup, WarmupLine, type WarmupLineProps, type WarmupProps, pinToScrollTop, usePresence, useScrollPin, useSize, useStableSlot, useTransitionLock, 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
@@ -13,7 +13,7 @@ import {
13
13
  useExpanded,
14
14
  useScrollPin,
15
15
  useTransitionLock
16
- } from "./chunk-4DDADLSW.js";
16
+ } from "./chunk-OGJMPKZX.js";
17
17
 
18
18
  // src/components/bellows.tsx
19
19
  import {
@@ -39,18 +39,20 @@ import {
39
39
  useInsertionEffect as useInsertionEffect2
40
40
  } from "react";
41
41
  import { jsx as jsx2 } from "react/jsx-runtime";
42
+ var HIDDEN_STYLE = { visibility: "hidden", opacity: 0 };
42
43
  var Slot = forwardRef2(
43
44
  function Slot2({ active, note, as: Tag = "div", style, children, ...props }, ref) {
44
45
  useInsertionEffect2(injectStyles, []);
45
46
  useContext(AxisContext);
46
47
  const activeNote = useContext(ActiveNoteContext);
47
- const isActive = active ?? (note != null && activeNote != null ? note === activeNote : true);
48
+ const isActive = active ?? (note != null ? note === activeNote : true);
49
+ const merged = isActive ? style : style ? { ...style, ...HIDDEN_STYLE } : HIDDEN_STYLE;
48
50
  return /* @__PURE__ */ jsx2(
49
51
  Tag,
50
52
  {
51
53
  ref,
52
54
  inert: !isActive || void 0,
53
- style,
55
+ style: merged,
54
56
  ...props,
55
57
  children
56
58
  }
@@ -63,22 +65,37 @@ import {
63
65
  forwardRef as forwardRef3,
64
66
  useInsertionEffect as useInsertionEffect3
65
67
  } from "react";
68
+
69
+ // src/components/vamp.tsx
70
+ import { createContext as createContext2, useContext as useContext2 } from "react";
66
71
  import { jsx as jsx3 } from "react/jsx-runtime";
72
+ var VampContext = createContext2(false);
73
+ function useVamp() {
74
+ return useContext2(VampContext);
75
+ }
76
+ function Vamp({ loading, children }) {
77
+ return /* @__PURE__ */ jsx3(VampContext.Provider, { value: loading, children });
78
+ }
79
+
80
+ // src/components/hum.tsx
81
+ import { jsx as jsx4 } from "react/jsx-runtime";
67
82
  var Hum = forwardRef3(
68
83
  function Hum2({ loading, as: Tag = "span", className, children, ...props }, ref) {
69
84
  useInsertionEffect3(injectStyles, []);
70
- if (loading) {
85
+ const vampLoading = useVamp();
86
+ const isLoading = loading ?? vampLoading;
87
+ if (isLoading) {
71
88
  const merged = className ? `concertina-warmup-line ${className}` : "concertina-warmup-line";
72
- return /* @__PURE__ */ jsx3(Tag, { ref, className: merged, ...props, children: /* @__PURE__ */ jsx3(Tag, { inert: true, children }) });
89
+ return /* @__PURE__ */ jsx4(Tag, { ref, className: merged, ...props, children: /* @__PURE__ */ jsx4(Tag, { inert: true, children }) });
73
90
  }
74
- return /* @__PURE__ */ jsx3(Tag, { ref, className, ...props, children });
91
+ return /* @__PURE__ */ jsx4(Tag, { ref, className, ...props, children });
75
92
  }
76
93
  );
77
94
 
78
- // src/components/ensemble.tsx
95
+ // src/components/overture.tsx
79
96
  import {
80
- forwardRef as forwardRef6,
81
- useInsertionEffect as useInsertionEffect6
97
+ forwardRef as forwardRef5,
98
+ useInsertionEffect as useInsertionEffect5
82
99
  } from "react";
83
100
 
84
101
  // src/components/gigbag.tsx
@@ -139,13 +156,13 @@ function useStableSlot(options = {}) {
139
156
  }
140
157
 
141
158
  // src/components/gigbag.tsx
142
- import { jsx as jsx4 } from "react/jsx-runtime";
159
+ import { jsx as jsx5 } from "react/jsx-runtime";
143
160
  var Gigbag = forwardRef4(
144
161
  function Gigbag2({ axis = "height", as: Tag = "div", className, style, children, ...props }, fwdRef) {
145
162
  useInsertionEffect4(injectStyles, []);
146
163
  const { ref: ratchetRef, style: ratchetStyle } = useStableSlot({ axis });
147
164
  const merged = className ? `concertina-gigbag ${className}` : "concertina-gigbag";
148
- return /* @__PURE__ */ jsx4(
165
+ return /* @__PURE__ */ jsx5(
149
166
  Tag,
150
167
  {
151
168
  ref: mergeRefs(ratchetRef, fwdRef),
@@ -158,32 +175,6 @@ var Gigbag = forwardRef4(
158
175
  }
159
176
  );
160
177
 
161
- // src/components/warmup.tsx
162
- import { forwardRef as forwardRef5, useInsertionEffect as useInsertionEffect5 } from "react";
163
- import { jsx as jsx5, jsxs } from "react/jsx-runtime";
164
- var Warmup = forwardRef5(
165
- function Warmup2({ rows, columns, as: Tag = "div", className, children, ...props }, ref) {
166
- useInsertionEffect5(injectStyles, []);
167
- const merged = className ? `concertina-warmup ${className}` : "concertina-warmup";
168
- const count = columns ? rows * columns : rows;
169
- const cells = Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsxs("div", { className: "concertina-warmup-bone", children: [
170
- /* @__PURE__ */ jsx5("div", { className: "concertina-warmup-line" }),
171
- /* @__PURE__ */ jsx5("div", { className: "concertina-warmup-line" })
172
- ] }, i));
173
- const gridStyle = columns ? { gridTemplateColumns: `repeat(${columns}, auto)`, gridTemplateAreas: `'${"chamber ".repeat(columns).trim()}'` } : { gridTemplateAreas: "'chamber'" };
174
- return /* @__PURE__ */ jsx5(
175
- Tag,
176
- {
177
- ref,
178
- className: merged,
179
- style: gridStyle,
180
- ...props,
181
- children: cells
182
- }
183
- );
184
- }
185
- );
186
-
187
178
  // src/primitives/use-warmup-exit.ts
188
179
  import { useState as useState2, useEffect, useRef as useRef2 } from "react";
189
180
  function useWarmupExit(loading, duration) {
@@ -206,8 +197,51 @@ function useWarmupExit(loading, duration) {
206
197
  };
207
198
  }
208
199
 
209
- // src/components/ensemble.tsx
200
+ // src/components/overture.tsx
210
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
+
243
+ // src/components/ensemble.tsx
244
+ import { jsx as jsx8 } from "react/jsx-runtime";
211
245
  function EnsembleInner({
212
246
  items,
213
247
  loading,
@@ -218,28 +252,28 @@ function EnsembleInner({
218
252
  className,
219
253
  ...props
220
254
  }, ref) {
221
- useInsertionEffect6(injectStyles, []);
255
+ useInsertionEffect7(injectStyles, []);
222
256
  const { showWarmup, exiting } = useWarmupExit(loading, exitDuration);
223
257
  const warmupClass = exiting ? className ? `concertina-warmup-exiting ${className}` : "concertina-warmup-exiting" : className;
224
- return /* @__PURE__ */ jsx6(Gigbag, { ref, axis: "height", as: Tag, ...props, children: showWarmup ? /* @__PURE__ */ jsx6(Warmup, { rows: stubCount, className: warmupClass }) : /* @__PURE__ */ jsx6(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) }) });
225
259
  }
226
- var Ensemble = forwardRef6(EnsembleInner);
260
+ var Ensemble = forwardRef7(EnsembleInner);
227
261
 
228
262
  // src/components/warmup-line.tsx
229
- import { forwardRef as forwardRef7, useInsertionEffect as useInsertionEffect7 } from "react";
230
- import { jsx as jsx7 } from "react/jsx-runtime";
231
- 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(
232
266
  function WarmupLine2({ as: Tag = "div", className, ...props }, ref) {
233
- useInsertionEffect7(injectStyles, []);
267
+ useInsertionEffect8(injectStyles, []);
234
268
  const merged = className ? `concertina-warmup-line ${className}` : "concertina-warmup-line";
235
- return /* @__PURE__ */ jsx7(Tag, { ref, className: merged, ...props });
269
+ return /* @__PURE__ */ jsx9(Tag, { ref, className: merged, ...props });
236
270
  }
237
271
  );
238
272
 
239
273
  // src/components/glide.tsx
240
274
  import {
241
- forwardRef as forwardRef8,
242
- useInsertionEffect as useInsertionEffect8
275
+ forwardRef as forwardRef9,
276
+ useInsertionEffect as useInsertionEffect9
243
277
  } from "react";
244
278
 
245
279
  // src/primitives/use-presence.ts
@@ -271,15 +305,15 @@ function usePresence(show) {
271
305
  }
272
306
 
273
307
  // src/components/glide.tsx
274
- import { jsx as jsx8 } from "react/jsx-runtime";
275
- var Glide = forwardRef8(
308
+ import { jsx as jsx10 } from "react/jsx-runtime";
309
+ var Glide = forwardRef9(
276
310
  function Glide2({ show, as: Tag = "div", className, children, ...props }, ref) {
277
- useInsertionEffect8(injectStyles, []);
311
+ useInsertionEffect9(injectStyles, []);
278
312
  const { mounted, phase, onAnimationEnd } = usePresence(show);
279
313
  if (!mounted) return null;
280
314
  const phaseClass = phase === "entering" ? "concertina-glide-entering" : phase === "exiting" ? "concertina-glide-exiting" : "";
281
315
  const merged = ["concertina-glide", phaseClass, className].filter(Boolean).join(" ");
282
- return /* @__PURE__ */ jsx8(
316
+ return /* @__PURE__ */ jsx10(
283
317
  Tag,
284
318
  {
285
319
  ref,
@@ -336,12 +370,15 @@ export {
336
370
  Header,
337
371
  Hum,
338
372
  Item,
373
+ Overture,
339
374
  Root,
340
375
  Slot,
341
376
  Ensemble as StableCollection,
342
377
  Bellows as StableSlot,
343
378
  Hum as StableText,
344
379
  Trigger2 as Trigger,
380
+ Vamp,
381
+ VampContext,
345
382
  Warmup,
346
383
  WarmupLine,
347
384
  pinToScrollTop,
@@ -352,5 +389,6 @@ export {
352
389
  useSize,
353
390
  useStableSlot,
354
391
  useTransitionLock,
392
+ useVamp,
355
393
  useWarmupExit
356
394
  };
package/dist/styles.css CHANGED
@@ -67,12 +67,13 @@
67
67
  }
68
68
 
69
69
  /* Inactive Slot hiding — belt and suspenders.
70
- Inline style sets visibility: hidden + opacity: 0 on the Slot div.
71
- CSS backup catches edge cases where inline styles are overridden.
70
+ Primary: inline style on the Slot element (visibility: hidden + opacity: 0).
71
+ Inline styles can't be overridden by any CSS cascade — this is the
72
+ bulletproof layer.
73
+ Backup: CSS rules below catch edge cases (e.g. if inline styles are
74
+ stripped by a framework or test harness).
72
75
  transition: none on descendants prevents children with transition-all
73
- from animating the inherited visibility change (CSS visibility is
74
- inherited; transition-all transitions it over the child's duration
75
- instead of hiding instantly). */
76
+ from animating the inherited visibility change. */
76
77
  .concertina-stable-slot > [inert] {
77
78
  visibility: hidden;
78
79
  opacity: 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concertina",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "React toolkit for layout stability.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",