@extrachill/components 0.4.8 → 0.4.10

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.10] - 2026-03-25
4
+
5
+ ### Changed
6
+ - Port shared tab styling into responsive tabs component
7
+
8
+ ## [0.4.9] - 2026-03-25
9
+
10
+ ### Changed
11
+ - Add responsive mobile accordion mode for shared tabs
12
+
3
13
  ## [0.4.8] - 2026-03-25
4
14
 
5
15
  ### Changed
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type TabItem } from './Tabs.tsx';
3
+ export interface ResponsiveTabsProps {
4
+ tabs: TabItem[];
5
+ active: string;
6
+ onChange: (id: string) => void;
7
+ renderPanel: (id: string) => ReactNode;
8
+ className?: string;
9
+ classPrefix?: string;
10
+ tabsClassName?: string;
11
+ tabsClassPrefix?: string;
12
+ mobileBreakpoint?: number;
13
+ accordionClassName?: string;
14
+ showDesktopTabs?: boolean;
15
+ }
16
+ export declare function ResponsiveTabs({ tabs, active, onChange, renderPanel, className, classPrefix, tabsClassName, tabsClassPrefix, mobileBreakpoint, accordionClassName, showDesktopTabs, }: ResponsiveTabsProps): import("react/jsx-runtime").JSX.Element | null;
17
+ //# sourceMappingURL=ResponsiveTabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResponsiveTabs.d.ts","sourceRoot":"","sources":["../src/ResponsiveTabs.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAE,EAAE,EAAE,MAAM,KAAM,IAAI,CAAC;IACjC,WAAW,EAAE,CAAE,EAAE,EAAE,MAAM,KAAM,SAAS,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,cAAc,CAAE,EAC/B,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,WAAW,EACX,SAAc,EACd,WAAkC,EAClC,aAAkB,EAClB,eAA2B,EAC3B,gBAAsB,EACtB,kBAAuB,EACvB,eAAsB,GACtB,EAAE,mBAAmB,kDA8ErB"}
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { Tabs } from "./Tabs.js";
4
+ export function ResponsiveTabs({ tabs, active, onChange, renderPanel, className = '', classPrefix = 'ec-responsive-tabs', tabsClassName = '', tabsClassPrefix = 'ec-tabs', mobileBreakpoint = 768, accordionClassName = '', showDesktopTabs = true, }) {
5
+ const [isMobile, setIsMobile] = useState(() => {
6
+ if (typeof window === 'undefined') {
7
+ return false;
8
+ }
9
+ return window.innerWidth < mobileBreakpoint;
10
+ });
11
+ useEffect(() => {
12
+ if (typeof window === 'undefined') {
13
+ return undefined;
14
+ }
15
+ const handleResize = () => {
16
+ setIsMobile(window.innerWidth < mobileBreakpoint);
17
+ };
18
+ handleResize();
19
+ window.addEventListener('resize', handleResize);
20
+ return () => window.removeEventListener('resize', handleResize);
21
+ }, [mobileBreakpoint]);
22
+ const rootClass = useMemo(() => [classPrefix, isMobile ? `${classPrefix}--mobile` : `${classPrefix}--desktop`, className]
23
+ .filter(Boolean)
24
+ .join(' '), [className, classPrefix, isMobile]);
25
+ if (tabs.length === 0) {
26
+ return null;
27
+ }
28
+ if (!isMobile) {
29
+ return (_jsxs("div", { className: rootClass, children: [showDesktopTabs && (_jsx(Tabs, { tabs: tabs, active: active, onChange: onChange, className: tabsClassName, classPrefix: tabsClassPrefix })), _jsx("div", { className: `${classPrefix}__desktop-panel`, children: renderPanel(active) })] }));
30
+ }
31
+ return (_jsx("div", { className: rootClass, children: _jsx("div", { className: [`${classPrefix}__accordion`, accordionClassName].filter(Boolean).join(' '), children: tabs.map((tab) => {
32
+ const isActive = tab.id === active;
33
+ return (_jsxs("div", { className: `${classPrefix}__item${isActive ? ' is-active' : ''}`, children: [_jsxs("button", { type: "button", className: `${classPrefix}__trigger${isActive ? ' is-active' : ''}`, "aria-expanded": isActive, onClick: () => onChange(tab.id), children: [_jsx("span", { children: tab.label }), _jsx("span", { className: `${classPrefix}__arrow`, "aria-hidden": "true", children: isActive ? '▲' : '▼' })] }), isActive && _jsx("div", { className: `${classPrefix}__panel`, children: renderPanel(tab.id) })] }, tab.id));
34
+ }) }) }));
35
+ }
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { Pagination, type PaginationProps } from './Pagination.tsx';
8
8
  export { SearchBox, type SearchBoxProps } from './SearchBox.tsx';
