concertina 0.1.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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/index.cjs +88 -0
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +66 -0
- package/dist/styles.css +54 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Ward
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/ryandward/concertina/main/concertina.svg" width="140" alt="concertina" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">concertina</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
React hook for scroll-pinned <a href="https://www.radix-ui.com/primitives/docs/components/accordion">Radix Accordion</a> panels. Zero runtime dependencies.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
## The problem
|
|
12
|
+
|
|
13
|
+
Radix Accordion in a scrollable container breaks in several ways at once. Switching between items plays both a close and open animation, so the scroll position jumps. Calling `scrollIntoView` to fix it cascades to the viewport on mobile and yanks the whole page. Using `flushSync` with inline styles to work around that fights Radix re-renders. Layout measurement happens before animations finish, so scroll targets are wrong. And suppressing animations for the switch has to be temporary or you lose them entirely.
|
|
14
|
+
|
|
15
|
+
These five issues interact. `concertina` solves them together.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install concertina
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { useConcertina } from "concertina";
|
|
27
|
+
import "concertina/styles.css";
|
|
28
|
+
import * as Accordion from "@radix-ui/react-accordion";
|
|
29
|
+
|
|
30
|
+
function MyAccordion({ items }) {
|
|
31
|
+
const { rootProps, getItemRef } = useConcertina();
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Accordion.Root type="single" collapsible {...rootProps}>
|
|
35
|
+
{items.map((item) => (
|
|
36
|
+
<Accordion.Item
|
|
37
|
+
key={item.id}
|
|
38
|
+
value={item.id}
|
|
39
|
+
ref={getItemRef(item.id)}
|
|
40
|
+
>
|
|
41
|
+
<Accordion.Header>
|
|
42
|
+
<Accordion.Trigger>{item.title}</Accordion.Trigger>
|
|
43
|
+
</Accordion.Header>
|
|
44
|
+
<Accordion.Content className="concertina-content">
|
|
45
|
+
{item.content}
|
|
46
|
+
</Accordion.Content>
|
|
47
|
+
</Accordion.Item>
|
|
48
|
+
))}
|
|
49
|
+
</Accordion.Root>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## How it works
|
|
55
|
+
|
|
56
|
+
The hook tracks which item is open via `value`/`onValueChange`. When switching from one item to another, it sets a `data-switching` attribute on the root. CSS rules keyed to that attribute skip the close/open animations so layout settles in one frame. A `useLayoutEffect` then adjusts the scroll container's `scrollTop` to pin the new item to the top (never `scrollIntoView`, which cascades). After paint, a `useEffect` clears the flag so the next interaction animates normally.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
### `useConcertina()`
|
|
61
|
+
|
|
62
|
+
| Property | Type | Description |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `value` | `string` | Currently expanded item, empty string when collapsed |
|
|
65
|
+
| `onValueChange` | `(value: string) => void` | Change handler with switching logic built in |
|
|
66
|
+
| `switching` | `boolean` | `true` during a switch between items |
|
|
67
|
+
| `rootProps` | `object` | Spread onto `Accordion.Root` (includes `value`, `onValueChange`, `data-switching`) |
|
|
68
|
+
| `getItemRef` | `(id: string) => RefCallback` | Ref callback for each `Accordion.Item` |
|
|
69
|
+
|
|
70
|
+
### `pinToScrollTop(el)`
|
|
71
|
+
|
|
72
|
+
Also exported standalone. Scrolls `el` to the top of its nearest `overflow-y: auto|scroll` ancestor without touching the viewport.
|
|
73
|
+
|
|
74
|
+
## Styling
|
|
75
|
+
|
|
76
|
+
Import `concertina/styles.css` for the default expand/collapse animations. Override timing with CSS custom properties on `.concertina-content`:
|
|
77
|
+
|
|
78
|
+
```css
|
|
79
|
+
.concertina-content {
|
|
80
|
+
--concertina-open-duration: 300ms;
|
|
81
|
+
--concertina-close-duration: 200ms;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Put `.concertina-content` on your `Accordion.Content` elements. The `[data-switching]` suppression rules are handled automatically.
|
|
86
|
+
|
|
87
|
+
If items near the bottom of your scroll container can't reach the top, add bottom padding:
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
.my-scroll-container {
|
|
91
|
+
padding-bottom: 50vh;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Requirements
|
|
96
|
+
|
|
97
|
+
- React >= 16.8
|
|
98
|
+
- `@radix-ui/react-accordion`
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
pinToScrollTop: () => pinToScrollTop,
|
|
24
|
+
useConcertina: () => useConcertina
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/use-concertina.ts
|
|
29
|
+
var import_react = require("react");
|
|
30
|
+
|
|
31
|
+
// src/pin-to-scroll-top.ts
|
|
32
|
+
function pinToScrollTop(el) {
|
|
33
|
+
if (!el) return;
|
|
34
|
+
let parent = el.parentElement;
|
|
35
|
+
while (parent) {
|
|
36
|
+
const { overflowY } = getComputedStyle(parent);
|
|
37
|
+
if (overflowY === "auto" || overflowY === "scroll") {
|
|
38
|
+
const box = parent.getBoundingClientRect();
|
|
39
|
+
const target = el.getBoundingClientRect();
|
|
40
|
+
parent.scrollTop += target.top - box.top;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
parent = parent.parentElement;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/use-concertina.ts
|
|
48
|
+
function useConcertina() {
|
|
49
|
+
const [value, setValue] = (0, import_react.useState)("");
|
|
50
|
+
const [switching, setSwitching] = (0, import_react.useState)(false);
|
|
51
|
+
const itemRefs = (0, import_react.useRef)({});
|
|
52
|
+
const onValueChange = (0, import_react.useCallback)(
|
|
53
|
+
(newValue) => {
|
|
54
|
+
if (!newValue) {
|
|
55
|
+
setSwitching(false);
|
|
56
|
+
setValue("");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
setSwitching(!!value && value !== newValue);
|
|
60
|
+
setValue(newValue);
|
|
61
|
+
},
|
|
62
|
+
[value]
|
|
63
|
+
);
|
|
64
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
65
|
+
if (!value) return;
|
|
66
|
+
pinToScrollTop(itemRefs.current[value]);
|
|
67
|
+
}, [value]);
|
|
68
|
+
(0, import_react.useEffect)(() => {
|
|
69
|
+
if (switching) setSwitching(false);
|
|
70
|
+
}, [switching]);
|
|
71
|
+
const getItemRef = (0, import_react.useCallback)(
|
|
72
|
+
(id) => (el) => {
|
|
73
|
+
itemRefs.current[id] = el;
|
|
74
|
+
},
|
|
75
|
+
[]
|
|
76
|
+
);
|
|
77
|
+
const rootProps = {
|
|
78
|
+
value,
|
|
79
|
+
onValueChange,
|
|
80
|
+
...switching ? { "data-switching": true } : {}
|
|
81
|
+
};
|
|
82
|
+
return { value, onValueChange, switching, rootProps, getItemRef };
|
|
83
|
+
}
|
|
84
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
85
|
+
0 && (module.exports = {
|
|
86
|
+
pinToScrollTop,
|
|
87
|
+
useConcertina
|
|
88
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
interface ConcertinaRootProps {
|
|
2
|
+
value: string;
|
|
3
|
+
onValueChange: (value: string) => void;
|
|
4
|
+
"data-switching"?: true;
|
|
5
|
+
}
|
|
6
|
+
interface UseConcertinaReturn {
|
|
7
|
+
/** Currently expanded item value, empty string when collapsed. */
|
|
8
|
+
value: string;
|
|
9
|
+
/** Change handler. Manages switching state automatically. */
|
|
10
|
+
onValueChange: (value: string) => void;
|
|
11
|
+
/** True during a switch between items (animations suppressed). */
|
|
12
|
+
switching: boolean;
|
|
13
|
+
/** Spread onto Accordion.Root. Includes value, onValueChange, data-switching. */
|
|
14
|
+
rootProps: ConcertinaRootProps;
|
|
15
|
+
/** Returns a ref callback for an Accordion.Item. Pass the item's value. */
|
|
16
|
+
getItemRef: (id: string) => (el: HTMLElement | null) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* React hook for scroll-pinned Radix Accordion panels.
|
|
20
|
+
*
|
|
21
|
+
* Handles five things:
|
|
22
|
+
* 1. Suppresses close/open animations when switching between items
|
|
23
|
+
* 2. Pins the newly opened item to the top of the scroll container
|
|
24
|
+
* 3. Uses scrollTop adjustment instead of scrollIntoView (no viewport cascade)
|
|
25
|
+
* 4. Coordinates React state batching so layout is final before scroll measurement
|
|
26
|
+
* 5. Clears the switching flag after paint so future animations work normally
|
|
27
|
+
*/
|
|
28
|
+
declare function useConcertina(): UseConcertinaReturn;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Scroll `el` to the top of its nearest scrollable ancestor.
|
|
32
|
+
*
|
|
33
|
+
* Only adjusts one container's scrollTop. Never cascades to the
|
|
34
|
+
* viewport, which matters on mobile where scrollIntoView pulls
|
|
35
|
+
* the whole page.
|
|
36
|
+
*/
|
|
37
|
+
declare function pinToScrollTop(el: HTMLElement | null): void;
|
|
38
|
+
|
|
39
|
+
export { type ConcertinaRootProps, type UseConcertinaReturn, pinToScrollTop, useConcertina };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
interface ConcertinaRootProps {
|
|
2
|
+
value: string;
|
|
3
|
+
onValueChange: (value: string) => void;
|
|
4
|
+
"data-switching"?: true;
|
|
5
|
+
}
|
|
6
|
+
interface UseConcertinaReturn {
|
|
7
|
+
/** Currently expanded item value, empty string when collapsed. */
|
|
8
|
+
value: string;
|
|
9
|
+
/** Change handler. Manages switching state automatically. */
|
|
10
|
+
onValueChange: (value: string) => void;
|
|
11
|
+
/** True during a switch between items (animations suppressed). */
|
|
12
|
+
switching: boolean;
|
|
13
|
+
/** Spread onto Accordion.Root. Includes value, onValueChange, data-switching. */
|
|
14
|
+
rootProps: ConcertinaRootProps;
|
|
15
|
+
/** Returns a ref callback for an Accordion.Item. Pass the item's value. */
|
|
16
|
+
getItemRef: (id: string) => (el: HTMLElement | null) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* React hook for scroll-pinned Radix Accordion panels.
|
|
20
|
+
*
|
|
21
|
+
* Handles five things:
|
|
22
|
+
* 1. Suppresses close/open animations when switching between items
|
|
23
|
+
* 2. Pins the newly opened item to the top of the scroll container
|
|
24
|
+
* 3. Uses scrollTop adjustment instead of scrollIntoView (no viewport cascade)
|
|
25
|
+
* 4. Coordinates React state batching so layout is final before scroll measurement
|
|
26
|
+
* 5. Clears the switching flag after paint so future animations work normally
|
|
27
|
+
*/
|
|
28
|
+
declare function useConcertina(): UseConcertinaReturn;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Scroll `el` to the top of its nearest scrollable ancestor.
|
|
32
|
+
*
|
|
33
|
+
* Only adjusts one container's scrollTop. Never cascades to the
|
|
34
|
+
* viewport, which matters on mobile where scrollIntoView pulls
|
|
35
|
+
* the whole page.
|
|
36
|
+
*/
|
|
37
|
+
declare function pinToScrollTop(el: HTMLElement | null): void;
|
|
38
|
+
|
|
39
|
+
export { type ConcertinaRootProps, type UseConcertinaReturn, pinToScrollTop, useConcertina };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/use-concertina.ts
|
|
2
|
+
import {
|
|
3
|
+
useState,
|
|
4
|
+
useCallback,
|
|
5
|
+
useRef,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useEffect
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
// src/pin-to-scroll-top.ts
|
|
11
|
+
function pinToScrollTop(el) {
|
|
12
|
+
if (!el) return;
|
|
13
|
+
let parent = el.parentElement;
|
|
14
|
+
while (parent) {
|
|
15
|
+
const { overflowY } = getComputedStyle(parent);
|
|
16
|
+
if (overflowY === "auto" || overflowY === "scroll") {
|
|
17
|
+
const box = parent.getBoundingClientRect();
|
|
18
|
+
const target = el.getBoundingClientRect();
|
|
19
|
+
parent.scrollTop += target.top - box.top;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
parent = parent.parentElement;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/use-concertina.ts
|
|
27
|
+
function useConcertina() {
|
|
28
|
+
const [value, setValue] = useState("");
|
|
29
|
+
const [switching, setSwitching] = useState(false);
|
|
30
|
+
const itemRefs = useRef({});
|
|
31
|
+
const onValueChange = useCallback(
|
|
32
|
+
(newValue) => {
|
|
33
|
+
if (!newValue) {
|
|
34
|
+
setSwitching(false);
|
|
35
|
+
setValue("");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setSwitching(!!value && value !== newValue);
|
|
39
|
+
setValue(newValue);
|
|
40
|
+
},
|
|
41
|
+
[value]
|
|
42
|
+
);
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
if (!value) return;
|
|
45
|
+
pinToScrollTop(itemRefs.current[value]);
|
|
46
|
+
}, [value]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (switching) setSwitching(false);
|
|
49
|
+
}, [switching]);
|
|
50
|
+
const getItemRef = useCallback(
|
|
51
|
+
(id) => (el) => {
|
|
52
|
+
itemRefs.current[id] = el;
|
|
53
|
+
},
|
|
54
|
+
[]
|
|
55
|
+
);
|
|
56
|
+
const rootProps = {
|
|
57
|
+
value,
|
|
58
|
+
onValueChange,
|
|
59
|
+
...switching ? { "data-switching": true } : {}
|
|
60
|
+
};
|
|
61
|
+
return { value, onValueChange, switching, rootProps, getItemRef };
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
pinToScrollTop,
|
|
65
|
+
useConcertina
|
|
66
|
+
};
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* concertina — Radix Accordion expand/collapse with scroll pinning support
|
|
2
|
+
*
|
|
3
|
+
* Add .concertina-content to your Accordion.Content elements.
|
|
4
|
+
* The [data-switching] rules are managed automatically by useConcertina().
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.concertina-content {
|
|
8
|
+
--concertina-open-duration: 200ms;
|
|
9
|
+
--concertina-close-duration: 150ms;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.concertina-content[data-state="open"] {
|
|
14
|
+
animation: concertina-open var(--concertina-open-duration) ease-out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.concertina-content[data-state="closed"] {
|
|
18
|
+
animation: concertina-close var(--concertina-close-duration) ease-out forwards;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes concertina-open {
|
|
22
|
+
from {
|
|
23
|
+
height: 0;
|
|
24
|
+
opacity: 0;
|
|
25
|
+
}
|
|
26
|
+
to {
|
|
27
|
+
height: var(--radix-accordion-content-height);
|
|
28
|
+
opacity: 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@keyframes concertina-close {
|
|
33
|
+
from {
|
|
34
|
+
height: var(--radix-accordion-content-height);
|
|
35
|
+
opacity: 1;
|
|
36
|
+
}
|
|
37
|
+
to {
|
|
38
|
+
height: 0;
|
|
39
|
+
opacity: 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* When switching between items, suppress all animations so the layout
|
|
44
|
+
is immediately in its final state for scroll pinning.
|
|
45
|
+
data-switching is set by useConcertina(), cleared after paint. */
|
|
46
|
+
[data-switching] .concertina-content[data-state="closed"] {
|
|
47
|
+
animation: none;
|
|
48
|
+
height: 0;
|
|
49
|
+
opacity: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[data-switching] .concertina-content[data-state="open"] {
|
|
53
|
+
animation: none;
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "concertina",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React hook for scroll-pinned Radix Accordion panels. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./styles.css": "./dist/styles.css"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": [
|
|
26
|
+
"./dist/styles.css"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup && cp src/styles.css dist/styles.css",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=16.8.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^19.0.0",
|
|
38
|
+
"react": "^19.0.0",
|
|
39
|
+
"tsup": "^8.0.0",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"radix",
|
|
44
|
+
"accordion",
|
|
45
|
+
"scroll",
|
|
46
|
+
"pin",
|
|
47
|
+
"react",
|
|
48
|
+
"hook"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/ryandward/concertina"
|
|
54
|
+
},
|
|
55
|
+
"author": "Ryan Ward"
|
|
56
|
+
}
|