panelset 1.0.8 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0] - 2026-06-22
4
+
5
+ ### Added
6
+ - PanelControl: a tablist controller with keyboard navigation
7
+ - Web component versions
8
+ - PNavigation on PanelSet: `next()`, `prev()` etc
9
+ - `addPanel()` and `removePanel()`
10
+ - `refresh()`
11
+ - Verb buttons wired by delegation: `data-ps-next`, `data-ps-prev`, `data-ps-close`.
12
+ - Lifecycle events
13
+ - Async content loading
14
+
15
+
16
+ ## [1.0.9] - 2026-04-10
17
+
18
+ ### Added
19
+ - Added Panel
20
+ - New documentation
21
+ - Added destroy() method
22
+ - Added data-panel-trigger implicit wiring (auto ID assignment)
23
+
24
+ ### Fixed
25
+ - Fixed mid-close reversal bug (smooth re-open from mid-animation)
26
+ - Fixed focus jump when closeSiblings closes a sibling
27
+
28
+ ### Removed
29
+ - Removed Core.measure()
30
+
31
+
3
32
 
4
33
  ## [1.0.8] - 2026-02-18
5
34
 
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # PanelSet
2
+
3
+ [![Version](https://img.shields.io/npm/v/panelset)]() [![Downloads](https://img.shields.io/npm/dt/panelset)]()
4
+
5
+ **Flexible panel management with smooth transitions.**
6
+
7
+ A small library for animating elements between sizes. Three classes share one animation core (a lock / measure / animate / unlock cycle): transitions are CSS only, JavaScript only measures and sets pixel values. Accessible by default (managed ARIA and focus), interrupt-safe, and it respects `prefers-reduced-motion`.
8
+
9
+ **Documentation:** the full guides, every option, and live examples are at <https://martinomagnifico.github.io/panelset/>. This README is a quick reference.
10
+
11
+
12
+
13
+ ## The three classes
14
+
15
+ | Class | What it does |
16
+ |---|---|
17
+ | **`Panel`** | A single element that opens and closes (accordions, show-more, sidebars, drawers). Animates `height` or `width`. |
18
+ | **`PanelSet`** | A container that switches between mutually exclusive panels (tabs, wizards, steppers). Animates its own height to fit the incoming panel. |
19
+ | **`PanelControl`** | Optional. Makes a tab strip or sidebar drive a `PanelSet`: keyboard navigation, roving `tabindex`, selection state (`aria-selected` on real tabs, `aria-current` otherwise), and tab locking through `setTabState()`. |
20
+
21
+ `PanelControl` is side-effect-free, so it tree-shakes out of the ESM build when you don’t import it.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install panelset
27
+ ```
28
+
29
+ ```js
30
+ import { Panel, PanelSet, PanelControl } from 'panelset';
31
+ import 'panelset/style.css';
32
+ ```
33
+
34
+ Or as a script tag (IIFE bundle):
35
+
36
+ ```html
37
+ <link rel="stylesheet" href="panelset.css">
38
+ <script src="panelset.js"></script>
39
+ <!-- exposes window.Panel / PanelSet / PanelControl and registers
40
+ <ps-panel> / <ps-panelset> / <ps-panelcontrol> -->
41
+ ```
42
+
43
+ ## Panel
44
+
45
+ Panels are **closed by default** (no class needed). Add `is-open` to start open.
46
+
47
+ ```html
48
+ <button aria-controls="my-panel" aria-expanded="false">Toggle</button>
49
+
50
+ <div id="my-panel" data-panel>
51
+ <div class="panel-wrapper">Content here</div>
52
+ </div>
53
+ ```
54
+
55
+ ```js
56
+ import { Panel } from 'panelset';
57
+ Panel.init();
58
+ ```
59
+
60
+ Any `[aria-controls]` element is a trigger. As a shortcut, a `[data-panel-trigger]` button placed next to a panel (or as the direct child of a heading next to it) gets its `id` and ARIA set up automatically.
61
+
62
+ ### Accordion (group)
63
+
64
+ ```html
65
+ <div data-panel-group data-panel-close-siblings>
66
+ <button aria-controls="acc-1" aria-expanded="false">Item 1</button>
67
+ <div id="acc-1" data-panel><div class="panel-wrapper">Answer 1</div></div>
68
+
69
+ <button aria-controls="acc-2" aria-expanded="false">Item 2</button>
70
+ <div id="acc-2" data-panel><div class="panel-wrapper">Answer 2</div></div>
71
+ </div>
72
+ ```
73
+
74
+ `data-panel-close-siblings` (or `closeSiblings: true`) makes opening one panel close the others.
75
+
76
+ ### Panel options
77
+
78
+ | Option | Type | Default | Description |
79
+ |---|---|---|---|
80
+ | `align` | `'start' \| 'center' \| 'end'` | `'start'` | Content alignment within the clipped container |
81
+ | `autoFocus` | `false \| true \| 'heading' \| 'first' \| 'input'` | `false` | Move focus into the panel on open |
82
+ | `axis` | `'vertical' \| 'horizontal'` | `'vertical'` | Which dimension animates (height or width) |
83
+ | `closeOnResize` | `boolean` | `false` | Close the panel when the window is resized |
84
+ | `closeSiblings` | `boolean` | `false` | Close other open panels in the same group |
85
+ | `debug` | `boolean` | `false` | Log events to the console |
86
+ | `deepLink` | `boolean` | `false` | Reflect open state in the `?panel=` URL |
87
+ | `interruptible` | `boolean` | `true` | Allow a new open/close to interrupt one in progress |
88
+ | `loadingDelay` | `number` | `320` | ms before the spinner appears during async loading |
89
+ | `loadingHeight` | `number` | `150` | px reserved while async content loads |
90
+ | `persist` | `boolean` | `false` | Save open/closed state to `localStorage` |
91
+ | `returnFocus` | `boolean` | `true` | Return focus to the trigger on close |
92
+ | `transitions` | `boolean` | `true` | Enable/disable CSS transitions |
93
+
94
+ ### Async content
95
+
96
+ ```js
97
+ const panel = new Panel('#my-panel');
98
+
99
+ panel.onBeforeOpen((el, signal) => {
100
+ return fetch('/api/content', { signal })
101
+ .then(r => r.text())
102
+ .then(html => { el.querySelector('.panel-wrapper').innerHTML = html; });
103
+ }, { once: true });
104
+ ```
105
+
106
+ Or listen for the `panel:beforeopen` event and call `e.detail.waitUntil(promise)` to hold the open until it resolves.
107
+
108
+ ## PanelSet
109
+
110
+ ```html
111
+ <nav role="tablist" data-panelcontrol>
112
+ <button role="tab" aria-controls="panel-1" aria-selected="true">Tab 1</button>
113
+ <button role="tab" aria-controls="panel-2">Tab 2</button>
114
+ </nav>
115
+
116
+ <div data-panelset>
117
+ <div class="panel-wrapper">
118
+ <div id="panel-1" role="tabpanel" class="active">Content 1</div>
119
+ <div id="panel-2" role="tabpanel" hidden>Content 2</div>
120
+ </div>
121
+ </div>
122
+ ```
123
+
124
+ ```js
125
+ import { PanelSet, PanelControl } from 'panelset';
126
+ PanelSet.init();
127
+ PanelControl.init(); // keyboard navigation + state for the tab strip
128
+ ```
129
+
130
+ Mark the starting panel with `class="active"` and every other panel `hidden`.
131
+
132
+ ### PanelSet options
133
+
134
+ | Option | Type | Default | Description |
135
+ |---|---|---|---|
136
+ | `align` | `'start' \| 'center' \| 'end'` | `'start'` | Alignment while opening/closing |
137
+ | `autoFocus` | `false \| true \| 'heading' \| 'first' \| 'input'` | `false` | Move focus into the panel on activation |
138
+ | `closable` | `boolean` | `false` | Allow the whole container to open and close |
139
+ | `closeOnTab` | `boolean` | `false` | Clicking the active tab closes the container (needs `closable`) |
140
+ | `debug` | `boolean` | `false` | Log events to the console |
141
+ | `deepLink` | `boolean` | `false` | Reflect the active panel in the `?panel=` URL |
142
+ | `disabledMode` | `'aria' \| 'native'` | `'aria'` | How `data-ps-next` / `-prev` buttons are disabled at the ends |
143
+ | `interruptible` | `boolean` | `true` | Allow a new activation to interrupt one in progress |
144
+ | `levels` | `boolean` | `false` | Give panels a depth order from DOM position; forward/back slide opposite ways |
145
+ | `loadingDelay` | `number` | `320` | ms before the spinner appears during async loading |
146
+ | `loadingHeight` | `number` | `150` | px reserved while async content loads |
147
+ | `loop` | `boolean` | `false` | `next()` / `prev()` wrap around the ends |
148
+ | `manageLabels` | `boolean` | `true` | Link each panel to its tab via `aria-labelledby` (auto-generates a tab id if needed) |
149
+ | `manageTriggers` | `boolean` | `true` | Reflect selection (`aria-selected` on real tabs, `aria-current` otherwise) / activating state onto `[aria-controls]` triggers |
150
+ | `persist` | `boolean` | `false` | Save the active panel id to `localStorage` |
151
+ | `returnFocus` | `boolean` | `false` | Return focus to the trigger on close |
152
+ | `transitions` | `boolean \| { panels?: boolean, height?: boolean }` | `true` | Enable/disable transitions (or per axis) |
153
+
154
+ ### Async content
155
+
156
+ ```js
157
+ const ps = new PanelSet('#my-panelset');
158
+
159
+ ps.onBeforeOpen((targetPanel, signal) => {
160
+ return fetch(`/api/${targetPanel.id}`, { signal })
161
+ .then(r => r.json())
162
+ .then(data => { targetPanel.innerHTML = render(data); });
163
+ }, { once: true });
164
+ ```
165
+
166
+ ## PanelControl
167
+
168
+ Drives one `PanelSet` from its trigger elements, so you do not hand-write the tab interaction. Put `data-panelcontrol` on the container; add `role="tablist"` (with `role="tab"` buttons) to switch on the keyboard model (arrow keys, `Home` / `End`, roving `tabindex`). It finds its `PanelSet` through the panels the triggers point at, so the control can live anywhere in the DOM.
169
+
170
+ Lock or unlock a tab from your own code:
171
+
172
+ ```js
173
+ control.setTabState('panel-3', 'disabled'); // lock
174
+ control.setTabState('panel-3', 'enabled'); // unlock
175
+ ```
176
+
177
+ | Option | Type | Default | Description |
178
+ |---|---|---|---|
179
+ | `activation` | `'manual' \| 'auto'` | `'manual'` | `manual`: arrows move focus, Enter/Space/click activates. `auto`: arrows activate too. |
180
+ | `debug` | `boolean` | `false` | Log to the console |
181
+
182
+ ## Configuration sources
183
+
184
+ Every option can also be set as a **data attribute** in the markup, or as a plain attribute on the **web component**. When the same option is set more than once, the most specific wins: **defaults -> JS options -> data attribute**. The exact attribute names are listed per option in the docs.
185
+
186
+ ## Web components
187
+
188
+ `register()` defines `<ps-panel>`, `<ps-panelset>`, and `<ps-panelcontrol>` (the script-tag build calls it for you). On the elements, options are plain attributes, no `data-` prefix.
189
+
190
+ ```js
191
+ import { register } from 'panelset';
192
+ register(); // default 'ps' prefix
193
+ register('acme'); // <acme-panel>, <acme-panelset>, <acme-panelcontrol>
194
+ ```
195
+
196
+ ## CSS
197
+
198
+ Timing and sizing are CSS custom properties, set on the element or any ancestor (all timing values need a unit, e.g. `0.25s` not `0`). For example, on a Panel:
199
+
200
+ ```css
201
+ [data-panel] {
202
+ --ps-open-speed: 0.25s;
203
+ --ps-open-timing: ease-in-out;
204
+ --ps-close-speed: 0.25s;
205
+ --ps-close-timing: ease-in-out;
206
+ }
207
+ ```
208
+
209
+ `PanelSet` exposes a similar set for its fade and height timing. See the docs for the full list.
210
+
211
+ ## Support
212
+
213
+ PanelSet is free and open source. If it saves you time, consider [sponsoring my work](https://ko-fi.com/martinomagnifico).
214
+
215
+ ## License
216
+
217
+ MIT © [Martinomagnifico](https://github.com/martinomagnifico)