9
9
  export { Modal, type ModalProps } from './Modal.tsx';
10
10
  export { Tabs, type TabsProps, type TabItem } from './Tabs.tsx';
11
+ export { ResponsiveTabs, type ResponsiveTabsProps } from './ResponsiveTabs.tsx';
11
12
  export { ShellTabs, type ShellTabsProps } from './ShellTabs.tsx';
12
13
  export { Panel, type PanelProps } from './Panel.tsx';
13
14
  export { Surface, type SurfaceProps } from './Surface.tsx';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { Pagination } from "./Pagination.js";
8
8
  export { SearchBox } from "./SearchBox.js";
9
9
  export { Modal } from "./Modal.js";
10
10
  export { Tabs } from "./Tabs.js";
11
+ export { ResponsiveTabs } from "./ResponsiveTabs.js";
11
12
  export { ShellTabs } from "./ShellTabs.js";
12
13
  export { Panel } from "./Panel.js";
13
14
  export { Surface } from "./Surface.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@extrachill/components",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Shared React components for the Extra Chill Platform.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,109 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import { Tabs, type TabItem } from './Tabs.tsx';
4
+
5
+ export interface ResponsiveTabsProps {
6
+ tabs: TabItem[];
7
+ active: string;
8
+ onChange: ( id: string ) => void;
9
+ renderPanel: ( id: string ) => ReactNode;
10
+ className?: string;
11
+ classPrefix?: string;
12
+ tabsClassName?: string;
13
+ tabsClassPrefix?: string;
14
+ mobileBreakpoint?: number;
15
+ accordionClassName?: string;
16
+ showDesktopTabs?: boolean;
17
+ }
18
+
19
+ export function ResponsiveTabs( {
20
+ tabs,
21
+ active,
22
+ onChange,
23
+ renderPanel,
24
+ className = '',
25
+ classPrefix = 'ec-responsive-tabs',
26
+ tabsClassName = '',
27
+ tabsClassPrefix = 'ec-tabs',
28
+ mobileBreakpoint = 768,
29
+ accordionClassName = '',
30
+ showDesktopTabs = true,
31
+ }: ResponsiveTabsProps ) {
32
+ const [ isMobile, setIsMobile ] = useState( () => {
33
+ if ( typeof window === 'undefined' ) {
34
+ return false;
35
+ }
36
+
37
+ return window.innerWidth < mobileBreakpoint;
38
+ } );
39
+
40
+ useEffect( () => {
41
+ if ( typeof window === 'undefined' ) {
42
+ return undefined;
43
+ }
44
+
45
+ const handleResize = () => {
46
+ setIsMobile( window.innerWidth < mobileBreakpoint );
47
+ };
48
+
49
+ handleResize();
50
+ window.addEventListener( 'resize', handleResize );
51
+
52
+ return () => window.removeEventListener( 'resize', handleResize );
53
+ }, [ mobileBreakpoint ] );
54
+
55
+ const rootClass = useMemo(
56
+ () => [ classPrefix, isMobile ? `${ classPrefix }--mobile` : `${ classPrefix }--desktop`, className ]
57
+ .filter( Boolean )
58
+ .join( ' ' ),
59
+ [ className, classPrefix, isMobile ]
60
+ );
61
+
62
+ if ( tabs.length === 0 ) {
63
+ return null;
64
+ }
65
+
66
+ if ( ! isMobile ) {
67
+ return (
68
+ <div className={ rootClass }>
69
+ { showDesktopTabs && (
70
+ <Tabs
71
+ tabs={ tabs }
72
+ active={ active }
73
+ onChange={ onChange }
74
+ className={ tabsClassName }
75
+ classPrefix={ tabsClassPrefix }
76
+ />
77
+ ) }
78
+ <div className={ `${ classPrefix }__desktop-panel` }>{ renderPanel( active ) }</div>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ return (
84
+ <div className={ rootClass }>
85
+ <div className={ [ `${ classPrefix }__accordion`, accordionClassName ].filter( Boolean ).join( ' ' ) }>
86
+ { tabs.map( ( tab ) => {
87
+ const isActive = tab.id === active;
88
+
89
+ return (
90
+ <div key={ tab.id } className={ `${ classPrefix }__item${ isActive ? ' is-active' : '' }` }>
91
+ <button
92
+ type="button"
93
+ className={ `${ classPrefix }__trigger${ isActive ? ' is-active' : '' }` }
94
+ aria-expanded={ isActive }
95
+ onClick={ () => onChange( tab.id ) }
96
+ >
97
+ <span>{ tab.label }</span>
98
+ <span className={ `${ classPrefix }__arrow` } aria-hidden="true">
99
+ { isActive ? '▲' : '▼' }
100
+ </span>
101
+ </button>
102
+ { isActive && <div className={ `${ classPrefix }__panel` }>{ renderPanel( tab.id ) }</div>}
103
+ </div>
104
+ );
105
+ } ) }
106
+ </div>
107
+ </div>
108
+ );
109
+ }
package/src/index.tsx CHANGED
@@ -9,6 +9,7 @@ export { Pagination, type PaginationProps } from './Pagination.tsx';
9
9
  export { SearchBox, type SearchBoxProps } from './SearchBox.tsx';
10
10
  export { Modal, type ModalProps } from './Modal.tsx';
11
11
  export { Tabs, type TabsProps, type TabItem } from './Tabs.tsx';
12
+ export { ResponsiveTabs, type ResponsiveTabsProps } from './ResponsiveTabs.tsx';
12
13
  export { ShellTabs, type ShellTabsProps } from './ShellTabs.tsx';
13
14
  export { Panel, type PanelProps } from './Panel.tsx';
14
15
  export { Surface, type SurfaceProps } from './Surface.tsx';
@@ -144,6 +144,139 @@
144
144
  padding-bottom: 0;
145
145
  }
146
146
 
147
+ .ec-responsive-tabs {
148
+ display: grid;
149
+ gap: var(--spacing-md, 1rem);
150
+ }
151
+
152
+ .ec-responsive-tabs__desktop-panel {
153
+ display: grid;
154
+ background: var(--background-color, #fff);
155
+ border: 1px solid var(--border-color, #ddd);
156
+ border-radius: 0 var(--border-radius-md, 8px) var(--border-radius-md, 8px) var(--border-radius-md, 8px);
157
+ padding: var(--spacing-lg, 1.5rem) var(--spacing-md, 1rem);
158
+ margin-bottom: var(--spacing-sm, 0.5rem);
159
+ position: relative;
160
+ z-index: 1;
161
+ box-sizing: border-box;
162
+ }
163
+
164
+ .ec-responsive-tabs__accordion {
165
+ display: grid;
166
+ gap: var(--spacing-md, 1rem);
167
+ }
168
+
169
+ .ec-responsive-tabs__item {
170
+ border: 1px solid var(--border-color, #ddd);
171
+ border-radius: var(--border-radius-md, 8px);
172
+ margin-bottom: var(--spacing-md, 1rem);
173
+ overflow: hidden;
174
+ background: var(--card-background, #f8f8f8);
175
+ box-sizing: border-box;
176
+ }
177
+
178
+ .ec-responsive-tabs__item:last-of-type {
179
+ margin-bottom: 0;
180
+ }
181
+
182
+ .ec-responsive-tabs__trigger {
183
+ width: 100%;
184
+ display: flex;
185
+ justify-content: space-between;
186
+ align-items: center;
187
+ gap: var(--spacing-sm, 0.5rem);
188
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
189
+ border: 0;
190
+ border-bottom: 1px solid var(--border-color, #ddd);
191
+ background: var(--card-background, #f8f8f8);
192
+ color: var(--text-color, #111);
193
+ font-family: var(--font-family-body, inherit);
194
+ font-size: var(--font-size-body, 1.125rem);
195
+ font-weight: 600;
196
+ line-height: 1.4;
197
+ cursor: pointer;
198
+ text-align: left;
199
+ transition: background 0.15s ease, color 0.15s ease;
200
+ }
201
+
202
+ .ec-responsive-tabs__item:last-of-type .ec-responsive-tabs__trigger:not(.is-active) {
203
+ border-bottom: 0;
204
+ }
205
+
206
+ .ec-responsive-tabs__trigger.is-active {
207
+ background: var(--background-color, #fff);
208
+ color: var(--link-color, #0b5394);
209
+ }
210
+
211
+ .ec-responsive-tabs__trigger:hover {
212
+ background: var(--background-color, #fff);
213
+ color: var(--link-color, #0b5394);
214
+ }
215
+
216
+ .ec-responsive-tabs__trigger:focus-visible {
217
+ outline: 2px solid var(--focus-border-color, #53940b);
218
+ outline-offset: -2px;
219
+ box-shadow: inset 0 0 0 1px var(--focus-border-color, #53940b);
220
+ }
221
+
222
+ .ec-responsive-tabs__panel {
223
+ padding: var(--spacing-sm, 0.5rem);
224
+ background: var(--background-color, #fff);
225
+ margin-bottom: 0;
226
+ }
227
+
228
+ .ec-responsive-tabs__panel > .ec-panel:first-child {
229
+ margin-top: 0;
230
+ }
231
+
232
+ .ec-responsive-tabs__arrow {
233
+ flex-shrink: 0;
234
+ font-size: var(--font-size-sm, 0.875rem);
235
+ margin-left: var(--spacing-sm, 0.5rem);
236
+ color: currentColor;
237
+ }
238
+
239
+ @media (min-width: 769px) {
240
+ .ec-responsive-tabs__accordion {
241
+ display: none;
242
+ }
243
+
244
+ .ec-responsive-tabs .ec-tabs__tabs {
245
+ display: flex;
246
+ flex-wrap: wrap;
247
+ margin-bottom: -1px;
248
+ position: relative;
249
+ }
250
+
251
+ .ec-responsive-tabs .ec-tabs__tab {
252
+ border: 1px solid var(--border-color, #ddd);
253
+ border-bottom: 1px solid var(--border-color, #ddd);
254
+ border-radius: var(--border-radius-md, 8px) var(--border-radius-md, 8px) 0 0;
255
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
256
+ font-size: var(--font-size-base, 1rem);
257
+ position: relative;
258
+ top: 1px;
259
+ margin-right: var(--spacing-xs, 0.25rem);
260
+ background: var(--card-background, #f8f8f8);
261
+ color: var(--text-color, #111);
262
+ z-index: 2;
263
+ }
264
+
265
+ .ec-responsive-tabs .ec-tabs__tab.is-active {
266
+ background: var(--background-color, #fff);
267
+ color: var(--link-color, #0b5394);
268
+ border-bottom: 1px solid var(--background-color, #fff);
269
+ z-index: 3;
270
+ }
271
+ }
272
+
273
+ @media (max-width: 768px) {
274
+ .ec-responsive-tabs .ec-tabs__tabs,
275
+ .ec-responsive-tabs__desktop-panel {
276
+ display: none;
277
+ }
278
+ }
279
+
147
280
  // Panel
148
281
  .ec-panel {
149
282
  border: 1px solid var(--border-color, #ddd);