panelset 0.5.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.
Files changed (39) hide show
  1. package/dist/index.d.ts +111 -0
  2. package/dist/panelset.css +1 -0
  3. package/dist/panelset.js +317 -0
  4. package/dist/panelset.js.map +1 -0
  5. package/package.json +49 -0
  6. package/src/docs/assets/scripts/copybutton.js +44 -0
  7. package/src/docs/assets/scripts/example-async.js +161 -0
  8. package/src/docs/assets/scripts/example-closable.js +27 -0
  9. package/src/docs/assets/scripts/example-megamenu.js +84 -0
  10. package/src/docs/assets/scripts/example.js +29 -0
  11. package/src/docs/assets/scripts/main.js +7 -0
  12. package/src/docs/assets/styles/_base.scss +13 -0
  13. package/src/docs/assets/styles/_code.scss +121 -0
  14. package/src/docs/assets/styles/_demos.scss +180 -0
  15. package/src/docs/assets/styles/_landingpage.scss +41 -0
  16. package/src/docs/assets/styles/_layout.scss +80 -0
  17. package/src/docs/assets/styles/_sidebar.scss +67 -0
  18. package/src/docs/assets/styles/_typography.scss +116 -0
  19. package/src/docs/assets/styles/_variables.scss +32 -0
  20. package/src/docs/assets/styles/docs.scss +64 -0
  21. package/src/docs/views/api-reference.pug +474 -0
  22. package/src/docs/views/configuration.pug +173 -0
  23. package/src/docs/views/events.pug +222 -0
  24. package/src/docs/views/examples/async.pug +268 -0
  25. package/src/docs/views/examples/basic.pug +155 -0
  26. package/src/docs/views/examples/closable.pug +97 -0
  27. package/src/docs/views/getting-started.pug +99 -0
  28. package/src/docs/views/index.pug +38 -0
  29. package/src/docs/views/templates/includes/_head.pug +11 -0
  30. package/src/docs/views/templates/includes/_mixins.pug +100 -0
  31. package/src/docs/views/templates/includes/_scripts.pug +14 -0
  32. package/src/docs/views/templates/includes/_sidebar.pug +18 -0
  33. package/src/docs/views/templates/layouts/_base.pug +36 -0
  34. package/src/docs/views/transitions.pug +141 -0
  35. package/src/lib/index.ts +685 -0
  36. package/src/lib/styles/_base.scss +99 -0
  37. package/src/lib/styles/_loading.scss +47 -0
  38. package/src/lib/styles/_variables.scss +19 -0
  39. package/src/lib/styles/panelset.scss +3 -0
