concertina 0.6.1 → 0.7.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/dist/index.cjs CHANGED
@@ -33,12 +33,15 @@ __export(index_exports, {
33
33
  ConcertinaContext: () => ConcertinaContext,
34
34
  ConcertinaStore: () => ConcertinaStore,
35
35
  Content: () => Content3,
36
+ Gigbag: () => Gigbag,
37
+ Glide: () => Glide,
36
38
  Header: () => Header,
37
39
  Item: () => Item2,
38
40
  Root: () => Root3,
39
41
  Slot: () => Slot,
40
42
  StableSlot: () => StableSlot,
41
43
  Trigger: () => Trigger2,
44
+ Warmup: () => Warmup,
42
45
  pinToScrollTop: () => pinToScrollTop,
43
46
  useConcertina: () => useConcertina,
44
47
  useExpanded: () => useExpanded,
@@ -1255,13 +1258,96 @@ function useStableSlot(options = {}) {
1255
1258
  return { ref, style };
1256
1259
  }
1257
1260
 
1258
- // src/use-concertina.ts
1261
+ // src/gigbag.tsx
1259
1262
  var import_react13 = require("react");
1263
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1264
+ var Gigbag = (0, import_react13.forwardRef)(
1265
+ function Gigbag2({ axis = "height", as: Tag = "div", className, style, children, ...props }, fwdRef) {
1266
+ const { ref: ratchetRef, style: ratchetStyle } = useStableSlot({ axis });
1267
+ const merged = className ? `concertina-gigbag ${className}` : "concertina-gigbag";
1268
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1269
+ Tag,
1270
+ {
1271
+ ref: (el) => {
1272
+ ratchetRef(el);
1273
+ if (typeof fwdRef === "function") fwdRef(el);
1274
+ else if (fwdRef) fwdRef.current = el;
1275
+ },
1276
+ className: merged,
1277
+ style: { ...ratchetStyle, ...style },
1278
+ ...props,
1279
+ children
1280
+ }
1281
+ );
1282
+ }
1283
+ );
1284
+
1285
+ // src/warmup.tsx
1286
+ var import_react14 = require("react");
1287
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1288
+ var Warmup = (0, import_react14.forwardRef)(
1289
+ function Warmup2({ rows = 3, columns = 1, as: Tag = "div", className, children, ...props }, ref) {
1290
+ const merged = className ? `concertina-warmup ${className}` : "concertina-warmup";
1291
+ const cells = Array.from({ length: rows * columns }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "concertina-warmup-bone" }, i));
1292
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1293
+ Tag,
1294
+ {
1295
+ ref,
1296
+ className: merged,
1297
+ style: { gridTemplateColumns: `repeat(${columns}, 1fr)` },
1298
+ ...props,
1299
+ children: cells
1300
+ }
1301
+ );
1302
+ }
1303
+ );
1304
+
1305
+ // src/glide.tsx
1306
+ var import_react15 = require("react");
1307
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1308
+ var Glide = (0, import_react15.forwardRef)(
1309
+ function Glide2({ show, as: Tag = "div", className, children, ...props }, ref) {
1310
+ const [mounted, setMounted] = (0, import_react15.useState)(show);
1311
+ const [phase, setPhase] = (0, import_react15.useState)(show ? "entered" : "exiting");
1312
+ (0, import_react15.useEffect)(() => {
1313
+ if (show) {
1314
+ setMounted(true);
1315
+ setPhase("entering");
1316
+ } else if (mounted) {
1317
+ setPhase("exiting");
1318
+ }
1319
+ }, [show]);
1320
+ const onAnimationEnd = (0, import_react15.useCallback)(
1321
+ (e) => {
1322
+ if (e.target !== e.currentTarget) return;
1323
+ if (phase === "entering") setPhase("entered");
1324
+ if (phase === "exiting") setMounted(false);
1325
+ },
1326
+ [phase]
1327
+ );
1328
+ if (!mounted) return null;
1329
+ const phaseClass = phase === "entering" ? "concertina-glide-entering" : phase === "exiting" ? "concertina-glide-exiting" : "";
1330
+ const merged = ["concertina-glide", phaseClass, className].filter(Boolean).join(" ");
1331
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1332
+ Tag,
1333
+ {
1334
+ ref,
1335
+ className: merged,
1336
+ onAnimationEnd,
1337
+ ...props,
1338
+ children
1339
+ }
1340
+ );
1341
+ }
1342
+ );
1343
+
1344
+ // src/use-concertina.ts
1345
+ var import_react16 = require("react");
1260
1346
  function useConcertina() {
1261
- const [value, setValue] = (0, import_react13.useState)("");
1262
- const [switching, setSwitching] = (0, import_react13.useState)(false);
1263
- const itemRefs = (0, import_react13.useRef)({});
1264
- const onValueChange = (0, import_react13.useCallback)(
1347
+ const [value, setValue] = (0, import_react16.useState)("");
1348
+ const [switching, setSwitching] = (0, import_react16.useState)(false);
1349
+ const itemRefs = (0, import_react16.useRef)({});
1350
+ const onValueChange = (0, import_react16.useCallback)(
1265
1351
  (newValue) => {
1266
1352
  if (!newValue) {
1267
1353
  setSwitching(false);
@@ -1273,14 +1359,14 @@ function useConcertina() {
1273
1359
  },
1274
1360
  [value]
1275
1361
  );
1276
- (0, import_react13.useLayoutEffect)(() => {
1362
+ (0, import_react16.useLayoutEffect)(() => {
1277
1363
  if (!value) return;
1278
1364
  pinToScrollTop(itemRefs.current[value]);
1279
1365
  }, [value]);
1280
- (0, import_react13.useEffect)(() => {
1366
+ (0, import_react16.useEffect)(() => {
1281
1367
  if (switching) setSwitching(false);
1282
1368
  }, [switching]);
1283
- const getItemRef = (0, import_react13.useCallback)(
1369
+ const getItemRef = (0, import_react16.useCallback)(
1284
1370
  (id) => (el) => {
1285
1371
  itemRefs.current[id] = el;
1286
1372
  },
@@ -1298,12 +1384,15 @@ function useConcertina() {
1298
1384
  ConcertinaContext,
1299
1385
  ConcertinaStore,
1300
1386
  Content,
1387
+ Gigbag,
1388
+ Glide,
1301
1389
  Header,
1302
1390
  Item,
1303
1391
  Root,
1304
1392
  Slot,
1305
1393
  StableSlot,
1306
1394
  Trigger,
1395
+ Warmup,
1307
1396
  pinToScrollTop,
1308
1397
  useConcertina,
1309
1398
  useExpanded,
package/dist/index.d.cts CHANGED
@@ -115,6 +115,60 @@ declare function useTransitionLock(): {
115
115
  readonly lock: () => void;
116
116
  };
117
117
 
118
+ interface GigbagProps extends HTMLAttributes<HTMLElement> {
119
+ /** Which axis to ratchet. Default: "height". */
120
+ axis?: Axis;
121
+ /** HTML element to render. Default: "div". */
122
+ as?: ElementType;
123
+ }
124
+ /**
125
+ * Size-reserving container.
126
+ *
127
+ * Remembers its largest-ever size (ResizeObserver ratchet) and never
128
+ * shrinks. Swap a spinner for a table inside — no reflow.
129
+ *
130
+ * Uses `contain: layout style` to isolate internal reflow from
131
+ * ancestors.
132
+ */
133
+ declare const Gigbag: react.ForwardRefExoticComponent<GigbagProps & react.RefAttributes<HTMLElement>>;
134
+
135
+ interface WarmupProps extends HTMLAttributes<HTMLElement> {
136
+ /** Number of placeholder rows. Default: 3. */
137
+ rows?: number;
138
+ /** Number of columns per row. Default: 1. */
139
+ columns?: number;
140
+ /** HTML element to render. Default: "div". */
141
+ as?: ElementType;
142
+ }
143
+ /**
144
+ * Structural placeholder — CSS-only shimmer grid.
145
+ *
146
+ * Renders `rows x columns` animated bones that approximate the
147
+ * dimensions of the real content. Pair with <Gigbag> so the
148
+ * container ratchets to the larger of placeholder vs real content.
149
+ *
150
+ * All dimensions are CSS custom properties — consuming apps theme
151
+ * without forking.
152
+ */
153
+ declare const Warmup: react.ForwardRefExoticComponent<WarmupProps & react.RefAttributes<HTMLElement>>;
154
+
155
+ interface GlideProps extends HTMLAttributes<HTMLElement> {
156
+ /** Whether the content is visible. */
157
+ show: boolean;
158
+ /** HTML element to render. Default: "div". */
159
+ as?: ElementType;
160
+ }
161
+ /**
162
+ * Enter/exit animation wrapper.
163
+ *
164
+ * State machine:
165
+ * show=true -> mount + "entering" -> animationEnd -> "entered"
166
+ * show=false -> "exiting" -> animationEnd -> unmount
167
+ *
168
+ * CSS classes: concertina-glide-entering, concertina-glide-exiting
169
+ */
170
+ declare const Glide: react.ForwardRefExoticComponent<GlideProps & react.RefAttributes<HTMLElement>>;
171
+
118
172
  /**
119
173
  * Scroll `el` to the top of its nearest scrollable ancestor,
120
174
  * clearing any sticky headers. Only adjusts one container's
@@ -157,4 +211,4 @@ interface UseConcertinaReturn {
157
211
  */
158
212
  declare function useConcertina(): UseConcertinaReturn;
159
213
 
160
- export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Item, Root, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, pinToScrollTop, useConcertina, useExpanded, useStableSlot, useTransitionLock };
214
+ export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Gigbag, type GigbagProps, Glide, type GlideProps, Item, Root, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, Warmup, type WarmupProps, pinToScrollTop, useConcertina, useExpanded, useStableSlot, useTransitionLock };
package/dist/index.d.ts CHANGED
@@ -115,6 +115,60 @@ declare function useTransitionLock(): {
115
115
  readonly lock: () => void;
116
116
  };
117
117
 
118
+ interface GigbagProps extends HTMLAttributes<HTMLElement> {
119
+ /** Which axis to ratchet. Default: "height". */
120
+ axis?: Axis;
121
+ /** HTML element to render. Default: "div". */
122
+ as?: ElementType;
123
+ }
124
+ /**
125
+ * Size-reserving container.
126
+ *
127
+ * Remembers its largest-ever size (ResizeObserver ratchet) and never
128
+ * shrinks. Swap a spinner for a table inside — no reflow.
129
+ *
130
+ * Uses `contain: layout style` to isolate internal reflow from
131
+ * ancestors.
132
+ */
133
+ declare const Gigbag: react.ForwardRefExoticComponent<GigbagProps & react.RefAttributes<HTMLElement>>;
134
+
135
+ interface WarmupProps extends HTMLAttributes<HTMLElement> {
136
+ /** Number of placeholder rows. Default: 3. */
137
+ rows?: number;
138
+ /** Number of columns per row. Default: 1. */
139
+ columns?: number;
140
+ /** HTML element to render. Default: "div". */
141
+ as?: ElementType;
142
+ }
143
+ /**
144
+ * Structural placeholder — CSS-only shimmer grid.
145
+ *
146
+ * Renders `rows x columns` animated bones that approximate the
147
+ * dimensions of the real content. Pair with <Gigbag> so the
148
+ * container ratchets to the larger of placeholder vs real content.
149
+ *
150
+ * All dimensions are CSS custom properties — consuming apps theme
151
+ * without forking.
152
+ */
153
+ declare const Warmup: react.ForwardRefExoticComponent<WarmupProps & react.RefAttributes<HTMLElement>>;
154
+
155
+ interface GlideProps extends HTMLAttributes<HTMLElement> {
156
+ /** Whether the content is visible. */
157
+ show: boolean;
158
+ /** HTML element to render. Default: "div". */
159
+ as?: ElementType;
160
+ }
161
+ /**
162
+ * Enter/exit animation wrapper.
163
+ *
164
+ * State machine:
165
+ * show=true -> mount + "entering" -> animationEnd -> "entered"
166
+ * show=false -> "exiting" -> animationEnd -> unmount
167
+ *
168
+ * CSS classes: concertina-glide-entering, concertina-glide-exiting
169
+ */
170
+ declare const Glide: react.ForwardRefExoticComponent<GlideProps & react.RefAttributes<HTMLElement>>;
171
+
118
172
  /**
119
173
  * Scroll `el` to the top of its nearest scrollable ancestor,
120
174
  * clearing any sticky headers. Only adjusts one container's
@@ -157,4 +211,4 @@ interface UseConcertinaReturn {
157
211
  */
158
212
  declare function useConcertina(): UseConcertinaReturn;
159
213
 
160
- export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Item, Root, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, pinToScrollTop, useConcertina, useExpanded, useStableSlot, useTransitionLock };
214
+ export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Gigbag, type GigbagProps, Glide, type GlideProps, Item, Root, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, Warmup, type WarmupProps, pinToScrollTop, useConcertina, useExpanded, useStableSlot, useTransitionLock };
package/dist/index.js CHANGED
@@ -1222,19 +1222,107 @@ function useStableSlot(options = {}) {
1222
1222
  return { ref, style };
1223
1223
  }
1224
1224
 
1225
- // src/use-concertina.ts
1225
+ // src/gigbag.tsx
1226
+ import { forwardRef as forwardRef9 } from "react";
1227
+ import { jsx as jsx13 } from "react/jsx-runtime";
1228
+ var Gigbag = forwardRef9(
1229
+ function Gigbag2({ axis = "height", as: Tag = "div", className, style, children, ...props }, fwdRef) {
1230
+ const { ref: ratchetRef, style: ratchetStyle } = useStableSlot({ axis });
1231
+ const merged = className ? `concertina-gigbag ${className}` : "concertina-gigbag";
1232
+ return /* @__PURE__ */ jsx13(
1233
+ Tag,
1234
+ {
1235
+ ref: (el) => {
1236
+ ratchetRef(el);
1237
+ if (typeof fwdRef === "function") fwdRef(el);
1238
+ else if (fwdRef) fwdRef.current = el;
1239
+ },
1240
+ className: merged,
1241
+ style: { ...ratchetStyle, ...style },
1242
+ ...props,
1243
+ children
1244
+ }
1245
+ );
1246
+ }
1247
+ );
1248
+
1249
+ // src/warmup.tsx
1250
+ import { forwardRef as forwardRef10 } from "react";
1251
+ import { jsx as jsx14 } from "react/jsx-runtime";
1252
+ var Warmup = forwardRef10(
1253
+ function Warmup2({ rows = 3, columns = 1, as: Tag = "div", className, children, ...props }, ref) {
1254
+ const merged = className ? `concertina-warmup ${className}` : "concertina-warmup";
1255
+ const cells = Array.from({ length: rows * columns }, (_, i) => /* @__PURE__ */ jsx14("div", { className: "concertina-warmup-bone" }, i));
1256
+ return /* @__PURE__ */ jsx14(
1257
+ Tag,
1258
+ {
1259
+ ref,
1260
+ className: merged,
1261
+ style: { gridTemplateColumns: `repeat(${columns}, 1fr)` },
1262
+ ...props,
1263
+ children: cells
1264
+ }
1265
+ );
1266
+ }
1267
+ );
1268
+
1269
+ // src/glide.tsx
1226
1270
  import {
1271
+ forwardRef as forwardRef11,
1227
1272
  useState as useState7,
1228
- useCallback as useCallback9,
1273
+ useEffect as useEffect6,
1274
+ useCallback as useCallback9
1275
+ } from "react";
1276
+ import { jsx as jsx15 } from "react/jsx-runtime";
1277
+ var Glide = forwardRef11(
1278
+ function Glide2({ show, as: Tag = "div", className, children, ...props }, ref) {
1279
+ const [mounted, setMounted] = useState7(show);
1280
+ const [phase, setPhase] = useState7(show ? "entered" : "exiting");
1281
+ useEffect6(() => {
1282
+ if (show) {
1283
+ setMounted(true);
1284
+ setPhase("entering");
1285
+ } else if (mounted) {
1286
+ setPhase("exiting");
1287
+ }
1288
+ }, [show]);
1289
+ const onAnimationEnd = useCallback9(
1290
+ (e) => {
1291
+ if (e.target !== e.currentTarget) return;
1292
+ if (phase === "entering") setPhase("entered");
1293
+ if (phase === "exiting") setMounted(false);
1294
+ },
1295
+ [phase]
1296
+ );
1297
+ if (!mounted) return null;
1298
+ const phaseClass = phase === "entering" ? "concertina-glide-entering" : phase === "exiting" ? "concertina-glide-exiting" : "";
1299
+ const merged = ["concertina-glide", phaseClass, className].filter(Boolean).join(" ");
1300
+ return /* @__PURE__ */ jsx15(
1301
+ Tag,
1302
+ {
1303
+ ref,
1304
+ className: merged,
1305
+ onAnimationEnd,
1306
+ ...props,
1307
+ children
1308
+ }
1309
+ );
1310
+ }
1311
+ );
1312
+
1313
+ // src/use-concertina.ts
1314
+ import {
1315
+ useState as useState8,
1316
+ useCallback as useCallback10,
1229
1317
  useRef as useRef7,
1230
1318
  useLayoutEffect as useLayoutEffect4,
1231
- useEffect as useEffect6
1319
+ useEffect as useEffect7
1232
1320
  } from "react";
1233
1321
  function useConcertina() {
1234
- const [value, setValue] = useState7("");
1235
- const [switching, setSwitching] = useState7(false);
1322
+ const [value, setValue] = useState8("");
1323
+ const [switching, setSwitching] = useState8(false);
1236
1324
  const itemRefs = useRef7({});
1237
- const onValueChange = useCallback9(
1325
+ const onValueChange = useCallback10(
1238
1326
  (newValue) => {
1239
1327
  if (!newValue) {
1240
1328
  setSwitching(false);
@@ -1250,10 +1338,10 @@ function useConcertina() {
1250
1338
  if (!value) return;
1251
1339
  pinToScrollTop(itemRefs.current[value]);
1252
1340
  }, [value]);
1253
- useEffect6(() => {
1341
+ useEffect7(() => {
1254
1342
  if (switching) setSwitching(false);
1255
1343
  }, [switching]);
1256
- const getItemRef = useCallback9(
1344
+ const getItemRef = useCallback10(
1257
1345
  (id) => (el) => {
1258
1346
  itemRefs.current[id] = el;
1259
1347
  },
@@ -1270,12 +1358,15 @@ export {
1270
1358
  ConcertinaContext,
1271
1359
  ConcertinaStore,
1272
1360
  Content3 as Content,
1361
+ Gigbag,
1362
+ Glide,
1273
1363
  Header,
1274
1364
  Item2 as Item,
1275
1365
  Root3 as Root,
1276
1366
  Slot,
1277
1367
  StableSlot,
1278
1368
  Trigger2 as Trigger,
1369
+ Warmup,
1279
1370
  pinToScrollTop,
1280
1371
  useConcertina,
1281
1372
  useExpanded,
package/dist/styles.css CHANGED
@@ -65,3 +65,51 @@
65
65
  display: flex;
66
66
  flex-direction: column;
67
67
  }
68
+
69
+ /* Gigbag — size-reserving container.
70
+ contain isolates internal reflow from ancestors. */
71
+ .concertina-gigbag {
72
+ contain: layout style;
73
+ }
74
+
75
+ /* Warmup — structural placeholder shimmer grid. */
76
+ .concertina-warmup {
77
+ display: grid;
78
+ gap: var(--concertina-warmup-gap, 0.75rem);
79
+ contain: layout style;
80
+ }
81
+
82
+ .concertina-warmup-bone {
83
+ height: var(--concertina-warmup-bone-height, 1rem);
84
+ border-radius: var(--concertina-warmup-bone-radius, 0.25rem);
85
+ background: var(--concertina-warmup-bone-color, #e5e7eb);
86
+ animation: concertina-shimmer 1.5s ease-in-out infinite;
87
+ }
88
+
89
+ @keyframes concertina-shimmer {
90
+ 0%, 100% { opacity: 1; }
91
+ 50% { opacity: 0.4; }
92
+ }
93
+
94
+ /* Glide — enter/exit animation wrapper. */
95
+ .concertina-glide {
96
+ --concertina-glide-duration: 200ms;
97
+ }
98
+
99
+ .concertina-glide-entering {
100
+ animation: concertina-glide-in var(--concertina-glide-duration) ease-out;
101
+ }
102
+
103
+ .concertina-glide-exiting {
104
+ animation: concertina-glide-out var(--concertina-glide-duration) ease-out forwards;
105
+ }
106
+
107
+ @keyframes concertina-glide-in {
108
+ from { opacity: 0; max-height: 0; overflow: hidden; }
109
+ to { opacity: 1; max-height: var(--concertina-glide-height, 1000px); overflow: hidden; }
110
+ }
111
+
112
+ @keyframes concertina-glide-out {
113
+ from { opacity: 1; max-height: var(--concertina-glide-height, 1000px); overflow: hidden; }
114
+ to { opacity: 0; max-height: 0; overflow: hidden; }
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concertina",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "React toolkit for layout stability.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",