concertina 0.4.1 → 0.5.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 +150 -50
- package/dist/index.d.cts +90 -18
- package/dist/index.d.ts +90 -18
- package/dist/index.js +140 -39
- package/dist/styles.css +9 -0
- package/package.json +6 -3
package/dist/index.cjs
CHANGED
|
@@ -36,15 +36,19 @@ __export(index_exports, {
|
|
|
36
36
|
Header: () => Header,
|
|
37
37
|
Item: () => Item2,
|
|
38
38
|
Root: () => Root3,
|
|
39
|
+
Slot: () => Slot,
|
|
40
|
+
StableSlot: () => StableSlot,
|
|
39
41
|
Trigger: () => Trigger2,
|
|
40
42
|
pinToScrollTop: () => pinToScrollTop,
|
|
41
43
|
useConcertina: () => useConcertina,
|
|
42
|
-
useExpanded: () => useExpanded
|
|
44
|
+
useExpanded: () => useExpanded,
|
|
45
|
+
useStableSlot: () => useStableSlot,
|
|
46
|
+
useTransitionLock: () => useTransitionLock
|
|
43
47
|
});
|
|
44
48
|
module.exports = __toCommonJS(index_exports);
|
|
45
49
|
|
|
46
50
|
// src/root.tsx
|
|
47
|
-
var
|
|
51
|
+
var import_react6 = require("react");
|
|
48
52
|
|
|
49
53
|
// node_modules/@radix-ui/react-accordion/dist/index.mjs
|
|
50
54
|
var import_react3 = __toESM(require("react"), 1);
|
|
@@ -156,7 +160,7 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
|
156
160
|
// @__NO_SIDE_EFFECTS__
|
|
157
161
|
function createSlot(ownerName) {
|
|
158
162
|
const SlotClone = /* @__PURE__ */ createSlotClone(ownerName);
|
|
159
|
-
const
|
|
163
|
+
const Slot22 = React3.forwardRef((props, forwardedRef) => {
|
|
160
164
|
const { children, ...slotProps } = props;
|
|
161
165
|
const childrenArray = React3.Children.toArray(children);
|
|
162
166
|
const slottable = childrenArray.find(isSlottable);
|
|
@@ -174,8 +178,8 @@ function createSlot(ownerName) {
|
|
|
174
178
|
}
|
|
175
179
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SlotClone, { ...slotProps, ref: forwardedRef, children });
|
|
176
180
|
});
|
|
177
|
-
|
|
178
|
-
return
|
|
181
|
+
Slot22.displayName = `${ownerName}.Slot`;
|
|
182
|
+
return Slot22;
|
|
179
183
|
}
|
|
180
184
|
// @__NO_SIDE_EFFECTS__
|
|
181
185
|
function createSlotClone(ownerName) {
|
|
@@ -413,10 +417,10 @@ var NODES = [
|
|
|
413
417
|
"ul"
|
|
414
418
|
];
|
|
415
419
|
var Primitive = NODES.reduce((primitive, node) => {
|
|
416
|
-
const
|
|
420
|
+
const Slot3 = createSlot(`Primitive.${node}`);
|
|
417
421
|
const Node2 = React7.forwardRef((props, forwardedRef) => {
|
|
418
422
|
const { asChild, ...primitiveProps } = props;
|
|
419
|
-
const Comp = asChild ?
|
|
423
|
+
const Comp = asChild ? Slot3 : node;
|
|
420
424
|
if (typeof window !== "undefined") {
|
|
421
425
|
window[/* @__PURE__ */ Symbol.for("radix-ui")] = true;
|
|
422
426
|
}
|
|
@@ -1010,7 +1014,6 @@ var import_react4 = require("react");
|
|
|
1010
1014
|
var ConcertinaStore = class {
|
|
1011
1015
|
constructor() {
|
|
1012
1016
|
this._value = "";
|
|
1013
|
-
this._switching = false;
|
|
1014
1017
|
this._itemRefs = {};
|
|
1015
1018
|
this._listeners = /* @__PURE__ */ new Set();
|
|
1016
1019
|
this.subscribe = (listener) => {
|
|
@@ -1018,22 +1021,14 @@ var ConcertinaStore = class {
|
|
|
1018
1021
|
return () => this._listeners.delete(listener);
|
|
1019
1022
|
};
|
|
1020
1023
|
this.getValue = () => this._value;
|
|
1021
|
-
this.getSwitching = () => this._switching;
|
|
1022
1024
|
}
|
|
1023
1025
|
_notify() {
|
|
1024
1026
|
for (const listener of this._listeners) listener();
|
|
1025
1027
|
}
|
|
1026
1028
|
setValue(newValue) {
|
|
1027
|
-
const wasSwitching = !!this._value && this._value !== newValue && !!newValue;
|
|
1028
|
-
this._switching = wasSwitching;
|
|
1029
1029
|
this._value = newValue || "";
|
|
1030
1030
|
this._notify();
|
|
1031
1031
|
}
|
|
1032
|
-
clearSwitching() {
|
|
1033
|
-
if (!this._switching) return;
|
|
1034
|
-
this._switching = false;
|
|
1035
|
-
this._notify();
|
|
1036
|
-
}
|
|
1037
1032
|
getItemRef(id) {
|
|
1038
1033
|
return this._itemRefs[id] ?? null;
|
|
1039
1034
|
}
|
|
@@ -1073,36 +1068,44 @@ function pinToScrollTop(el) {
|
|
|
1073
1068
|
}
|
|
1074
1069
|
}
|
|
1075
1070
|
|
|
1071
|
+
// src/use-transition-lock.ts
|
|
1072
|
+
var import_react5 = require("react");
|
|
1073
|
+
function useTransitionLock() {
|
|
1074
|
+
const [locked, setLocked] = (0, import_react5.useState)(false);
|
|
1075
|
+
const lock = (0, import_react5.useCallback)(() => setLocked(true), []);
|
|
1076
|
+
(0, import_react5.useEffect)(() => {
|
|
1077
|
+
if (locked) setLocked(false);
|
|
1078
|
+
}, [locked]);
|
|
1079
|
+
return { locked, lock };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1076
1082
|
// src/root.tsx
|
|
1077
1083
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1078
|
-
var Root3 = (0,
|
|
1084
|
+
var Root3 = (0, import_react6.forwardRef)(
|
|
1079
1085
|
function Root4({ collapsible = true, children, ...props }, forwardedRef) {
|
|
1080
|
-
const storeRef = (0,
|
|
1086
|
+
const storeRef = (0, import_react6.useRef)(null);
|
|
1081
1087
|
if (!storeRef.current) {
|
|
1082
1088
|
storeRef.current = new ConcertinaStore();
|
|
1083
1089
|
}
|
|
1084
1090
|
const store = storeRef.current;
|
|
1085
|
-
const value = (0,
|
|
1091
|
+
const value = (0, import_react6.useSyncExternalStore)(
|
|
1086
1092
|
store.subscribe,
|
|
1087
1093
|
store.getValue,
|
|
1088
1094
|
store.getValue
|
|
1089
1095
|
);
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
[store]
|
|
1096
|
+
const { locked, lock } = useTransitionLock();
|
|
1097
|
+
const onValueChange = (0, import_react6.useCallback)(
|
|
1098
|
+
(newValue) => {
|
|
1099
|
+
const isSwitching = !!store.getValue() && store.getValue() !== newValue && !!newValue;
|
|
1100
|
+
if (isSwitching) lock();
|
|
1101
|
+
store.setValue(newValue);
|
|
1102
|
+
},
|
|
1103
|
+
[store, lock]
|
|
1098
1104
|
);
|
|
1099
|
-
(0,
|
|
1105
|
+
(0, import_react6.useLayoutEffect)(() => {
|
|
1100
1106
|
if (!value) return;
|
|
1101
1107
|
pinToScrollTop(store.getItemRef(value));
|
|
1102
1108
|
}, [value, store]);
|
|
1103
|
-
(0, import_react5.useEffect)(() => {
|
|
1104
|
-
if (switching) store.clearSwitching();
|
|
1105
|
-
}, [switching, store]);
|
|
1106
1109
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ConcertinaContext.Provider, { value: store, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1107
1110
|
Root2,
|
|
1108
1111
|
{
|
|
@@ -1111,7 +1114,7 @@ var Root3 = (0, import_react5.forwardRef)(
|
|
|
1111
1114
|
collapsible,
|
|
1112
1115
|
value,
|
|
1113
1116
|
onValueChange,
|
|
1114
|
-
"data-switching":
|
|
1117
|
+
"data-switching": locked || void 0,
|
|
1115
1118
|
...props,
|
|
1116
1119
|
children
|
|
1117
1120
|
}
|
|
@@ -1120,11 +1123,11 @@ var Root3 = (0, import_react5.forwardRef)(
|
|
|
1120
1123
|
);
|
|
1121
1124
|
|
|
1122
1125
|
// src/item.tsx
|
|
1123
|
-
var
|
|
1126
|
+
var import_react7 = require("react");
|
|
1124
1127
|
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1125
|
-
var Item2 = (0,
|
|
1126
|
-
const store = (0,
|
|
1127
|
-
const mergedRef = (0,
|
|
1128
|
+
var Item2 = (0, import_react7.forwardRef)(function Item3({ value, ...props }, forwardedRef) {
|
|
1129
|
+
const store = (0, import_react7.useContext)(ConcertinaContext);
|
|
1130
|
+
const mergedRef = (0, import_react7.useCallback)(
|
|
1128
1131
|
(el) => {
|
|
1129
1132
|
if (typeof forwardedRef === "function") {
|
|
1130
1133
|
forwardedRef(el);
|
|
@@ -1139,17 +1142,17 @@ var Item2 = (0, import_react6.forwardRef)(function Item3({ value, ...props }, fo
|
|
|
1139
1142
|
});
|
|
1140
1143
|
|
|
1141
1144
|
// src/content.tsx
|
|
1142
|
-
var
|
|
1145
|
+
var import_react8 = require("react");
|
|
1143
1146
|
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
1144
|
-
var Content3 = (0,
|
|
1147
|
+
var Content3 = (0, import_react8.forwardRef)(function Content4({ className, ...props }, ref) {
|
|
1145
1148
|
const merged = className ? `concertina-content ${className}` : "concertina-content";
|
|
1146
1149
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Content2, { ref, className: merged, ...props });
|
|
1147
1150
|
});
|
|
1148
1151
|
|
|
1149
1152
|
// src/use-expanded.ts
|
|
1150
|
-
var
|
|
1153
|
+
var import_react9 = require("react");
|
|
1151
1154
|
function useStore() {
|
|
1152
|
-
const store = (0,
|
|
1155
|
+
const store = (0, import_react9.useContext)(ConcertinaContext);
|
|
1153
1156
|
if (!store) {
|
|
1154
1157
|
throw new Error("useExpanded must be used inside <Concertina.Root>");
|
|
1155
1158
|
}
|
|
@@ -1157,7 +1160,7 @@ function useStore() {
|
|
|
1157
1160
|
}
|
|
1158
1161
|
function useExpanded(id) {
|
|
1159
1162
|
const store = useStore();
|
|
1160
|
-
return (0,
|
|
1163
|
+
return (0, import_react9.useSyncExternalStore)(
|
|
1161
1164
|
store.subscribe,
|
|
1162
1165
|
() => store.getValue() === id,
|
|
1163
1166
|
() => false
|
|
@@ -1165,13 +1168,106 @@ function useExpanded(id) {
|
|
|
1165
1168
|
);
|
|
1166
1169
|
}
|
|
1167
1170
|
|
|
1171
|
+
// src/stable-slot.tsx
|
|
1172
|
+
var import_react10 = require("react");
|
|
1173
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
1174
|
+
var AxisContext = (0, import_react10.createContext)("both");
|
|
1175
|
+
var StableSlot = (0, import_react10.forwardRef)(
|
|
1176
|
+
function StableSlot2({ axis = "both", className, children, ...props }, ref) {
|
|
1177
|
+
const merged = className ? `concertina-stable-slot ${className}` : "concertina-stable-slot";
|
|
1178
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AxisContext.Provider, { value: axis, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { ref, className: merged, ...props, children }) });
|
|
1179
|
+
}
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
// src/slot.tsx
|
|
1183
|
+
var import_react11 = require("react");
|
|
1184
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
1185
|
+
function inactiveStyle(axis) {
|
|
1186
|
+
const base = { visibility: "hidden", overflow: "hidden" };
|
|
1187
|
+
if (axis === "width") {
|
|
1188
|
+
base.maxHeight = 0;
|
|
1189
|
+
} else if (axis === "height") {
|
|
1190
|
+
base.maxWidth = 0;
|
|
1191
|
+
}
|
|
1192
|
+
return base;
|
|
1193
|
+
}
|
|
1194
|
+
var Slot = (0, import_react11.forwardRef)(
|
|
1195
|
+
function Slot2({ active, style, children, ...props }, ref) {
|
|
1196
|
+
const axis = (0, import_react11.useContext)(AxisContext);
|
|
1197
|
+
const merged = active ? { ...style } : { ...inactiveStyle(axis), ...style };
|
|
1198
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1199
|
+
"div",
|
|
1200
|
+
{
|
|
1201
|
+
ref,
|
|
1202
|
+
inert: !active || void 0,
|
|
1203
|
+
style: merged,
|
|
1204
|
+
...props,
|
|
1205
|
+
children
|
|
1206
|
+
}
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
// src/use-stable-slot.ts
|
|
1212
|
+
var import_react12 = require("react");
|
|
1213
|
+
function useStableSlot(options = {}) {
|
|
1214
|
+
const { axis = "both" } = options;
|
|
1215
|
+
const [style, setStyle] = (0, import_react12.useState)({});
|
|
1216
|
+
const maxRef = (0, import_react12.useRef)({ w: 0, h: 0 });
|
|
1217
|
+
const observerRef = (0, import_react12.useRef)(null);
|
|
1218
|
+
const ref = (0, import_react12.useCallback)(
|
|
1219
|
+
(el) => {
|
|
1220
|
+
if (observerRef.current) {
|
|
1221
|
+
observerRef.current.disconnect();
|
|
1222
|
+
observerRef.current = null;
|
|
1223
|
+
}
|
|
1224
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
1225
|
+
const observer = new ResizeObserver((entries) => {
|
|
1226
|
+
for (const entry of entries) {
|
|
1227
|
+
let w;
|
|
1228
|
+
let h;
|
|
1229
|
+
if (entry.borderBoxSize?.length) {
|
|
1230
|
+
const box = entry.borderBoxSize[0];
|
|
1231
|
+
w = box.inlineSize;
|
|
1232
|
+
h = box.blockSize;
|
|
1233
|
+
} else {
|
|
1234
|
+
const rect = entry.target.getBoundingClientRect();
|
|
1235
|
+
w = rect.width;
|
|
1236
|
+
h = rect.height;
|
|
1237
|
+
}
|
|
1238
|
+
const max = maxRef.current;
|
|
1239
|
+
let grew = false;
|
|
1240
|
+
if ((axis === "width" || axis === "both") && w > max.w) {
|
|
1241
|
+
max.w = w;
|
|
1242
|
+
grew = true;
|
|
1243
|
+
}
|
|
1244
|
+
if ((axis === "height" || axis === "both") && h > max.h) {
|
|
1245
|
+
max.h = h;
|
|
1246
|
+
grew = true;
|
|
1247
|
+
}
|
|
1248
|
+
if (grew) {
|
|
1249
|
+
const next = {};
|
|
1250
|
+
if (axis === "width" || axis === "both") next.minWidth = max.w;
|
|
1251
|
+
if (axis === "height" || axis === "both") next.minHeight = max.h;
|
|
1252
|
+
setStyle(next);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
observer.observe(el, { box: "border-box" });
|
|
1257
|
+
observerRef.current = observer;
|
|
1258
|
+
},
|
|
1259
|
+
[axis]
|
|
1260
|
+
);
|
|
1261
|
+
return { ref, style };
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1168
1264
|
// src/use-concertina.ts
|
|
1169
|
-
var
|
|
1265
|
+
var import_react13 = require("react");
|
|
1170
1266
|
function useConcertina() {
|
|
1171
|
-
const [value, setValue] = (0,
|
|
1172
|
-
const [switching, setSwitching] = (0,
|
|
1173
|
-
const itemRefs = (0,
|
|
1174
|
-
const onValueChange = (0,
|
|
1267
|
+
const [value, setValue] = (0, import_react13.useState)("");
|
|
1268
|
+
const [switching, setSwitching] = (0, import_react13.useState)(false);
|
|
1269
|
+
const itemRefs = (0, import_react13.useRef)({});
|
|
1270
|
+
const onValueChange = (0, import_react13.useCallback)(
|
|
1175
1271
|
(newValue) => {
|
|
1176
1272
|
if (!newValue) {
|
|
1177
1273
|
setSwitching(false);
|
|
@@ -1183,14 +1279,14 @@ function useConcertina() {
|
|
|
1183
1279
|
},
|
|
1184
1280
|
[value]
|
|
1185
1281
|
);
|
|
1186
|
-
(0,
|
|
1282
|
+
(0, import_react13.useLayoutEffect)(() => {
|
|
1187
1283
|
if (!value) return;
|
|
1188
1284
|
pinToScrollTop(itemRefs.current[value]);
|
|
1189
1285
|
}, [value]);
|
|
1190
|
-
(0,
|
|
1286
|
+
(0, import_react13.useEffect)(() => {
|
|
1191
1287
|
if (switching) setSwitching(false);
|
|
1192
1288
|
}, [switching]);
|
|
1193
|
-
const getItemRef = (0,
|
|
1289
|
+
const getItemRef = (0, import_react13.useCallback)(
|
|
1194
1290
|
(id) => (el) => {
|
|
1195
1291
|
itemRefs.current[id] = el;
|
|
1196
1292
|
},
|
|
@@ -1211,8 +1307,12 @@ function useConcertina() {
|
|
|
1211
1307
|
Header,
|
|
1212
1308
|
Item,
|
|
1213
1309
|
Root,
|
|
1310
|
+
Slot,
|
|
1311
|
+
StableSlot,
|
|
1214
1312
|
Trigger,
|
|
1215
1313
|
pinToScrollTop,
|
|
1216
1314
|
useConcertina,
|
|
1217
|
-
useExpanded
|
|
1315
|
+
useExpanded,
|
|
1316
|
+
useStableSlot,
|
|
1317
|
+
useTransitionLock
|
|
1218
1318
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, CSSProperties } from 'react';
|
|
2
3
|
import * as Accordion from '@radix-ui/react-accordion';
|
|
3
4
|
export { Header, Trigger } from '@radix-ui/react-accordion';
|
|
4
5
|
|
|
@@ -23,24 +24,108 @@ type Listener = () => void;
|
|
|
23
24
|
/**
|
|
24
25
|
* External store for concertina accordion state.
|
|
25
26
|
* Lives outside React — one instance per Root.
|
|
26
|
-
*
|
|
27
|
+
* Holds value + item refs. Switching logic moved to useTransitionLock.
|
|
27
28
|
*/
|
|
28
29
|
declare class ConcertinaStore {
|
|
29
30
|
private _value;
|
|
30
|
-
private _switching;
|
|
31
31
|
private _itemRefs;
|
|
32
32
|
private _listeners;
|
|
33
33
|
subscribe: (listener: Listener) => (() => void);
|
|
34
34
|
private _notify;
|
|
35
35
|
getValue: () => string;
|
|
36
|
-
getSwitching: () => boolean;
|
|
37
36
|
setValue(newValue: string): void;
|
|
38
|
-
clearSwitching(): void;
|
|
39
37
|
getItemRef(id: string): HTMLElement | null;
|
|
40
38
|
setItemRef(id: string, el: HTMLElement | null): void;
|
|
41
39
|
}
|
|
42
40
|
declare const ConcertinaContext: react.Context<ConcertinaStore | null>;
|
|
43
41
|
|
|
42
|
+
type Axis = "width" | "height" | "both";
|
|
43
|
+
interface StableSlotProps extends HTMLAttributes<HTMLDivElement> {
|
|
44
|
+
/** Which axis to stabilize. Default: "both". */
|
|
45
|
+
axis?: Axis;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Grid container that auto-sizes to the largest child.
|
|
49
|
+
* All children overlap in the same grid cell (1/1).
|
|
50
|
+
* Use <Slot active={bool}> as children.
|
|
51
|
+
*
|
|
52
|
+
* Zero JS measurement — pure CSS grid sizing.
|
|
53
|
+
*/
|
|
54
|
+
declare const StableSlot: react.ForwardRefExoticComponent<StableSlotProps & react.RefAttributes<HTMLDivElement>>;
|
|
55
|
+
|
|
56
|
+
interface SlotProps extends HTMLAttributes<HTMLDivElement> {
|
|
57
|
+
/** Whether this slot is the active (visible) variant. */
|
|
58
|
+
active: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A single variant inside a <StableSlot>.
|
|
62
|
+
* All slots overlap via CSS grid. Inactive slots are hidden
|
|
63
|
+
* but still contribute to grid cell sizing.
|
|
64
|
+
*
|
|
65
|
+
* Five things work together:
|
|
66
|
+
* 1. grid-area: 1/1 — all slots overlap in the same cell
|
|
67
|
+
* 2. visibility: hidden — invisible but in layout flow
|
|
68
|
+
* 3. inert — no focus, no clicks, no screen reader
|
|
69
|
+
* 4. max-height/max-width: 0 — axis-aware collapse
|
|
70
|
+
* 5. overflow: hidden — prevents content bleed from collapsed axis
|
|
71
|
+
*/
|
|
72
|
+
declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<HTMLDivElement>>;
|
|
73
|
+
|
|
74
|
+
interface UseStableSlotOptions {
|
|
75
|
+
/** Which axis to ratchet. Default: "both". */
|
|
76
|
+
axis?: Axis;
|
|
77
|
+
}
|
|
78
|
+
interface UseStableSlotReturn {
|
|
79
|
+
/** RefCallback — attach to the container element. */
|
|
80
|
+
ref: (el: HTMLElement | null) => void;
|
|
81
|
+
/** Spread onto the element: { minWidth?, minHeight? } */
|
|
82
|
+
style: CSSProperties;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* ResizeObserver ratchet for dynamic content.
|
|
86
|
+
*
|
|
87
|
+
* Watches the element, tracks maximum width/height ever observed,
|
|
88
|
+
* applies min-width/min-height that only ratchets up.
|
|
89
|
+
*
|
|
90
|
+
* Five things work together:
|
|
91
|
+
* 1. ResizeObserver uses borderBoxSize — includes padding/border
|
|
92
|
+
* 2. Ratchet is one-way — max only increases, never resets
|
|
93
|
+
* 3. setStyle only called when ratchet grows — no infinite loops
|
|
94
|
+
* 4. RefCallback disconnects observer on unmount — no leak
|
|
95
|
+
* 5. SSR graceful no-op — typeof ResizeObserver guard
|
|
96
|
+
*/
|
|
97
|
+
declare function useStableSlot(options?: UseStableSlotOptions): UseStableSlotReturn;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Suppress CSS transitions during batched state changes.
|
|
101
|
+
*
|
|
102
|
+
* Three things work together:
|
|
103
|
+
* 1. lock() sets the flag synchronously — batched with state changes in React 18
|
|
104
|
+
* 2. After DOM commit (useLayoutEffect window) — consumer does measurement/scroll/pin work
|
|
105
|
+
* 3. useEffect auto-clears the flag after paint — transitions re-enable
|
|
106
|
+
*
|
|
107
|
+
* Usage:
|
|
108
|
+
* const { locked, lock } = useTransitionLock();
|
|
109
|
+
* <div data-locked={locked || undefined}>...</div>
|
|
110
|
+
*/
|
|
111
|
+
declare function useTransitionLock(): {
|
|
112
|
+
readonly locked: boolean;
|
|
113
|
+
readonly lock: () => void;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Scroll `el` to the top of its nearest scrollable ancestor,
|
|
118
|
+
* clearing any sticky headers. Only adjusts one container's
|
|
119
|
+
* scrollTop. Never cascades to the viewport, which matters on
|
|
120
|
+
* mobile where scrollIntoView pulls the whole page.
|
|
121
|
+
*
|
|
122
|
+
* Skips elements that have overflow: auto/scroll in CSS but
|
|
123
|
+
* don't actually scroll (scrollHeight <= clientHeight). Without
|
|
124
|
+
* this check, a non-scrolling ancestor with overflow-auto traps
|
|
125
|
+
* the walk and the real scroll container never gets adjusted.
|
|
126
|
+
*/
|
|
127
|
+
declare function pinToScrollTop(el: HTMLElement | null): void;
|
|
128
|
+
|
|
44
129
|
interface ConcertinaRootProps {
|
|
45
130
|
value: string;
|
|
46
131
|
onValueChange: (value: string) => void;
|
|
@@ -70,17 +155,4 @@ interface UseConcertinaReturn {
|
|
|
70
155
|
*/
|
|
71
156
|
declare function useConcertina(): UseConcertinaReturn;
|
|
72
157
|
|
|
73
|
-
|
|
74
|
-
* Scroll `el` to the top of its nearest scrollable ancestor,
|
|
75
|
-
* clearing any sticky headers. Only adjusts one container's
|
|
76
|
-
* scrollTop. Never cascades to the viewport, which matters on
|
|
77
|
-
* mobile where scrollIntoView pulls the whole page.
|
|
78
|
-
*
|
|
79
|
-
* Skips elements that have overflow: auto/scroll in CSS but
|
|
80
|
-
* don't actually scroll (scrollHeight <= clientHeight). Without
|
|
81
|
-
* this check, a non-scrolling ancestor with overflow-auto traps
|
|
82
|
-
* the walk and the real scroll container never gets adjusted.
|
|
83
|
-
*/
|
|
84
|
-
declare function pinToScrollTop(el: HTMLElement | null): void;
|
|
85
|
-
|
|
86
|
-
export { ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Item, Root, type UseConcertinaReturn, pinToScrollTop, useConcertina, useExpanded };
|
|
158
|
+
export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Item, Root, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, pinToScrollTop, useConcertina, useExpanded, useStableSlot, useTransitionLock };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, CSSProperties } from 'react';
|
|
2
3
|
import * as Accordion from '@radix-ui/react-accordion';
|
|
3
4
|
export { Header, Trigger } from '@radix-ui/react-accordion';
|
|
4
5
|
|
|
@@ -23,24 +24,108 @@ type Listener = () => void;
|
|
|
23
24
|
/**
|
|
24
25
|
* External store for concertina accordion state.
|
|
25
26
|
* Lives outside React — one instance per Root.
|
|
26
|
-
*
|
|
27
|
+
* Holds value + item refs. Switching logic moved to useTransitionLock.
|
|
27
28
|
*/
|
|
28
29
|
declare class ConcertinaStore {
|
|
29
30
|
private _value;
|
|
30
|
-
private _switching;
|
|
31
31
|
private _itemRefs;
|
|
32
32
|
private _listeners;
|
|
33
33
|
subscribe: (listener: Listener) => (() => void);
|
|
34
34
|
private _notify;
|
|
35
35
|
getValue: () => string;
|
|
36
|
-
getSwitching: () => boolean;
|
|
37
36
|
setValue(newValue: string): void;
|
|
38
|
-
clearSwitching(): void;
|
|
39
37
|
getItemRef(id: string): HTMLElement | null;
|
|
40
38
|
setItemRef(id: string, el: HTMLElement | null): void;
|
|
41
39
|
}
|
|
42
40
|
declare const ConcertinaContext: react.Context<ConcertinaStore | null>;
|
|
43
41
|
|
|
42
|
+
type Axis = "width" | "height" | "both";
|
|
43
|
+
interface StableSlotProps extends HTMLAttributes<HTMLDivElement> {
|
|
44
|
+
/** Which axis to stabilize. Default: "both". */
|
|
45
|
+
axis?: Axis;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Grid container that auto-sizes to the largest child.
|
|
49
|
+
* All children overlap in the same grid cell (1/1).
|
|
50
|
+
* Use <Slot active={bool}> as children.
|
|
51
|
+
*
|
|
52
|
+
* Zero JS measurement — pure CSS grid sizing.
|
|
53
|
+
*/
|
|
54
|
+
declare const StableSlot: react.ForwardRefExoticComponent<StableSlotProps & react.RefAttributes<HTMLDivElement>>;
|
|
55
|
+
|
|
56
|
+
interface SlotProps extends HTMLAttributes<HTMLDivElement> {
|
|
57
|
+
/** Whether this slot is the active (visible) variant. */
|
|
58
|
+
active: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A single variant inside a <StableSlot>.
|
|
62
|
+
* All slots overlap via CSS grid. Inactive slots are hidden
|
|
63
|
+
* but still contribute to grid cell sizing.
|
|
64
|
+
*
|
|
65
|
+
* Five things work together:
|
|
66
|
+
* 1. grid-area: 1/1 — all slots overlap in the same cell
|
|
67
|
+
* 2. visibility: hidden — invisible but in layout flow
|
|
68
|
+
* 3. inert — no focus, no clicks, no screen reader
|
|
69
|
+
* 4. max-height/max-width: 0 — axis-aware collapse
|
|
70
|
+
* 5. overflow: hidden — prevents content bleed from collapsed axis
|
|
71
|
+
*/
|
|
72
|
+
declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<HTMLDivElement>>;
|
|
73
|
+
|
|
74
|
+
interface UseStableSlotOptions {
|
|
75
|
+
/** Which axis to ratchet. Default: "both". */
|
|
76
|
+
axis?: Axis;
|
|
77
|
+
}
|
|
78
|
+
interface UseStableSlotReturn {
|
|
79
|
+
/** RefCallback — attach to the container element. */
|
|
80
|
+
ref: (el: HTMLElement | null) => void;
|
|
81
|
+
/** Spread onto the element: { minWidth?, minHeight? } */
|
|
82
|
+
style: CSSProperties;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* ResizeObserver ratchet for dynamic content.
|
|
86
|
+
*
|
|
87
|
+
* Watches the element, tracks maximum width/height ever observed,
|
|
88
|
+
* applies min-width/min-height that only ratchets up.
|
|
89
|
+
*
|
|
90
|
+
* Five things work together:
|
|
91
|
+
* 1. ResizeObserver uses borderBoxSize — includes padding/border
|
|
92
|
+
* 2. Ratchet is one-way — max only increases, never resets
|
|
93
|
+
* 3. setStyle only called when ratchet grows — no infinite loops
|
|
94
|
+
* 4. RefCallback disconnects observer on unmount — no leak
|
|
95
|
+
* 5. SSR graceful no-op — typeof ResizeObserver guard
|
|
96
|
+
*/
|
|
97
|
+
declare function useStableSlot(options?: UseStableSlotOptions): UseStableSlotReturn;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Suppress CSS transitions during batched state changes.
|
|
101
|
+
*
|
|
102
|
+
* Three things work together:
|
|
103
|
+
* 1. lock() sets the flag synchronously — batched with state changes in React 18
|
|
104
|
+
* 2. After DOM commit (useLayoutEffect window) — consumer does measurement/scroll/pin work
|
|
105
|
+
* 3. useEffect auto-clears the flag after paint — transitions re-enable
|
|
106
|
+
*
|
|
107
|
+
* Usage:
|
|
108
|
+
* const { locked, lock } = useTransitionLock();
|
|
109
|
+
* <div data-locked={locked || undefined}>...</div>
|
|
110
|
+
*/
|
|
111
|
+
declare function useTransitionLock(): {
|
|
112
|
+
readonly locked: boolean;
|
|
113
|
+
readonly lock: () => void;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Scroll `el` to the top of its nearest scrollable ancestor,
|
|
118
|
+
* clearing any sticky headers. Only adjusts one container's
|
|
119
|
+
* scrollTop. Never cascades to the viewport, which matters on
|
|
120
|
+
* mobile where scrollIntoView pulls the whole page.
|
|
121
|
+
*
|
|
122
|
+
* Skips elements that have overflow: auto/scroll in CSS but
|
|
123
|
+
* don't actually scroll (scrollHeight <= clientHeight). Without
|
|
124
|
+
* this check, a non-scrolling ancestor with overflow-auto traps
|
|
125
|
+
* the walk and the real scroll container never gets adjusted.
|
|
126
|
+
*/
|
|
127
|
+
declare function pinToScrollTop(el: HTMLElement | null): void;
|
|
128
|
+
|
|
44
129
|
interface ConcertinaRootProps {
|
|
45
130
|
value: string;
|
|
46
131
|
onValueChange: (value: string) => void;
|
|
@@ -70,17 +155,4 @@ interface UseConcertinaReturn {
|
|
|
70
155
|
*/
|
|
71
156
|
declare function useConcertina(): UseConcertinaReturn;
|
|
72
157
|
|
|
73
|
-
|
|
74
|
-
* Scroll `el` to the top of its nearest scrollable ancestor,
|
|
75
|
-
* clearing any sticky headers. Only adjusts one container's
|
|
76
|
-
* scrollTop. Never cascades to the viewport, which matters on
|
|
77
|
-
* mobile where scrollIntoView pulls the whole page.
|
|
78
|
-
*
|
|
79
|
-
* Skips elements that have overflow: auto/scroll in CSS but
|
|
80
|
-
* don't actually scroll (scrollHeight <= clientHeight). Without
|
|
81
|
-
* this check, a non-scrolling ancestor with overflow-auto traps
|
|
82
|
-
* the walk and the real scroll container never gets adjusted.
|
|
83
|
-
*/
|
|
84
|
-
declare function pinToScrollTop(el: HTMLElement | null): void;
|
|
85
|
-
|
|
86
|
-
export { ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Item, Root, type UseConcertinaReturn, pinToScrollTop, useConcertina, useExpanded };
|
|
158
|
+
export { type Axis, ConcertinaContext, type ConcertinaRootProps, ConcertinaStore, Content, Item, Root, Slot, type SlotProps, StableSlot, type StableSlotProps, type UseConcertinaReturn, pinToScrollTop, useConcertina, useExpanded, useStableSlot, useTransitionLock };
|
package/dist/index.js
CHANGED
|
@@ -3,9 +3,8 @@ import {
|
|
|
3
3
|
forwardRef as forwardRef4,
|
|
4
4
|
useRef as useRef5,
|
|
5
5
|
useLayoutEffect as useLayoutEffect3,
|
|
6
|
-
useEffect as useEffect5,
|
|
7
6
|
useSyncExternalStore,
|
|
8
|
-
useCallback as
|
|
7
|
+
useCallback as useCallback6
|
|
9
8
|
} from "react";
|
|
10
9
|
|
|
11
10
|
// node_modules/@radix-ui/react-accordion/dist/index.mjs
|
|
@@ -118,7 +117,7 @@ import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
|
|
|
118
117
|
// @__NO_SIDE_EFFECTS__
|
|
119
118
|
function createSlot(ownerName) {
|
|
120
119
|
const SlotClone = /* @__PURE__ */ createSlotClone(ownerName);
|
|
121
|
-
const
|
|
120
|
+
const Slot22 = React3.forwardRef((props, forwardedRef) => {
|
|
122
121
|
const { children, ...slotProps } = props;
|
|
123
122
|
const childrenArray = React3.Children.toArray(children);
|
|
124
123
|
const slottable = childrenArray.find(isSlottable);
|
|
@@ -136,8 +135,8 @@ function createSlot(ownerName) {
|
|
|
136
135
|
}
|
|
137
136
|
return /* @__PURE__ */ jsx2(SlotClone, { ...slotProps, ref: forwardedRef, children });
|
|
138
137
|
});
|
|
139
|
-
|
|
140
|
-
return
|
|
138
|
+
Slot22.displayName = `${ownerName}.Slot`;
|
|
139
|
+
return Slot22;
|
|
141
140
|
}
|
|
142
141
|
// @__NO_SIDE_EFFECTS__
|
|
143
142
|
function createSlotClone(ownerName) {
|
|
@@ -375,10 +374,10 @@ var NODES = [
|
|
|
375
374
|
"ul"
|
|
376
375
|
];
|
|
377
376
|
var Primitive = NODES.reduce((primitive, node) => {
|
|
378
|
-
const
|
|
377
|
+
const Slot3 = createSlot(`Primitive.${node}`);
|
|
379
378
|
const Node2 = React7.forwardRef((props, forwardedRef) => {
|
|
380
379
|
const { asChild, ...primitiveProps } = props;
|
|
381
|
-
const Comp = asChild ?
|
|
380
|
+
const Comp = asChild ? Slot3 : node;
|
|
382
381
|
if (typeof window !== "undefined") {
|
|
383
382
|
window[/* @__PURE__ */ Symbol.for("radix-ui")] = true;
|
|
384
383
|
}
|
|
@@ -972,7 +971,6 @@ import { createContext as createContext3 } from "react";
|
|
|
972
971
|
var ConcertinaStore = class {
|
|
973
972
|
constructor() {
|
|
974
973
|
this._value = "";
|
|
975
|
-
this._switching = false;
|
|
976
974
|
this._itemRefs = {};
|
|
977
975
|
this._listeners = /* @__PURE__ */ new Set();
|
|
978
976
|
this.subscribe = (listener) => {
|
|
@@ -980,22 +978,14 @@ var ConcertinaStore = class {
|
|
|
980
978
|
return () => this._listeners.delete(listener);
|
|
981
979
|
};
|
|
982
980
|
this.getValue = () => this._value;
|
|
983
|
-
this.getSwitching = () => this._switching;
|
|
984
981
|
}
|
|
985
982
|
_notify() {
|
|
986
983
|
for (const listener of this._listeners) listener();
|
|
987
984
|
}
|
|
988
985
|
setValue(newValue) {
|
|
989
|
-
const wasSwitching = !!this._value && this._value !== newValue && !!newValue;
|
|
990
|
-
this._switching = wasSwitching;
|
|
991
986
|
this._value = newValue || "";
|
|
992
987
|
this._notify();
|
|
993
988
|
}
|
|
994
|
-
clearSwitching() {
|
|
995
|
-
if (!this._switching) return;
|
|
996
|
-
this._switching = false;
|
|
997
|
-
this._notify();
|
|
998
|
-
}
|
|
999
989
|
getItemRef(id) {
|
|
1000
990
|
return this._itemRefs[id] ?? null;
|
|
1001
991
|
}
|
|
@@ -1035,6 +1025,17 @@ function pinToScrollTop(el) {
|
|
|
1035
1025
|
}
|
|
1036
1026
|
}
|
|
1037
1027
|
|
|
1028
|
+
// src/use-transition-lock.ts
|
|
1029
|
+
import { useState as useState5, useEffect as useEffect5, useCallback as useCallback5 } from "react";
|
|
1030
|
+
function useTransitionLock() {
|
|
1031
|
+
const [locked, setLocked] = useState5(false);
|
|
1032
|
+
const lock = useCallback5(() => setLocked(true), []);
|
|
1033
|
+
useEffect5(() => {
|
|
1034
|
+
if (locked) setLocked(false);
|
|
1035
|
+
}, [locked]);
|
|
1036
|
+
return { locked, lock };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1038
1039
|
// src/root.tsx
|
|
1039
1040
|
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
1040
1041
|
var Root3 = forwardRef4(
|
|
@@ -1049,22 +1050,19 @@ var Root3 = forwardRef4(
|
|
|
1049
1050
|
store.getValue,
|
|
1050
1051
|
store.getValue
|
|
1051
1052
|
);
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
[store]
|
|
1053
|
+
const { locked, lock } = useTransitionLock();
|
|
1054
|
+
const onValueChange = useCallback6(
|
|
1055
|
+
(newValue) => {
|
|
1056
|
+
const isSwitching = !!store.getValue() && store.getValue() !== newValue && !!newValue;
|
|
1057
|
+
if (isSwitching) lock();
|
|
1058
|
+
store.setValue(newValue);
|
|
1059
|
+
},
|
|
1060
|
+
[store, lock]
|
|
1060
1061
|
);
|
|
1061
1062
|
useLayoutEffect3(() => {
|
|
1062
1063
|
if (!value) return;
|
|
1063
1064
|
pinToScrollTop(store.getItemRef(value));
|
|
1064
1065
|
}, [value, store]);
|
|
1065
|
-
useEffect5(() => {
|
|
1066
|
-
if (switching) store.clearSwitching();
|
|
1067
|
-
}, [switching, store]);
|
|
1068
1066
|
return /* @__PURE__ */ jsx8(ConcertinaContext.Provider, { value: store, children: /* @__PURE__ */ jsx8(
|
|
1069
1067
|
Root2,
|
|
1070
1068
|
{
|
|
@@ -1073,7 +1071,7 @@ var Root3 = forwardRef4(
|
|
|
1073
1071
|
collapsible,
|
|
1074
1072
|
value,
|
|
1075
1073
|
onValueChange,
|
|
1076
|
-
"data-switching":
|
|
1074
|
+
"data-switching": locked || void 0,
|
|
1077
1075
|
...props,
|
|
1078
1076
|
children
|
|
1079
1077
|
}
|
|
@@ -1085,12 +1083,12 @@ var Root3 = forwardRef4(
|
|
|
1085
1083
|
import {
|
|
1086
1084
|
forwardRef as forwardRef5,
|
|
1087
1085
|
useContext as useContext3,
|
|
1088
|
-
useCallback as
|
|
1086
|
+
useCallback as useCallback7
|
|
1089
1087
|
} from "react";
|
|
1090
1088
|
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
1091
1089
|
var Item2 = forwardRef5(function Item3({ value, ...props }, forwardedRef) {
|
|
1092
1090
|
const store = useContext3(ConcertinaContext);
|
|
1093
|
-
const mergedRef =
|
|
1091
|
+
const mergedRef = useCallback7(
|
|
1094
1092
|
(el) => {
|
|
1095
1093
|
if (typeof forwardedRef === "function") {
|
|
1096
1094
|
forwardedRef(el);
|
|
@@ -1131,19 +1129,118 @@ function useExpanded(id) {
|
|
|
1131
1129
|
);
|
|
1132
1130
|
}
|
|
1133
1131
|
|
|
1132
|
+
// src/stable-slot.tsx
|
|
1133
|
+
import {
|
|
1134
|
+
forwardRef as forwardRef7,
|
|
1135
|
+
createContext as createContext4
|
|
1136
|
+
} from "react";
|
|
1137
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
1138
|
+
var AxisContext = createContext4("both");
|
|
1139
|
+
var StableSlot = forwardRef7(
|
|
1140
|
+
function StableSlot2({ axis = "both", className, children, ...props }, ref) {
|
|
1141
|
+
const merged = className ? `concertina-stable-slot ${className}` : "concertina-stable-slot";
|
|
1142
|
+
return /* @__PURE__ */ jsx11(AxisContext.Provider, { value: axis, children: /* @__PURE__ */ jsx11("div", { ref, className: merged, ...props, children }) });
|
|
1143
|
+
}
|
|
1144
|
+
);
|
|
1145
|
+
|
|
1146
|
+
// src/slot.tsx
|
|
1147
|
+
import {
|
|
1148
|
+
forwardRef as forwardRef8,
|
|
1149
|
+
useContext as useContext5
|
|
1150
|
+
} from "react";
|
|
1151
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
1152
|
+
function inactiveStyle(axis) {
|
|
1153
|
+
const base = { visibility: "hidden", overflow: "hidden" };
|
|
1154
|
+
if (axis === "width") {
|
|
1155
|
+
base.maxHeight = 0;
|
|
1156
|
+
} else if (axis === "height") {
|
|
1157
|
+
base.maxWidth = 0;
|
|
1158
|
+
}
|
|
1159
|
+
return base;
|
|
1160
|
+
}
|
|
1161
|
+
var Slot = forwardRef8(
|
|
1162
|
+
function Slot2({ active, style, children, ...props }, ref) {
|
|
1163
|
+
const axis = useContext5(AxisContext);
|
|
1164
|
+
const merged = active ? { ...style } : { ...inactiveStyle(axis), ...style };
|
|
1165
|
+
return /* @__PURE__ */ jsx12(
|
|
1166
|
+
"div",
|
|
1167
|
+
{
|
|
1168
|
+
ref,
|
|
1169
|
+
inert: !active || void 0,
|
|
1170
|
+
style: merged,
|
|
1171
|
+
...props,
|
|
1172
|
+
children
|
|
1173
|
+
}
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
// src/use-stable-slot.ts
|
|
1179
|
+
import { useState as useState6, useCallback as useCallback8, useRef as useRef6 } from "react";
|
|
1180
|
+
function useStableSlot(options = {}) {
|
|
1181
|
+
const { axis = "both" } = options;
|
|
1182
|
+
const [style, setStyle] = useState6({});
|
|
1183
|
+
const maxRef = useRef6({ w: 0, h: 0 });
|
|
1184
|
+
const observerRef = useRef6(null);
|
|
1185
|
+
const ref = useCallback8(
|
|
1186
|
+
(el) => {
|
|
1187
|
+
if (observerRef.current) {
|
|
1188
|
+
observerRef.current.disconnect();
|
|
1189
|
+
observerRef.current = null;
|
|
1190
|
+
}
|
|
1191
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
1192
|
+
const observer = new ResizeObserver((entries) => {
|
|
1193
|
+
for (const entry of entries) {
|
|
1194
|
+
let w;
|
|
1195
|
+
let h;
|
|
1196
|
+
if (entry.borderBoxSize?.length) {
|
|
1197
|
+
const box = entry.borderBoxSize[0];
|
|
1198
|
+
w = box.inlineSize;
|
|
1199
|
+
h = box.blockSize;
|
|
1200
|
+
} else {
|
|
1201
|
+
const rect = entry.target.getBoundingClientRect();
|
|
1202
|
+
w = rect.width;
|
|
1203
|
+
h = rect.height;
|
|
1204
|
+
}
|
|
1205
|
+
const max = maxRef.current;
|
|
1206
|
+
let grew = false;
|
|
1207
|
+
if ((axis === "width" || axis === "both") && w > max.w) {
|
|
1208
|
+
max.w = w;
|
|
1209
|
+
grew = true;
|
|
1210
|
+
}
|
|
1211
|
+
if ((axis === "height" || axis === "both") && h > max.h) {
|
|
1212
|
+
max.h = h;
|
|
1213
|
+
grew = true;
|
|
1214
|
+
}
|
|
1215
|
+
if (grew) {
|
|
1216
|
+
const next = {};
|
|
1217
|
+
if (axis === "width" || axis === "both") next.minWidth = max.w;
|
|
1218
|
+
if (axis === "height" || axis === "both") next.minHeight = max.h;
|
|
1219
|
+
setStyle(next);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
observer.observe(el, { box: "border-box" });
|
|
1224
|
+
observerRef.current = observer;
|
|
1225
|
+
},
|
|
1226
|
+
[axis]
|
|
1227
|
+
);
|
|
1228
|
+
return { ref, style };
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1134
1231
|
// src/use-concertina.ts
|
|
1135
1232
|
import {
|
|
1136
|
-
useState as
|
|
1137
|
-
useCallback as
|
|
1138
|
-
useRef as
|
|
1233
|
+
useState as useState7,
|
|
1234
|
+
useCallback as useCallback9,
|
|
1235
|
+
useRef as useRef7,
|
|
1139
1236
|
useLayoutEffect as useLayoutEffect4,
|
|
1140
1237
|
useEffect as useEffect6
|
|
1141
1238
|
} from "react";
|
|
1142
1239
|
function useConcertina() {
|
|
1143
|
-
const [value, setValue] =
|
|
1144
|
-
const [switching, setSwitching] =
|
|
1145
|
-
const itemRefs =
|
|
1146
|
-
const onValueChange =
|
|
1240
|
+
const [value, setValue] = useState7("");
|
|
1241
|
+
const [switching, setSwitching] = useState7(false);
|
|
1242
|
+
const itemRefs = useRef7({});
|
|
1243
|
+
const onValueChange = useCallback9(
|
|
1147
1244
|
(newValue) => {
|
|
1148
1245
|
if (!newValue) {
|
|
1149
1246
|
setSwitching(false);
|
|
@@ -1162,7 +1259,7 @@ function useConcertina() {
|
|
|
1162
1259
|
useEffect6(() => {
|
|
1163
1260
|
if (switching) setSwitching(false);
|
|
1164
1261
|
}, [switching]);
|
|
1165
|
-
const getItemRef =
|
|
1262
|
+
const getItemRef = useCallback9(
|
|
1166
1263
|
(id) => (el) => {
|
|
1167
1264
|
itemRefs.current[id] = el;
|
|
1168
1265
|
},
|
|
@@ -1182,8 +1279,12 @@ export {
|
|
|
1182
1279
|
Header,
|
|
1183
1280
|
Item2 as Item,
|
|
1184
1281
|
Root3 as Root,
|
|
1282
|
+
Slot,
|
|
1283
|
+
StableSlot,
|
|
1185
1284
|
Trigger2 as Trigger,
|
|
1186
1285
|
pinToScrollTop,
|
|
1187
1286
|
useConcertina,
|
|
1188
|
-
useExpanded
|
|
1287
|
+
useExpanded,
|
|
1288
|
+
useStableSlot,
|
|
1289
|
+
useTransitionLock
|
|
1189
1290
|
};
|
package/dist/styles.css
CHANGED
|
@@ -52,3 +52,12 @@
|
|
|
52
52
|
[data-switching] .concertina-content[data-state="open"] {
|
|
53
53
|
animation-duration: 0s;
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/* StableSlot — all children overlap in the same grid cell.
|
|
57
|
+
Grid auto-sizes to the largest child. */
|
|
58
|
+
.concertina-stable-slot {
|
|
59
|
+
display: grid;
|
|
60
|
+
}
|
|
61
|
+
.concertina-stable-slot > * {
|
|
62
|
+
grid-area: 1 / 1;
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "concertina",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Layout stability primitives + scroll-pinned Radix Accordion panels with per-item memoization.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -52,7 +52,10 @@
|
|
|
52
52
|
"react",
|
|
53
53
|
"hook",
|
|
54
54
|
"memoization",
|
|
55
|
-
"useSyncExternalStore"
|
|
55
|
+
"useSyncExternalStore",
|
|
56
|
+
"layout-stability",
|
|
57
|
+
"stable-slot",
|
|
58
|
+
"resize-observer"
|
|
56
59
|
],
|
|
57
60
|
"license": "MIT",
|
|
58
61
|
"repository": {
|