concertina 0.5.2 → 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 CHANGED
@@ -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 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
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
- | `className` | `string` | | Passed to wrapper div |
53
+ | `as` | `ElementType` | `"div"` | HTML element to render. Use `"span"` inside buttons. |
54
+ | `className` | `string` | — | Passed to wrapper element |
54
55
 
55
- All other div attributes are forwarded.
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)("div", { ref, className: merged, ...props, children }) });
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(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;
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
- "div",
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<HTMLDivElement> {
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<HTMLDivElement>>;
56
+ declare const StableSlot: react.ForwardRefExoticComponent<StableSlotProps & react.RefAttributes<HTMLElement>>;
55
57
 
56
- interface SlotProps extends HTMLAttributes<HTMLDivElement> {
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
- * Five things work together:
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<HTMLDivElement>>;
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<HTMLDivElement> {
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<HTMLDivElement>>;
56
+ declare const StableSlot: react.ForwardRefExoticComponent<StableSlotProps & react.RefAttributes<HTMLElement>>;
55
57
 
56
- interface SlotProps extends HTMLAttributes<HTMLDivElement> {
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
- * Five things work together:
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<HTMLDivElement>>;
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("div", { ref, className: merged, ...props, children }) });
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(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;
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
- "div",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "concertina",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "React toolkit for layout stability.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -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
  },