concertina 0.5.1 → 0.6.1
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 +44 -7
- package/dist/index.cjs +6 -12
- package/dist/index.d.cts +10 -8
- package/dist/index.d.ts +10 -8
- package/dist/index.js +6 -12
- package/dist/styles.css +5 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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
|
|
@@ -39,10 +39,10 @@ A UI slot toggles between variants of different sizes (Add button ↔ quantity s
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
**How it works:**
|
|
42
|
-
1. `display: grid` on container, `grid-area: 1/1` on all
|
|
43
|
-
2. `visibility: hidden` on inactive
|
|
44
|
-
3. `inert` attribute on inactive
|
|
45
|
-
4.
|
|
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. `display: flex; flex-direction: column` on Slots — content stretches to fill the reserved width
|
|
46
46
|
5. Zero JS measurement — pure CSS, works on first frame
|
|
47
47
|
|
|
48
48
|
**StableSlot props:**
|
|
@@ -50,15 +50,52 @@ A UI slot toggles between variants of different sizes (Add button ↔ quantity s
|
|
|
50
50
|
| Prop | Type | Default | Description |
|
|
51
51
|
|------|------|---------|-------------|
|
|
52
52
|
| `axis` | `"width"` \| `"height"` \| `"both"` | `"both"` | Which axis to stabilize |
|
|
53
|
-
| `
|
|
53
|
+
| `as` | `ElementType` | `"div"` | HTML element to render. Use `"span"` inside buttons. |
|
|
54
|
+
| `className` | `string` | — | Passed to wrapper element |
|
|
54
55
|
|
|
55
|
-
All other
|
|
56
|
+
All other HTML attributes are forwarded.
|
|
56
57
|
|
|
57
58
|
**Slot props:**
|
|
58
59
|
|
|
59
60
|
| Prop | Type | Description |
|
|
60
61
|
|------|------|-------------|
|
|
61
62
|
| `active` | `boolean` | Controls visibility |
|
|
63
|
+
| `as` | `ElementType` | HTML element to render. Default `"div"`. |
|
|
64
|
+
|
|
65
|
+
#### Rules for correct behavior
|
|
66
|
+
|
|
67
|
+
**1. Parent containers must allow content-based sizing.**
|
|
68
|
+
A fixed-width parent (e.g., `grid-template-columns: 10rem`) clips the StableSlot and defeats the mechanism. If a grid column contains a StableSlot, use `auto`:
|
|
69
|
+
|
|
70
|
+
```css
|
|
71
|
+
/* Bad — fixed column ignores StableSlot's intrinsic width */
|
|
72
|
+
grid-template-columns: 1fr 10rem;
|
|
73
|
+
|
|
74
|
+
/* Good — column auto-sizes to the StableSlot's widest child */
|
|
75
|
+
grid-template-columns: 1fr auto;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**2. Every independently appearing element needs its own StableSlot.**
|
|
79
|
+
If an element appears in one state but not another (e.g., an Undo link below a Charge button), it must be in a separate StableSlot — not nested inside one Slot of the main StableSlot. Stack StableSlots vertically:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<div className="action-column">
|
|
83
|
+
{/* Main action — morphs between Deliver/Charge/Retry/paid */}
|
|
84
|
+
<Concertina.StableSlot axis="width">
|
|
85
|
+
<Concertina.Slot active={showDeliver}><Button>Deliver</Button></Concertina.Slot>
|
|
86
|
+
<Concertina.Slot active={showCharge}><Button>Charge</Button></Concertina.Slot>
|
|
87
|
+
<Concertina.Slot active={showRetry}><Button>Retry</Button></Concertina.Slot>
|
|
88
|
+
</Concertina.StableSlot>
|
|
89
|
+
{/* Undo — appears only in Charge state, but space is always reserved */}
|
|
90
|
+
<Concertina.StableSlot>
|
|
91
|
+
<Concertina.Slot active={showCharge}>
|
|
92
|
+
<button className="undo-link">Undo</button>
|
|
93
|
+
</Concertina.Slot>
|
|
94
|
+
</Concertina.StableSlot>
|
|
95
|
+
</div>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A single Slot inside a StableSlot is valid — it simply reserves the element's space, showing or hiding it without layout shift.
|
|
62
99
|
|
|
63
100
|
### useStableSlot — ResizeObserver ratchet for dynamic content
|
|
64
101
|
|
package/dist/index.cjs
CHANGED
|
@@ -1173,30 +1173,24 @@ var import_react10 = require("react");
|
|
|
1173
1173
|
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
1174
1174
|
var AxisContext = (0, import_react10.createContext)("both");
|
|
1175
1175
|
var StableSlot = (0, import_react10.forwardRef)(
|
|
1176
|
-
function StableSlot2({ axis = "both", className, children, ...props }, ref) {
|
|
1176
|
+
function StableSlot2({ axis = "both", as: Tag = "div", className, style, children, ...props }, ref) {
|
|
1177
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)(
|
|
1178
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AxisContext.Provider, { value: axis, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Tag, { ref, className: merged, style, ...props, children }) });
|
|
1179
1179
|
}
|
|
1180
1180
|
);
|
|
1181
1181
|
|
|
1182
1182
|
// src/slot.tsx
|
|
1183
1183
|
var import_react11 = require("react");
|
|
1184
1184
|
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
1185
|
-
function inactiveStyle(
|
|
1186
|
-
|
|
1187
|
-
if (axis === "width") {
|
|
1188
|
-
base.maxHeight = 0;
|
|
1189
|
-
} else if (axis === "height") {
|
|
1190
|
-
base.maxWidth = 0;
|
|
1191
|
-
}
|
|
1192
|
-
return base;
|
|
1185
|
+
function inactiveStyle(_axis) {
|
|
1186
|
+
return { visibility: "hidden" };
|
|
1193
1187
|
}
|
|
1194
1188
|
var Slot = (0, import_react11.forwardRef)(
|
|
1195
|
-
function Slot2({ active, style, children, ...props }, ref) {
|
|
1189
|
+
function Slot2({ active, as: Tag = "div", style, children, ...props }, ref) {
|
|
1196
1190
|
const axis = (0, import_react11.useContext)(AxisContext);
|
|
1197
1191
|
const merged = active ? { ...style } : { ...inactiveStyle(axis), ...style };
|
|
1198
1192
|
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1199
|
-
|
|
1193
|
+
Tag,
|
|
1200
1194
|
{
|
|
1201
1195
|
ref,
|
|
1202
1196
|
inert: !active || void 0,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { HTMLAttributes, CSSProperties } from 'react';
|
|
2
|
+
import { HTMLAttributes, ElementType, CSSProperties } from 'react';
|
|
3
3
|
import * as Accordion from '@radix-ui/react-accordion';
|
|
4
4
|
export { Header, Trigger } from '@radix-ui/react-accordion';
|
|
5
5
|
|
|
@@ -40,9 +40,11 @@ declare class ConcertinaStore {
|
|
|
40
40
|
declare const ConcertinaContext: react.Context<ConcertinaStore | null>;
|
|
41
41
|
|
|
42
42
|
type Axis = "width" | "height" | "both";
|
|
43
|
-
interface StableSlotProps extends HTMLAttributes<
|
|
43
|
+
interface StableSlotProps extends HTMLAttributes<HTMLElement> {
|
|
44
44
|
/** Which axis to stabilize. Default: "both". */
|
|
45
45
|
axis?: Axis;
|
|
46
|
+
/** HTML element to render. Use "span" inside buttons. Default: "div". */
|
|
47
|
+
as?: ElementType;
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
50
|
* Grid container that auto-sizes to the largest child.
|
|
@@ -51,25 +53,25 @@ interface StableSlotProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
51
53
|
*
|
|
52
54
|
* Zero JS measurement — pure CSS grid sizing.
|
|
53
55
|
*/
|
|
54
|
-
declare const StableSlot: react.ForwardRefExoticComponent<StableSlotProps & react.RefAttributes<
|
|
56
|
+
declare const StableSlot: react.ForwardRefExoticComponent<StableSlotProps & react.RefAttributes<HTMLElement>>;
|
|
55
57
|
|
|
56
|
-
interface SlotProps extends HTMLAttributes<
|
|
58
|
+
interface SlotProps extends HTMLAttributes<HTMLElement> {
|
|
57
59
|
/** Whether this slot is the active (visible) variant. */
|
|
58
60
|
active: boolean;
|
|
61
|
+
/** HTML element to render. Use "span" inside buttons. Default: "div". */
|
|
62
|
+
as?: ElementType;
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
61
65
|
* A single variant inside a <StableSlot>.
|
|
62
66
|
* All slots overlap via CSS grid. Inactive slots are hidden
|
|
63
67
|
* but still contribute to grid cell sizing.
|
|
64
68
|
*
|
|
65
|
-
*
|
|
69
|
+
* Three things work together:
|
|
66
70
|
* 1. grid-area: 1/1 — all slots overlap in the same cell
|
|
67
71
|
* 2. visibility: hidden — invisible but in layout flow
|
|
68
72
|
* 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
73
|
*/
|
|
72
|
-
declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<
|
|
74
|
+
declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<HTMLElement>>;
|
|
73
75
|
|
|
74
76
|
interface UseStableSlotOptions {
|
|
75
77
|
/** Which axis to ratchet. Default: "both". */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { HTMLAttributes, CSSProperties } from 'react';
|
|
2
|
+
import { HTMLAttributes, ElementType, CSSProperties } from 'react';
|
|
3
3
|
import * as Accordion from '@radix-ui/react-accordion';
|
|
4
4
|
export { Header, Trigger } from '@radix-ui/react-accordion';
|
|
5
5
|
|
|
@@ -40,9 +40,11 @@ declare class ConcertinaStore {
|
|
|
40
40
|
declare const ConcertinaContext: react.Context<ConcertinaStore | null>;
|
|
41
41
|
|
|
42
42
|
type Axis = "width" | "height" | "both";
|
|
43
|
-
interface StableSlotProps extends HTMLAttributes<
|
|
43
|
+
interface StableSlotProps extends HTMLAttributes<HTMLElement> {
|
|
44
44
|
/** Which axis to stabilize. Default: "both". */
|
|
45
45
|
axis?: Axis;
|
|
46
|
+
/** HTML element to render. Use "span" inside buttons. Default: "div". */
|
|
47
|
+
as?: ElementType;
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
50
|
* Grid container that auto-sizes to the largest child.
|
|
@@ -51,25 +53,25 @@ interface StableSlotProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
51
53
|
*
|
|
52
54
|
* Zero JS measurement — pure CSS grid sizing.
|
|
53
55
|
*/
|
|
54
|
-
declare const StableSlot: react.ForwardRefExoticComponent<StableSlotProps & react.RefAttributes<
|
|
56
|
+
declare const StableSlot: react.ForwardRefExoticComponent<StableSlotProps & react.RefAttributes<HTMLElement>>;
|
|
55
57
|
|
|
56
|
-
interface SlotProps extends HTMLAttributes<
|
|
58
|
+
interface SlotProps extends HTMLAttributes<HTMLElement> {
|
|
57
59
|
/** Whether this slot is the active (visible) variant. */
|
|
58
60
|
active: boolean;
|
|
61
|
+
/** HTML element to render. Use "span" inside buttons. Default: "div". */
|
|
62
|
+
as?: ElementType;
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
61
65
|
* A single variant inside a <StableSlot>.
|
|
62
66
|
* All slots overlap via CSS grid. Inactive slots are hidden
|
|
63
67
|
* but still contribute to grid cell sizing.
|
|
64
68
|
*
|
|
65
|
-
*
|
|
69
|
+
* Three things work together:
|
|
66
70
|
* 1. grid-area: 1/1 — all slots overlap in the same cell
|
|
67
71
|
* 2. visibility: hidden — invisible but in layout flow
|
|
68
72
|
* 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
73
|
*/
|
|
72
|
-
declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<
|
|
74
|
+
declare const Slot: react.ForwardRefExoticComponent<SlotProps & react.RefAttributes<HTMLElement>>;
|
|
73
75
|
|
|
74
76
|
interface UseStableSlotOptions {
|
|
75
77
|
/** Which axis to ratchet. Default: "both". */
|
package/dist/index.js
CHANGED
|
@@ -1137,9 +1137,9 @@ import {
|
|
|
1137
1137
|
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
1138
1138
|
var AxisContext = createContext4("both");
|
|
1139
1139
|
var StableSlot = forwardRef7(
|
|
1140
|
-
function StableSlot2({ axis = "both", className, children, ...props }, ref) {
|
|
1140
|
+
function StableSlot2({ axis = "both", as: Tag = "div", className, style, children, ...props }, ref) {
|
|
1141
1141
|
const merged = className ? `concertina-stable-slot ${className}` : "concertina-stable-slot";
|
|
1142
|
-
return /* @__PURE__ */ jsx11(AxisContext.Provider, { value: axis, children: /* @__PURE__ */ jsx11(
|
|
1142
|
+
return /* @__PURE__ */ jsx11(AxisContext.Provider, { value: axis, children: /* @__PURE__ */ jsx11(Tag, { ref, className: merged, style, ...props, children }) });
|
|
1143
1143
|
}
|
|
1144
1144
|
);
|
|
1145
1145
|
|
|
@@ -1149,21 +1149,15 @@ import {
|
|
|
1149
1149
|
useContext as useContext5
|
|
1150
1150
|
} from "react";
|
|
1151
1151
|
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
1152
|
-
function inactiveStyle(
|
|
1153
|
-
|
|
1154
|
-
if (axis === "width") {
|
|
1155
|
-
base.maxHeight = 0;
|
|
1156
|
-
} else if (axis === "height") {
|
|
1157
|
-
base.maxWidth = 0;
|
|
1158
|
-
}
|
|
1159
|
-
return base;
|
|
1152
|
+
function inactiveStyle(_axis) {
|
|
1153
|
+
return { visibility: "hidden" };
|
|
1160
1154
|
}
|
|
1161
1155
|
var Slot = forwardRef8(
|
|
1162
|
-
function Slot2({ active, style, children, ...props }, ref) {
|
|
1156
|
+
function Slot2({ active, as: Tag = "div", style, children, ...props }, ref) {
|
|
1163
1157
|
const axis = useContext5(AxisContext);
|
|
1164
1158
|
const merged = active ? { ...style } : { ...inactiveStyle(axis), ...style };
|
|
1165
1159
|
return /* @__PURE__ */ jsx12(
|
|
1166
|
-
|
|
1160
|
+
Tag,
|
|
1167
1161
|
{
|
|
1168
1162
|
ref,
|
|
1169
1163
|
inert: !active || void 0,
|
package/dist/styles.css
CHANGED
|
@@ -54,10 +54,14 @@
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/* StableSlot — all children overlap in the same grid cell.
|
|
57
|
-
Grid auto-sizes to the largest child.
|
|
57
|
+
Grid auto-sizes to the largest child.
|
|
58
|
+
Slots use flex-column so their content stretches to fill
|
|
59
|
+
the reserved width — the visual footprint is constant. */
|
|
58
60
|
.concertina-stable-slot {
|
|
59
61
|
display: grid;
|
|
60
62
|
}
|
|
61
63
|
.concertina-stable-slot > * {
|
|
62
64
|
grid-area: 1 / 1;
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
63
67
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "concertina",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "React toolkit for layout stability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsup && cp src/styles.css dist/styles.css",
|
|
30
|
+
"prepare": "npm run build",
|
|
30
31
|
"typecheck": "tsc --noEmit",
|
|
31
32
|
"prepublishOnly": "npm run build"
|
|
32
33
|
},
|