@@ -0,0 +1,222 @@
1
+ extends /docs/views/templates/layouts/_base
2
+
3
+ append variables
4
+ - pagetitle = "Events"
5
+
6
+
7
+ block content
8
+ .lane
9
+ .container
10
+ .page-header
11
+ h1 Events
12
+ p.lead PanelSet dispatches custom events for activation lifecycle tracking
13
+ p All events are dispatched from the panelset container element and bubble up the DOM.
14
+
15
+ .lane
16
+ .container
17
+ h2 Available events and possible uses
18
+
19
+ .example.api
20
+ h3.code.method-signature Event: 'ps:ready'
21
+ p Fires when the PanelSet is fully initialized
22
+ h4 Detail:
23
+ dl
24
+ dt.code container: HTMLElement
25
+ dd The container element the PanelSet was initialized on
26
+
27
+ dt.code instance: PanelSet
28
+ dd The PanelSet instance that was initialized
29
+ br
30
+ +codeblock('JavaScript').
31
+ document.addEventListener('ps:ready', (e) => {
32
+ const container = e.detail.container; // or e.target
33
+ const instance = e.detail.instance;
34
+
35
+ // Track which panelset was initialized
36
+ analytics.track('PanelSet Initialized', {
37
+ id: container.id,
38
+ panelCount: instance.panels.length
39
+ });
40
+ });
41
+ //-
42
+
43
+ .example.api.white
44
+ h3.code.method-signature Event: 'ps:beforeactivate'
45
+ p Fires before activation begins, can be used for async content loading
46
+
47
+ h4 Detail:
48
+ dl
49
+ dt.code panelId: string
50
+ dd ID of the panel being activated
51
+
52
+ dt.code targetPanel: HTMLElement
53
+ dd The panel element being activated
54
+
55
+ dt.code outgoingPanel: HTMLElement | null
56
+ dd The panel being deactivated
57
+
58
+ dt.code signal: AbortSignal
59
+ dd Signal to abort async operations if user clicks away
60
+
61
+ dt.code promise: Promise | null
62
+ dd Set this to a promise to delay activation until it resolves
63
+
64
+ br
65
+ +codeblock('JavaScript').
66
+ container.addEventListener('ps:beforeactivate', (e) => {
67
+ const { targetPanel, signal } = e.detail;
68
+
69
+ // Skip if already loaded
70
+ if (targetPanel.dataset.loaded === 'true') return;
71
+
72
+ // Set promise to load content
73
+ e.detail.promise = fetch(`/api/panel/${targetPanel.id}`, { signal })
74
+ .then(response => response.text())
75
+ .then(html => {
76
+ targetPanel.innerHTML = html;
77
+ targetPanel.dataset.loaded = 'true';
78
+ });
79
+ });
80
+ //-
81
+ br
82
+ p Or use the convenience method:
83
+ +codeblock('JavaScript').
84
+ panelSet.onBeforeActivate((targetPanel, signal) => {
85
+ return fetch(`/api/panel/${targetPanel.id}`, { signal })
86
+ .then(response => response.text())
87
+ .then(html => targetPanel.innerHTML = html);
88
+ }, { once: true }); // Auto-caching
89
+ //-
90
+
91
+ .example.api.white
92
+ h3.code.method-signature Event: 'ps:activationstart'
93
+ p Fires before any transitions begin
94
+ h4 Detail:
95
+ dl
96
+ dt.code panelId: string
97
+ dd ID of the panel being activated
98
+
99
+ dt.code trigger: HTMLElement | null
100
+ dd The button/element that triggered the activation (if provided)
101
+ br
102
+ +codeblock('JavaScript').
103
+ container.addEventListener('ps:activationstart', (e) => {
104
+ console.log('Starting activation:', e.detail.panelId);
105
+ console.log('Triggered by:', e.detail.trigger);
106
+
107
+ // Update UI, analytics, etc.
108
+ if (e.detail.trigger) {
109
+ e.detail.trigger.setAttribute('aria-busy', 'true');
110
+ }
111
+ });
112
+ //-
113
+
114
+
115
+ .example.api.white
116
+ h3.code.method-signature Event: 'ps:activationcomplete'
117
+ p Fires after height and panel transitions complete
118
+ h4 Detail:
119
+ dl
120
+ dt.code panelId: string
121
+ dd ID of the panel being activated
122
+
123
+ dt.code trigger: HTMLElement | null
124
+ dd The button/element that triggered the activation (if provided)
125
+ br
126
+ +codeblock('JavaScript').
127
+ container.addEventListener('ps:activationcomplete', (e) => {
128
+ console.log('Activation complete:', e.detail.panelId);
129
+
130
+ // Focus management, scroll, analytics
131
+ const panel = document.getElementById(e.detail.panelId);
132
+ const firstInput = panel.querySelector('input, button');
133
+ firstInput?.focus();
134
+ });
135
+ //-
136
+
137
+
138
+ .example.api.white
139
+ h3.code.method-signature Event: 'ps:activationaborted'
140
+ p Fires if an activation is aborted due to a new activation request
141
+ h4 Detail:
142
+ dl
143
+ dt.code panelId: string
144
+ dd ID of the panel of which the activation was aborted
145
+
146
+ dt.code trigger: HTMLElement | null
147
+ dd The button/element that triggered the event
148
+ br
149
+ +codeblock('JavaScript').
150
+ container.addEventListener('ps:activationaborted', (e) => {
151
+ console.log('Activation aborted:', e.detail.panelId);
152
+
153
+ // Cleanup, remove loading states
154
+ if (e.detail.trigger) {
155
+ e.detail.trigger.setAttribute('aria-busy', 'false');
156
+ }
157
+ });
158
+ //-
159
+
160
+
161
+ append scripts
162
+ if isProd
163
+ script(type="module", vite-ignore).
164
+ import { PanelSet } from '/lib/panelset.js';
165
+ PanelSet.init();
166
+ else
167
+ script(type="module").
168
+ import { PanelSet } from '/src/lib/index.js';
169
+ import '/src/lib/styles/panelset.scss';
170
+ PanelSet.init({debug: true});
171
+
172
+ script.
173
+ let transitionsEnabled = true;
174
+
175
+ let closablePanelSet = null;
176
+ document.addEventListener('panelset:ready', (e) => {
177
+ const instance = e.detail.instance;
178
+ const container = e.target;
179
+ if (container.id == 'config-closable-demo') {
180
+ closablePanelSet = instance;
181
+ }
182
+ })
183
+
184
+ document.addEventListener('click', (e) => {
185
+ const button = e.target.closest('button');
186
+ if (!button) return;
187
+
188
+ if (button.dataset.panel) {
189
+ const panelId = button.getAttribute('data-panel');
190
+ const panel = document.getElementById(panelId);
191
+ const container = panel?.closest('[data-panelset]');
192
+
193
+ if (container?.panelSet) {
194
+ container.panelSet.setActive(panelId, true, { trigger: button });
195
+ }
196
+ }
197
+
198
+ if (button.id == "toggle-transitions") {
199
+ const transDemo = document.getElementById('config-transitions-demo');
200
+ if (transDemo?.panelSet.config.transitions == false) {
201
+ transDemo.panelSet.config.transitions = true;
202
+ button.textContent = "Disable Transitions";
203
+ } else {
204
+ transDemo.panelSet.config.transitions = false;
205
+ button.textContent = "Enable Transitions";
206
+ }
207
+ }
208
+ });
209
+
210
+
211
+
212
+ // Log events
213
+ document.addEventListener('activationstart', (e) => {
214
+ console.log('[Demo PanelSet log] Activationstart:', e.detail);
215
+ });
216
+
217
+ document.addEventListener('activationcomplete', (e) => {
218
+ console.log('[Demo PanelsSet log] Activationcomplete:', e.detail);
219
+ });
220
+
221
+
222
+
@@ -0,0 +1,268 @@
1
+ extends /docs/views/templates/layouts/_base
2
+
3
+ append variables
4
+ - pagetitle = "Async Loading"
5
+
6
+ block content
7
+ .lane
8
+ .container
9
+ .page-header
10
+ h1 Async Loading
11
+ p.lead Load panel content on-demand with promises, fetch, or heavy computation
12
+
13
+ .lane
14
+ .container
15
+ .title-text
16
+ h2 Overview
17
+ p PanelSet supports async content loading through the #[code ps:beforeactivate] event. This allows you to fetch data from APIs, process large datasets, or perform any async operation before a panel becomes visible.
18
+
19
+ h3 Key Features
20
+ ul
21
+ li Load content only when needed (lazy loading)
22
+ li Automatic loading spinner with configurable delay
23
+ li Built-in abort support for cancelled activations
24
+ li Auto-caching with the #[code once] option
25
+ li Smooth height transitions after content loads
26
+
27
+ .lane
28
+ .container
29
+ h2 The onBeforeActivate() Method
30
+
31
+ .title-text
32
+ p The easiest way to add async loading is with the #[code onBeforeActivate()] convenience method:
33
+
34
+ +codeblock('JS').
35
+ const panelSet = new PanelSet('#my-panelset');
36
+
37
+ panelSet.onBeforeActivate((targetPanel, signal) => {
38
+ // Return a promise to delay activation
39
+ return fetch(`/api/content/${targetPanel.id}`, { signal })
40
+ .then(response => response.text())
41
+ .then(html => {
42
+ targetPanel.innerHTML = html;
43
+ });
44
+ }, { once: true }); // Load once, cache forever
45
+
46
+ .example.api
47
+ h3 Parameters
48
+ dl
49
+ dt targetPanel
50
+ dd The HTMLElement being activated
51
+
52
+ dt signal
53
+ dd AbortSignal for canceling the operation if user clicks away
54
+
55
+ dt options.once
56
+ dd If #[code true], content loads only once and is cached. Set to #[code false] to always reload. Default: #[code false]
57
+
58
+ .lane
59
+ .container
60
+ h2 Live Example: Fetch from API
61
+ p This example uses a 'slowfetch' to mimic a slow network request to fetch recipe data. The renderRecipe function just formats the data into HTML.
62
+
63
+ .example
64
+ .demo
65
+ .demo-controls
66
+ button(data-panel="async-panel-1") Panel 1 (Static)
67
+ button(data-panel="async-panel-2") Panel 2 (Fetch Recipe)
68
+ button(data-panel="async-panel-3") Panel 3 (Static)
69
+
70
+ #async-demo(data-panelset data-empty-panel-height="300" data-loading-delay="200")
71
+ .panel-wrapper
72
+ #async-panel-1(role="tabpanel" class="active" data-loaded="true")
73
+ h3 Static Content
74
+ p This panel has pre-loaded content.
75
+
76
+ #async-panel-2(role="tabpanel" hidden)
77
+ // Empty - will load async
78
+
79
+ #async-panel-3(role="tabpanel" hidden)
80
+ h3 Another Static Panel
81
+ p More pre-loaded content.
82
+
83
+ +codeblock('JS').
84
+ asyncPanelSet.onBeforeActivate((targetPanel, signal) => {
85
+ if (targetPanel.id === 'async-panel-2') {
86
+ return slowfetch(`https://dummyjson.com/recipes/${randomIntFromInterval(1, 10)}`, { signal })
87
+ .then(response => response.json())
88
+ .then(data => {
89
+ targetPanel.innerHTML = renderRecipe(data);
90
+ });
91
+ }
92
+ });
93
+
94
+ .lane
95
+ .container
96
+ h2 Example: Heavy Computation
97
+
98
+ .title-text
99
+ p You can also use async loading for CPU-intensive operations:
100
+
101
+ +codeblock('JS').
102
+ panelSet.onBeforeActivate((targetPanel, signal) => {
103
+ if (targetPanel.id === 'heavy-calc') {
104
+ return new Promise((resolve) => {
105
+ // Simulate heavy calculation
106
+ setTimeout(() => {
107
+ const result = fibonacci(40); // Actually slow!
108
+ targetPanel.innerHTML = `
109
+ <h3>Calculation Complete</h3>
110
+ <p>Fibonacci(40) = ${result}</p>
111
+ `;
112
+ resolve();
113
+ }, 2000);
114
+ });
115
+ }
116
+ });
117
+
118
+ function fibonacci(n) {
119
+ if (n <= 1) return n;
120
+ return fibonacci(n - 1) + fibonacci(n - 2);
121
+ }
122
+
123
+ .lane
124
+ .container
125
+ .title-text
126
+ h2 Using the Event Directly
127
+ p For more control, listen to the #[code ps:beforeactivate] event directly:
128
+
129
+ +codeblock('JS').
130
+ container.addEventListener('ps:beforeactivate', (e) => {
131
+ const { panelId, targetPanel, signal } = e.detail;
132
+
133
+ // Skip if already loaded
134
+ if (targetPanel.dataset.loaded === 'true') return;
135
+
136
+ // Set the promise property to delay activation
137
+ e.detail.promise = fetch(`/api/panel/${panelId}`, { signal })
138
+ .then(response => response.text())
139
+ .then(html => {
140
+ targetPanel.innerHTML = html;
141
+ targetPanel.dataset.loaded = 'true';
142
+ });
143
+ });
144
+
145
+ .example.api
146
+ h3 Event Detail Properties
147
+ dl
148
+ dt panelId
149
+ dd String - ID of the panel being activated
150
+
151
+ dt targetPanel
152
+ dd HTMLElement - The panel element
153
+
154
+ dt outgoingPanel
155
+ dd HTMLElement | null - The panel being deactivated
156
+
157
+ dt signal
158
+ dd AbortSignal - Use with fetch to cancel if user clicks away
159
+
160
+ dt promise
161
+ dd Promise | null - Set this to delay activation until it resolves
162
+
163
+ .lane
164
+ .container
165
+ h2 Loading States
166
+
167
+ .title-text
168
+ h3 Loading Spinner
169
+ p During async operations, PanelSet shows a loading spinner. Configure the delay to prevent flashing on fast connections:
170
+
171
+ +codeblock('JS').
172
+ const panelSet = new PanelSet('#my-panelset', {
173
+ loadingDelay: 300 // Wait 300ms before showing spinner
174
+ });
175
+
176
+ .title-text
177
+ p Or via data attribute:
178
+
179
+ +codeblock('HTML').
180
+ <div data-panelset data-loading-delay="300">
181
+
182
+ .title-text
183
+ h3 Empty Panel Height
184
+ p Set a minimum height during loading (shows spinner in that space):
185
+
186
+ +codeblock('JS').
187
+ const panelSet = new PanelSet('#my-panelset', {
188
+ emptyPanelHeight: 200 // 200px minimum during load
189
+ });
190
+
191
+ .lane
192
+ .container
193
+ h2 Abort Handling
194
+
195
+ .title-text
196
+ p When users click away during loading, the operation is automatically aborted via the AbortSignal:
197
+
198
+ +codeblock('JS').
199
+ panelSet.onBeforeActivate((targetPanel, signal) => {
200
+ // Pass signal to fetch - automatically cancels on abort
201
+ return fetch('/api/data', { signal })
202
+ .then(/* ... */);
203
+ });
204
+
205
+ .title-text
206
+ p The #[code ps:activationaborted] event fires when an async load is cancelled:
207
+
208
+ +codeblock('JS').
209
+ container.addEventListener('ps:activationaborted', (e) => {
210
+ console.log('Load cancelled:', e.detail.panelId);
211
+ // Clean up, show message, etc.
212
+ });
213
+
214
+ .lane
215
+ .container
216
+ h2 Complete Example
217
+
218
+ +codeblock('JS').
219
+ import { PanelSet } from 'panelset';
220
+ import 'panelset/dist/panelset.css';
221
+
222
+ const panelSet = new PanelSet('#my-panelset', {
223
+ emptyPanelHeight: 300,
224
+ loadingDelay: 200
225
+ });
226
+
227
+ // Async loading with error handling
228
+ panelSet.onBeforeActivate((targetPanel, signal) => {
229
+ if (targetPanel.id === 'async-content') {
230
+ return fetch('/api/content', { signal })
231
+ .then(response => {
232
+ if (!response.ok) throw new Error('Load failed');
233
+ return response.json();
234
+ })
235
+ .then(data => {
236
+ targetPanel.innerHTML = renderContent(data);
237
+ })
238
+ .catch(error => {
239
+ if (error.name === 'AbortError') return; // User clicked away
240
+ targetPanel.innerHTML = `<p class="error">Failed to load content</p>`;
241
+ });
242
+ }
243
+ }, { once: true });
244
+
245
+ // Event listeners
246
+ panelSet.element.addEventListener('ps:activationaborted', (e) => {
247
+ console.log('Cancelled:', e.detail.panelId);
248
+ });
249
+
250
+ panelSet.element.addEventListener('ps:activationcomplete', (e) => {
251
+ console.log('Complete:', e.detail.panelId);
252
+ });
253
+
254
+ append scripts
255
+ if isProd
256
+ script(type="module", vite-ignore).
257
+ import { PanelSet } from '/lib/panelset.js';
258
+ PanelSet.init({debug: true});
259
+ else
260
+ script(type="module").
261
+ import { PanelSet } from '/src/lib/index.js';
262
+ import '/src/lib/styles/panelset.scss';
263
+ PanelSet.init({debug: true});
264
+
265
+ if isProd
266
+ script(src="/assets/scripts/examples/async.js" type="module" vite-ignore)
267
+ else
268
+ script(type="module" src="/src/docs/assets/scripts/example-async.js")
@@ -0,0 +1,155 @@
1
+ extends /docs/views/templates/layouts/_base
2
+
3
+ append variables
4
+ - pagetitle = "Basic tabs"
5
+
6
+
7
+ block content
8
+ .lane
9
+ .container
10
+ .page-header
11
+ h1 Basic tabs example
12
+ p.lead The most common use case - a simple tabbed interface
13
+
14
+ .lane
15
+ .container
16
+ h2 Live demo
17
+
18
+ .example
19
+
20
+ .demo
21
+ nav.tabs(role="tablist" aria-label="Content sections")
22
+ button(role="tab" aria-controls="demo-panel-1" aria-selected="true") Features
23
+ button(role="tab" aria-controls="demo-panel-2") Pricing
24
+ button(role="tab" aria-controls="demo-panel-3") Support
25
+
26
+ +examplepanelset('demo').tab-content
27
+
28
+
29
+ +codeblock('HTML')
30
+ +examplepanelset('demo').tab-content
31
+
32
+ +codeblock('JavaScript').
33
+ const tabs = document.querySelectorAll('[role="tab"]');
34
+
35
+ document.addEventListener('click', (e) => {
36
+ const button = e.target.closest('button');
37
+ if (!button) return;
38
+
39
+ if (button.hasAttribute('aria-controls')) {
40
+ const panelId = button.getAttribute('aria-controls');
41
+ const panel = document.getElementById(panelId);
42
+ const container = panel?.closest('[data-panelset]');
43
+
44
+ if (container?.panelSet) {
45
+ container.panelSet.setActive(panelId, true, { trigger: button });
46
+ }
47
+ }
48
+ });
49
+
50
+ // Update ARIA states
51
+ document.addEventListener('ps:activationstart', (e) => {
52
+ tabs.forEach(tab => {
53
+ const isActive = tab.getAttribute('aria-controls') === e.detail.panelId;
54
+ tab.setAttribute('aria-selected', isActive);
55
+ });
56
+ });
57
+
58
+
59
+ .lane
60
+ .container
61
+ h2 Accessibility features
62
+ p PanelSet is built with accessibility in mind, but because it provides low-level functionality, there are only a few of them built in:
63
+ ul
64
+ li
65
+ strong ARIA roles:
66
+ | Uses #[code role='tabpanel']. This is used internally to find the panels.
67
+ li
68
+ strong Hidden attribute:
69
+ | Properly hides inactive panels from screen readers
70
+
71
+ p Beyond that it's up to you to implement proper ARIA roles and attributes in your markup, like linking the button and panel ID's with aria. Futhermore, PanelSet does not manage focus or keyboard shortcuts like arrow keys support for you, so you'll need to handle that as appropriate for your use case.
72
+
73
+ append scripts
74
+ if isProd
75
+ script(type="module", vite-ignore).
76
+ import { PanelSet } from '/lib/panelset.js';
77
+ PanelSet.init();
78
+ else
79
+ script(type="module").
80
+ import { PanelSet } from '/src/lib/index.js';
81
+ import '/src/lib/styles/panelset.scss';
82
+ PanelSet.init({debug: true});
83
+
84
+ script.
85
+ const tabs = document.querySelectorAll('[role="tab"]');
86
+
87
+ document.addEventListener('click', (e) => {
88
+ const button = e.target.closest('button');
89
+ if (!button) return;
90
+
91
+ if (button.hasAttribute('aria-controls')) {
92
+ const panelId = button.getAttribute('aria-controls');
93
+ const panel = document.getElementById(panelId);
94
+ const container = panel?.closest('[data-panelset]');
95
+
96
+ if (container?.panelSet) {
97
+ container.panelSet.show(panelId, true, { trigger: button });
98
+ }
99
+ }
100
+ });
101
+
102
+ // Update ARIA states
103
+ document.addEventListener('ps:activationstart', (e) => {
104
+ tabs.forEach(tab => {
105
+ const isActive = tab.getAttribute('aria-controls') === e.detail.panelId;
106
+ tab.setAttribute('aria-selected', isActive);
107
+ });
108
+ });
109
+
110
+
111
+
112
+ append styles
113
+ style.
114
+ /* Tab-specific styles for this example */
115
+ //- #basic-tabs {
116
+ //- --fadeout-speed: 0.5s;
117
+ //- --fadein-speed: 0.5s;
118
+ //- --fadein-delay: 0.25s;
119
+ //- }
120
+ .tabs {
121
+ padding: 0 0.5rem;
122
+ display: flex;
123
+ //- gap: 0.5rem;
124
+ margin-bottom: 2px;
125
+ }
126
+
127
+ .tabs button {
128
+ background: transparent;
129
+ border: none;
130
+ border-bottom: 3px solid transparent;
131
+ padding: 0.75rem 1.5rem;
132
+ font-size: 1rem;
133
+ font-weight: 500;
134
+ color: #6b7280;
135
+ cursor: pointer;
136
+ transition: all 0.2s;
137
+ margin-bottom: -2px;
138
+ }
139
+
140
+ .tabs button:hover {
141
+ color: #3b82f6;
142
+ background: #f9fafb;
143
+ }
144
+
145
+ .tabs button[aria-selected="true"] {
146
+ color: #3b82f6;
147
+ border-bottom-color: #3b82f6;
148
+ }
149
+
150
+ .tab-content {
151
+ padding: 1.5rem;
152
+ background: white;
153
+ border: 1px solid #e5e7eb;
154
+ border-radius: 6px;
155
+ }