@extrachill/components 0.4.8 → 0.4.9

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.9] - 2026-03-25
4
+
5
+ ### Changed
6
+ - Add responsive mobile accordion mode for shared tabs
7
+
3
8
  ## [0.4.8] - 2026-03-25
4
9
 
5
10
  ### 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.9",
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,63 @@
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
+ }
155
+
156
+ .ec-responsive-tabs__accordion {
157
+ display: grid;
158
+ gap: var(--spacing-md, 1rem);
159
+ }
160
+
161
+ .ec-responsive-tabs__item {
162
+ border: 1px solid var(--border-color, #ddd);
163
+ border-radius: var(--border-radius-md, 6px);
164
+ overflow: hidden;
165
+ background: var(--card-background, #f8f8f8);
166
+ }
167
+
168
+ .ec-responsive-tabs__trigger {
169
+ width: 100%;
170
+ display: flex;
171
+ justify-content: space-between;
172
+ align-items: center;
173
+ gap: var(--spacing-sm, 0.5rem);
174
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
175
+ border: 0;
176
+ border-bottom: 1px solid var(--border-color, #ddd);
177
+ background: var(--card-background, #f8f8f8);
178
+ color: var(--text-color, #111);
179
+ font: inherit;
180
+ font-weight: 600;
181
+ cursor: pointer;
182
+ text-align: left;
183
+ }
184
+
185
+ .ec-responsive-tabs__trigger:not(.is-active) {
186
+ border-bottom-color: transparent;
187
+ }
188
+
189
+ .ec-responsive-tabs__trigger.is-active {
190
+ background: var(--background-color, #fff);
191
+ color: var(--link-color, #0b5394);
192
+ }
193
+
194
+ .ec-responsive-tabs__panel {
195
+ padding: var(--spacing-sm, 0.5rem);
196
+ background: var(--background-color, #fff);
197
+ }
198
+
199
+ .ec-responsive-tabs__arrow {
200
+ flex-shrink: 0;
201
+ font-size: var(--font-size-sm, 0.875rem);
202
+ }
203
+
147
204
  // Panel
148
205
  .ec-panel {
149
206
  border: 1px solid var(--border-color, #ddd);