concertina 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,67 +5,159 @@
|
|
|
5
5
|
<h1 align="center">concertina</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
8
|
+
React toolkit for layout stability.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Install
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
npm install concertina
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
```tsx
|
|
18
|
-
import
|
|
18
|
+
import * as Concertina from "concertina";
|
|
19
19
|
import "concertina/styles.css";
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Layer 1: Primitives
|
|
25
|
+
|
|
26
|
+
### StableSlot + Slot — Zero-shift variant switching
|
|
27
|
+
|
|
28
|
+
A UI slot toggles between variants of different sizes (Add button ↔ quantity stepper). Surrounding content reflows. The fix: render **all variants simultaneously** in the same CSS grid cell. The cell auto-sizes to the largest child. Only the active variant is visible.
|
|
23
29
|
|
|
24
30
|
```tsx
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
<Concertina.StableSlot axis="width" className="action-slot">
|
|
32
|
+
<Concertina.Slot active={!isInCart}>
|
|
33
|
+
<AddButton />
|
|
34
|
+
</Concertina.Slot>
|
|
35
|
+
<Concertina.Slot active={isInCart}>
|
|
36
|
+
<QuantityControl />
|
|
37
|
+
</Concertina.Slot>
|
|
38
|
+
</Concertina.StableSlot>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**How it works:**
|
|
42
|
+
1. `display: grid` on container, `grid-area: 1/1` on all slots — everything overlaps
|
|
43
|
+
2. `visibility: hidden` on inactive slots — invisible but still in layout flow
|
|
44
|
+
3. `inert` attribute on inactive slots — no focus, no clicks, no screen reader
|
|
45
|
+
4. Axis-aware collapse (`max-height: 0` or `max-width: 0`) so only the relevant axis contributes to sizing
|
|
46
|
+
5. Zero JS measurement — pure CSS, works on first frame
|
|
47
|
+
|
|
48
|
+
**StableSlot props:**
|
|
49
|
+
|
|
50
|
+
| Prop | Type | Default | Description |
|
|
51
|
+
|------|------|---------|-------------|
|
|
52
|
+
| `axis` | `"width"` \| `"height"` \| `"both"` | `"both"` | Which axis to stabilize |
|
|
53
|
+
| `className` | `string` | — | Passed to wrapper div |
|
|
54
|
+
|
|
55
|
+
All other div attributes are forwarded.
|
|
56
|
+
|
|
57
|
+
**Slot props:**
|
|
58
|
+
|
|
59
|
+
| Prop | Type | Description |
|
|
60
|
+
|------|------|-------------|
|
|
61
|
+
| `active` | `boolean` | Controls visibility |
|
|
62
|
+
|
|
63
|
+
### useStableSlot — ResizeObserver ratchet for dynamic content
|
|
64
|
+
|
|
65
|
+
For content that changes size unpredictably (prices, names, status messages) where you can't enumerate all variants upfront. Watches the container, tracks the maximum size ever observed, applies min-width/min-height that only ratchets up.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
const slot = Concertina.useStableSlot({ axis: "width" });
|
|
69
|
+
|
|
70
|
+
<div ref={slot.ref} style={slot.style} className="price-amount">
|
|
71
|
+
{formattedPrice}
|
|
72
|
+
</div>
|
|
43
73
|
```
|
|
44
74
|
|
|
45
|
-
|
|
75
|
+
| Option | Type | Default | Description |
|
|
76
|
+
|--------|------|---------|-------------|
|
|
77
|
+
| `axis` | `"width"` \| `"height"` \| `"both"` | `"both"` | Which axis to ratchet |
|
|
78
|
+
|
|
79
|
+
Returns `{ ref, style }` — attach both to the container element.
|
|
46
80
|
|
|
47
|
-
|
|
81
|
+
### useTransitionLock — Animation suppression
|
|
48
82
|
|
|
49
|
-
|
|
83
|
+
Suppress CSS transitions during batched state changes. Sets a flag synchronously (batched with state updates in React 18), auto-clears after paint.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
const { locked, lock } = Concertina.useTransitionLock();
|
|
87
|
+
|
|
88
|
+
const handleChange = (newValue) => {
|
|
89
|
+
lock();
|
|
90
|
+
setValue(newValue);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
<div data-locked={locked || undefined}>
|
|
94
|
+
{/* CSS: [data-locked] .animated { transition-duration: 0s } */}
|
|
95
|
+
</div>
|
|
96
|
+
```
|
|
50
97
|
|
|
51
|
-
|
|
98
|
+
### pinToScrollTop(el)
|
|
52
99
|
|
|
53
|
-
|
|
100
|
+
Scrolls `el` to the top of its nearest scrollable ancestor. Adjusts `scrollTop` only — never cascades to the viewport (critical on mobile where `scrollIntoView` yanks the whole page). Accounts for sticky headers automatically.
|
|
54
101
|
|
|
55
|
-
###
|
|
102
|
+
### When to use which
|
|
56
103
|
|
|
57
|
-
|
|
|
104
|
+
| Content type | Tool | Shift behavior |
|
|
105
|
+
|-------------|------|----------------|
|
|
106
|
+
| Discrete variants (button A ↔ button B) | `StableSlot` + `Slot` | Zero — ever |
|
|
107
|
+
| Dynamic text (prices, names, messages) | `useStableSlot` | Once per new max, then stable |
|
|
108
|
+
| Numeric text specifically | CSS `tabular-nums` | Zero (font-level) |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Layer 2: Accordion
|
|
113
|
+
|
|
114
|
+
Wraps Radix Accordion with scroll pinning, animation suppression during switches, and per-item memoization via `useSyncExternalStore`.
|
|
115
|
+
|
|
116
|
+
### Component API
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<Concertina.Root className="my-accordion">
|
|
120
|
+
{items.map((item) => (
|
|
121
|
+
<Concertina.Item key={item.id} value={item.id}>
|
|
122
|
+
<Concertina.Header>
|
|
123
|
+
<Concertina.Trigger>{item.title}</Concertina.Trigger>
|
|
124
|
+
</Concertina.Header>
|
|
125
|
+
<Concertina.Content>{item.body}</Concertina.Content>
|
|
126
|
+
</Concertina.Item>
|
|
127
|
+
))}
|
|
128
|
+
</Concertina.Root>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
When you switch between items, the new one pins to the top of the scroll container. Animations are suppressed during the switch and restored after paint.
|
|
132
|
+
|
|
133
|
+
**`useExpanded(id)`** — per-item expansion hook. Only re-renders when this item's boolean flips:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
function MyItem({ item }) {
|
|
137
|
+
const expanded = Concertina.useExpanded(item.id);
|
|
138
|
+
// only re-renders when this specific item opens/closes
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Hook API (legacy)
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
const { rootProps, getItemRef } = Concertina.useConcertina();
|
|
146
|
+
|
|
147
|
+
<Accordion.Root type="single" collapsible {...rootProps}>
|
|
148
|
+
<Accordion.Item value="a" ref={getItemRef("a")}>
|
|
149
|
+
...
|
|
150
|
+
</Accordion.Item>
|
|
151
|
+
</Accordion.Root>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
| Property | Type | Description |
|
|
58
155
|
|---|---|---|
|
|
59
|
-
| `rootProps` | `object` |
|
|
60
|
-
| `getItemRef` | `(id: string) => RefCallback` |
|
|
156
|
+
| `rootProps` | `object` | Spread onto `Accordion.Root` — contains `value`, `onValueChange`, `data-switching` |
|
|
157
|
+
| `getItemRef` | `(id: string) => RefCallback` | Attach to each `Accordion.Item` |
|
|
61
158
|
| `value` | `string` | Currently expanded item (empty string when collapsed) |
|
|
62
|
-
| `onValueChange` | `(value: string) => void` | Change handler, available if you need it directly |
|
|
63
159
|
| `switching` | `boolean` | True during a switch between items |
|
|
64
160
|
|
|
65
|
-
### `pinToScrollTop(el)`
|
|
66
|
-
|
|
67
|
-
Also exported standalone. Scrolls `el` to the top of its nearest scrollable ancestor without touching the viewport.
|
|
68
|
-
|
|
69
161
|
## Customize animation timing
|
|
70
162
|
|
|
71
163
|
```css
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "concertina",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "React toolkit for layout stability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|