juxscript 1.1.31 → 1.1.32

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.
@@ -1,8 +1,8 @@
1
- import { BaseComponent } from './base/BaseComponent.js';
1
+ import { BaseComponent, BaseState } from './base/BaseComponent.js';
2
2
  export interface Tab {
3
3
  id: string;
4
4
  label: string;
5
- content: string | HTMLElement;
5
+ content: string | HTMLElement | BaseComponent<any>;
6
6
  icon?: string;
7
7
  }
8
8
  export interface TabsOptions {
@@ -12,25 +12,38 @@ export interface TabsOptions {
12
12
  style?: string;
13
13
  class?: string;
14
14
  }
15
- type TabsState = {
15
+ type TabsState = BaseState & {
16
16
  tabs: Tab[];
17
17
  activeTab: string;
18
18
  variant: string;
19
- style: string;
20
- class: string;
21
19
  };
22
20
  export declare class Tabs extends BaseComponent<TabsState> {
21
+ private _tabsWrapper;
23
22
  constructor(id: string, options?: TabsOptions);
24
23
  protected getTriggerEvents(): readonly string[];
25
24
  protected getCallbackEvents(): readonly string[];
25
+ update(prop: string, value: any): void;
26
+ private _updateActiveTab;
27
+ private _rebuildTabs;
28
+ /**
29
+ * ✅ NEW: Smart content rendering
30
+ * Handles strings, DOM elements, and component instances
31
+ */
32
+ private _renderTabContent;
26
33
  tabs(value: Tab[]): this;
27
34
  addTab(tab: Tab): this;
35
+ removeTab(tabId: string): this;
28
36
  activeTab(value: string): this;
29
37
  variant(value: 'default' | 'pills' | 'underline'): this;
30
- private _updateActiveTab;
31
- update(prop: string, value: any): void;
38
+ /**
39
+ * NEW: Update content of a specific tab
40
+ */
41
+ updateTabContent(tabId: string, content: string | HTMLElement | BaseComponent<any>): this;
42
+ /**
43
+ * ✅ NEW: Get current tab
44
+ */
45
+ getCurrentTab(): Tab | undefined;
32
46
  render(targetId?: string): this;
33
- private _switchTab;
34
47
  }
35
48
  export declare function tabs(id: string, options?: TabsOptions): Tabs;
36
49
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAMxD,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,qBAAa,IAAK,SAAQ,aAAa,CAAC,SAAS,CAAC;gBACpC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB;IAUjD,SAAS,CAAC,gBAAgB,IAAI,SAAS,MAAM,EAAE;IAI/C,SAAS,CAAC,iBAAiB,IAAI,SAAS,MAAM,EAAE;IAUhD,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,IAAI;IAKxB,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI;IAKtB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM9B,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,IAAI;IAKvD,OAAO,CAAC,gBAAgB;IAgBxB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAQtC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAwI/B,OAAO,CAAC,UAAU;CAcnB;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAEhE"}
1
+ {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAMnE,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,SAAS,GAAG,SAAS,GAAG;IAC3B,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,IAAK,SAAQ,aAAa,CAAC,SAAS,CAAC;IAChD,OAAO,CAAC,YAAY,CAA4B;gBAEpC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB;IAUjD,SAAS,CAAC,gBAAgB,IAAI,SAAS,MAAM,EAAE;IAI/C,SAAS,CAAC,iBAAiB,IAAI,SAAS,MAAM,EAAE;IAQhD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAuBtC,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,YAAY;IAuDpB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAkBzB,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,IAAI;IAKxB,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI;IAKtB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAW9B,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK9B,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,IAAI;IAKvD;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,IAAI;IAkBzF;;OAEG;IACH,aAAa,IAAI,GAAG,GAAG,SAAS;IAQhC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;CAqEhC;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAEhE"}
@@ -11,6 +11,7 @@ export class Tabs extends BaseComponent {
11
11
  style: options.style ?? '',
12
12
  class: options.class ?? ''
13
13
  });
14
+ this._tabsWrapper = null;
14
15
  }
15
16
  getTriggerEvents() {
16
17
  return TRIGGER_EVENTS;
@@ -18,10 +19,110 @@ export class Tabs extends BaseComponent {
18
19
  getCallbackEvents() {
19
20
  return CALLBACK_EVENTS;
20
21
  }
22
+ /* ═════════════════════════════════════════════════════════════════
23
+ * REACTIVE UPDATE
24
+ * ═════════════════════════════════════════════════════════════════ */
25
+ update(prop, value) {
26
+ super.update(prop, value); // ✅ Handle base properties
27
+ if (!this._tabsWrapper)
28
+ return;
29
+ switch (prop) {
30
+ case 'activeTab':
31
+ this._updateActiveTab(value);
32
+ break;
33
+ case 'tabs':
34
+ this._rebuildTabs();
35
+ break;
36
+ case 'variant':
37
+ this._tabsWrapper.className = `jux-tabs jux-tabs-${value}`;
38
+ if (this.state.class) {
39
+ this._tabsWrapper.className += ` ${this.state.class}`;
40
+ }
41
+ break;
42
+ }
43
+ }
44
+ _updateActiveTab(tabId) {
45
+ if (!this._tabsWrapper)
46
+ return;
47
+ // Update button states
48
+ this._tabsWrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
49
+ btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === tabId);
50
+ });
51
+ // Update panel visibility
52
+ this._tabsWrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
53
+ const isActive = panel.getAttribute('data-tab') === tabId;
54
+ panel.classList.toggle('jux-tabs-panel-active', isActive);
55
+ panel.style.display = isActive ? 'block' : 'none';
56
+ });
57
+ }
58
+ _rebuildTabs() {
59
+ if (!this._tabsWrapper)
60
+ return;
61
+ const tabList = this._tabsWrapper.querySelector('.jux-tabs-list');
62
+ const tabPanels = this._tabsWrapper.querySelector('.jux-tabs-panels');
63
+ if (!tabList || !tabPanels)
64
+ return;
65
+ // Clear existing
66
+ tabList.innerHTML = '';
67
+ tabPanels.innerHTML = '';
68
+ // Rebuild
69
+ this.state.tabs.forEach(tab => {
70
+ // Button
71
+ const tabButton = document.createElement('button');
72
+ tabButton.className = 'jux-tabs-button';
73
+ tabButton.setAttribute('data-tab', tab.id);
74
+ if (tab.id === this.state.activeTab) {
75
+ tabButton.classList.add('jux-tabs-button-active');
76
+ }
77
+ if (tab.icon) {
78
+ const icon = document.createElement('span');
79
+ icon.className = 'jux-tabs-icon';
80
+ icon.textContent = tab.icon;
81
+ tabButton.appendChild(icon);
82
+ }
83
+ tabButton.appendChild(document.createTextNode(tab.label));
84
+ tabButton.addEventListener('click', () => {
85
+ this.state.activeTab = tab.id;
86
+ this._triggerCallback('tabChange', tab.id);
87
+ });
88
+ tabList.appendChild(tabButton);
89
+ // Panel
90
+ const tabPanel = document.createElement('div');
91
+ tabPanel.className = 'jux-tabs-panel';
92
+ tabPanel.setAttribute('data-tab', tab.id);
93
+ if (tab.id === this.state.activeTab) {
94
+ tabPanel.classList.add('jux-tabs-panel-active');
95
+ }
96
+ else {
97
+ tabPanel.style.display = 'none';
98
+ }
99
+ // ✅ Render content (supports strings, DOM, and components)
100
+ this._renderTabContent(tabPanel, tab.content);
101
+ tabPanels.appendChild(tabPanel);
102
+ });
103
+ }
104
+ /**
105
+ * ✅ NEW: Smart content rendering
106
+ * Handles strings, DOM elements, and component instances
107
+ */
108
+ _renderTabContent(panel, content) {
109
+ if (typeof content === 'string') {
110
+ panel.innerHTML = content;
111
+ }
112
+ else if (content instanceof HTMLElement) {
113
+ panel.appendChild(content);
114
+ }
115
+ else if (content && typeof content.render === 'function') {
116
+ // It's a component instance - create a container and render into it
117
+ const componentContainer = document.createElement('div');
118
+ componentContainer.id = `${panel.getAttribute('data-tab')}-content`;
119
+ panel.appendChild(componentContainer);
120
+ content.render(`#${componentContainer.id}`);
121
+ }
122
+ }
21
123
  /* ═════════════════════════════════════════════════════════════════
22
124
  * FLUENT API
23
125
  * ═════════════════════════════════════════════════════════════════ */
24
- // ✅ Inherited from BaseComponent
25
126
  tabs(value) {
26
127
  this.state.tabs = value;
27
128
  return this;
@@ -30,32 +131,45 @@ export class Tabs extends BaseComponent {
30
131
  this.state.tabs = [...this.state.tabs, tab];
31
132
  return this;
32
133
  }
134
+ removeTab(tabId) {
135
+ this.state.tabs = this.state.tabs.filter(t => t.id !== tabId);
136
+ // If removing active tab, switch to first tab
137
+ if (this.state.activeTab === tabId && this.state.tabs.length > 0) {
138
+ this.state.activeTab = this.state.tabs[0].id;
139
+ }
140
+ return this;
141
+ }
33
142
  activeTab(value) {
34
143
  this.state.activeTab = value;
35
- this._updateActiveTab();
36
144
  return this;
37
145
  }
38
146
  variant(value) {
39
147
  this.state.variant = value;
40
148
  return this;
41
149
  }
42
- _updateActiveTab() {
43
- if (!this.container)
44
- return;
45
- const wrapper = this.container.querySelector(`#${this._id}`);
46
- if (!wrapper)
47
- return;
48
- wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
49
- btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === this.state.activeTab);
50
- });
51
- wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
52
- const isActive = panel.getAttribute('data-tab') === this.state.activeTab;
53
- panel.classList.toggle('jux-tabs-panel-active', isActive);
54
- panel.style.display = isActive ? 'block' : 'none';
55
- });
150
+ /**
151
+ * ✅ NEW: Update content of a specific tab
152
+ */
153
+ updateTabContent(tabId, content) {
154
+ const tab = this.state.tabs.find(t => t.id === tabId);
155
+ if (!tab)
156
+ return this;
157
+ tab.content = content;
158
+ // Update DOM if tab is rendered
159
+ if (this._tabsWrapper) {
160
+ const panel = this._tabsWrapper.querySelector(`.jux-tabs-panel[data-tab="${tabId}"]`);
161
+ if (panel) {
162
+ panel.innerHTML = ''; // Clear existing content
163
+ this._renderTabContent(panel, content);
164
+ }
165
+ }
166
+ return this;
56
167
  }
57
- update(prop, value) {
58
- // No reactive updates needed
168
+ /**
169
+ * NEW: Get current tab
170
+ */
171
+ getCurrentTab() {
172
+ return this.state.tabs.find(t => t.id === this.state.activeTab);
59
173
  }
60
174
  /* ═════════════════════════════════════════════════════════════════
61
175
  * RENDER
@@ -63,7 +177,6 @@ export class Tabs extends BaseComponent {
63
177
  render(targetId) {
64
178
  const container = this._setupContainer(targetId);
65
179
  const { tabs, activeTab, variant, style, class: className } = this.state;
66
- const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
67
180
  const wrapper = document.createElement('div');
68
181
  wrapper.className = `jux-tabs jux-tabs-${variant}`;
69
182
  wrapper.id = this._id;
@@ -76,13 +189,26 @@ export class Tabs extends BaseComponent {
76
189
  const tabPanels = document.createElement('div');
77
190
  tabPanels.className = 'jux-tabs-panels';
78
191
  tabs.forEach(tab => {
192
+ // Tab button
79
193
  const tabButton = document.createElement('button');
80
194
  tabButton.className = 'jux-tabs-button';
81
195
  tabButton.setAttribute('data-tab', tab.id);
82
- if (tab.id === activeTab)
196
+ if (tab.id === activeTab) {
83
197
  tabButton.classList.add('jux-tabs-button-active');
84
- tabButton.textContent = tab.label;
198
+ }
199
+ if (tab.icon) {
200
+ const icon = document.createElement('span');
201
+ icon.className = 'jux-tabs-icon';
202
+ icon.textContent = tab.icon;
203
+ tabButton.appendChild(icon);
204
+ }
205
+ tabButton.appendChild(document.createTextNode(tab.label));
206
+ tabButton.addEventListener('click', () => {
207
+ this.state.activeTab = tab.id;
208
+ this._triggerCallback('tabChange', tab.id);
209
+ });
85
210
  tabList.appendChild(tabButton);
211
+ // Tab panel
86
212
  const tabPanel = document.createElement('div');
87
213
  tabPanel.className = 'jux-tabs-panel';
88
214
  tabPanel.setAttribute('data-tab', tab.id);
@@ -92,109 +218,18 @@ export class Tabs extends BaseComponent {
92
218
  else {
93
219
  tabPanel.style.display = 'none';
94
220
  }
95
- if (typeof tab.content === 'string') {
96
- tabPanel.innerHTML = tab.content;
97
- }
98
- else {
99
- tabPanel.appendChild(tab.content);
100
- }
221
+ // Render content (supports strings, DOM, and components)
222
+ this._renderTabContent(tabPanel, tab.content);
101
223
  tabPanels.appendChild(tabPanel);
102
224
  });
103
225
  wrapper.appendChild(tabList);
104
226
  wrapper.appendChild(tabPanels);
105
- if (!hasActiveTabSync) {
106
- tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
107
- button.addEventListener('click', () => {
108
- const tabId = button.getAttribute('data-tab');
109
- this.state.activeTab = tabId;
110
- this._switchTab(tabId, wrapper);
111
- });
112
- });
113
- }
114
227
  this._wireStandardEvents(wrapper);
115
- this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
116
- if (property === 'activeTab') {
117
- const transformToState = toState || ((v) => String(v));
118
- const transformToComponent = toComponent || ((v) => String(v));
119
- let isUpdating = false;
120
- stateObj.subscribe((val) => {
121
- if (isUpdating)
122
- return;
123
- const transformed = transformToComponent(val);
124
- this.state.activeTab = transformed;
125
- this._switchTab(transformed, wrapper);
126
- });
127
- tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
128
- button.addEventListener('click', () => {
129
- if (isUpdating)
130
- return;
131
- isUpdating = true;
132
- const tabId = button.getAttribute('data-tab');
133
- this.state.activeTab = tabId;
134
- this._switchTab(tabId, wrapper);
135
- const transformed = transformToState(tabId);
136
- stateObj.set(transformed);
137
- setTimeout(() => { isUpdating = false; }, 0);
138
- });
139
- });
140
- }
141
- else if (property === 'tabs') {
142
- const transform = toComponent || ((v) => v);
143
- stateObj.subscribe((val) => {
144
- const transformed = transform(val);
145
- this.state.tabs = transformed;
146
- tabList.innerHTML = '';
147
- tabPanels.innerHTML = '';
148
- transformed.forEach((tab) => {
149
- const tabButton = document.createElement('button');
150
- tabButton.className = 'jux-tabs-button';
151
- tabButton.setAttribute('data-tab', tab.id);
152
- if (tab.id === this.state.activeTab)
153
- tabButton.classList.add('jux-tabs-button-active');
154
- tabButton.textContent = tab.label;
155
- tabList.appendChild(tabButton);
156
- const tabPanel = document.createElement('div');
157
- tabPanel.className = 'jux-tabs-panel';
158
- tabPanel.setAttribute('data-tab', tab.id);
159
- if (tab.id === this.state.activeTab) {
160
- tabPanel.classList.add('jux-tabs-panel-active');
161
- }
162
- else {
163
- tabPanel.style.display = 'none';
164
- }
165
- if (typeof tab.content === 'string') {
166
- tabPanel.innerHTML = tab.content;
167
- }
168
- else {
169
- tabPanel.appendChild(tab.content);
170
- }
171
- tabPanels.appendChild(tabPanel);
172
- });
173
- tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
174
- button.addEventListener('click', () => {
175
- const tabId = button.getAttribute('data-tab');
176
- this.state.activeTab = tabId;
177
- this._switchTab(tabId, wrapper);
178
- });
179
- });
180
- });
181
- }
182
- });
228
+ this._wireAllSyncs();
183
229
  container.appendChild(wrapper);
230
+ this._tabsWrapper = wrapper;
184
231
  return this;
185
232
  }
186
- _switchTab(tabId, wrapper) {
187
- wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
188
- btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === tabId);
189
- });
190
- wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
191
- const isActive = panel.getAttribute('data-tab') === tabId;
192
- panel.classList.toggle('jux-tabs-panel-active', isActive);
193
- panel.style.display = isActive ? 'block' : 'none';
194
- });
195
- // 🎯 Fire the tabChange callback event
196
- this._triggerCallback('tabChange', tabId);
197
- }
198
233
  }
199
234
  export function tabs(id, options = {}) {
200
235
  return new Tabs(id, options);
@@ -1,4 +1,4 @@
1
- import { BaseComponent } from './base/BaseComponent.js';
1
+ import { BaseComponent, BaseState } from './base/BaseComponent.js';
2
2
 
3
3
  // Event definitions
4
4
  const TRIGGER_EVENTS = [] as const;
@@ -7,7 +7,7 @@ const CALLBACK_EVENTS = ['tabChange'] as const;
7
7
  export interface Tab {
8
8
  id: string;
9
9
  label: string;
10
- content: string | HTMLElement;
10
+ content: string | HTMLElement | BaseComponent<any>; // ✅ Support component instances
11
11
  icon?: string;
12
12
  }
13
13
 
@@ -19,15 +19,15 @@ export interface TabsOptions {
19
19
  class?: string;
20
20
  }
21
21
 
22
- type TabsState = {
22
+ type TabsState = BaseState & {
23
23
  tabs: Tab[];
24
24
  activeTab: string;
25
25
  variant: string;
26
- style: string;
27
- class: string;
28
26
  };
29
27
 
30
28
  export class Tabs extends BaseComponent<TabsState> {
29
+ private _tabsWrapper: HTMLElement | null = null;
30
+
31
31
  constructor(id: string, options: TabsOptions = {}) {
32
32
  super(id, {
33
33
  tabs: options.tabs ?? [],
@@ -47,10 +47,124 @@ export class Tabs extends BaseComponent<TabsState> {
47
47
  }
48
48
 
49
49
  /* ═════════════════════════════════════════════════════════════════
50
- * FLUENT API
50
+ * REACTIVE UPDATE
51
51
  * ═════════════════════════════════════════════════════════════════ */
52
52
 
53
- // Inherited from BaseComponent
53
+ update(prop: string, value: any): void {
54
+ super.update(prop, value); // ✅ Handle base properties
55
+
56
+ if (!this._tabsWrapper) return;
57
+
58
+ switch (prop) {
59
+ case 'activeTab':
60
+ this._updateActiveTab(value);
61
+ break;
62
+
63
+ case 'tabs':
64
+ this._rebuildTabs();
65
+ break;
66
+
67
+ case 'variant':
68
+ this._tabsWrapper.className = `jux-tabs jux-tabs-${value}`;
69
+ if (this.state.class) {
70
+ this._tabsWrapper.className += ` ${this.state.class}`;
71
+ }
72
+ break;
73
+ }
74
+ }
75
+
76
+ private _updateActiveTab(tabId: string): void {
77
+ if (!this._tabsWrapper) return;
78
+
79
+ // Update button states
80
+ this._tabsWrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
81
+ btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === tabId);
82
+ });
83
+
84
+ // Update panel visibility
85
+ this._tabsWrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
86
+ const isActive = panel.getAttribute('data-tab') === tabId;
87
+ panel.classList.toggle('jux-tabs-panel-active', isActive);
88
+ (panel as HTMLElement).style.display = isActive ? 'block' : 'none';
89
+ });
90
+ }
91
+
92
+ private _rebuildTabs(): void {
93
+ if (!this._tabsWrapper) return;
94
+
95
+ const tabList = this._tabsWrapper.querySelector('.jux-tabs-list');
96
+ const tabPanels = this._tabsWrapper.querySelector('.jux-tabs-panels');
97
+
98
+ if (!tabList || !tabPanels) return;
99
+
100
+ // Clear existing
101
+ tabList.innerHTML = '';
102
+ tabPanels.innerHTML = '';
103
+
104
+ // Rebuild
105
+ this.state.tabs.forEach(tab => {
106
+ // Button
107
+ const tabButton = document.createElement('button');
108
+ tabButton.className = 'jux-tabs-button';
109
+ tabButton.setAttribute('data-tab', tab.id);
110
+ if (tab.id === this.state.activeTab) {
111
+ tabButton.classList.add('jux-tabs-button-active');
112
+ }
113
+
114
+ if (tab.icon) {
115
+ const icon = document.createElement('span');
116
+ icon.className = 'jux-tabs-icon';
117
+ icon.textContent = tab.icon;
118
+ tabButton.appendChild(icon);
119
+ }
120
+
121
+ tabButton.appendChild(document.createTextNode(tab.label));
122
+ tabButton.addEventListener('click', () => {
123
+ this.state.activeTab = tab.id;
124
+ this._triggerCallback('tabChange', tab.id);
125
+ });
126
+
127
+ tabList.appendChild(tabButton);
128
+
129
+ // Panel
130
+ const tabPanel = document.createElement('div');
131
+ tabPanel.className = 'jux-tabs-panel';
132
+ tabPanel.setAttribute('data-tab', tab.id);
133
+
134
+ if (tab.id === this.state.activeTab) {
135
+ tabPanel.classList.add('jux-tabs-panel-active');
136
+ } else {
137
+ tabPanel.style.display = 'none';
138
+ }
139
+
140
+ // ✅ Render content (supports strings, DOM, and components)
141
+ this._renderTabContent(tabPanel, tab.content);
142
+
143
+ tabPanels.appendChild(tabPanel);
144
+ });
145
+ }
146
+
147
+ /**
148
+ * ✅ NEW: Smart content rendering
149
+ * Handles strings, DOM elements, and component instances
150
+ */
151
+ private _renderTabContent(panel: HTMLElement, content: string | HTMLElement | BaseComponent<any>): void {
152
+ if (typeof content === 'string') {
153
+ panel.innerHTML = content;
154
+ } else if (content instanceof HTMLElement) {
155
+ panel.appendChild(content);
156
+ } else if (content && typeof (content as any).render === 'function') {
157
+ // It's a component instance - create a container and render into it
158
+ const componentContainer = document.createElement('div');
159
+ componentContainer.id = `${panel.getAttribute('data-tab')}-content`;
160
+ panel.appendChild(componentContainer);
161
+ (content as BaseComponent<any>).render(`#${componentContainer.id}`);
162
+ }
163
+ }
164
+
165
+ /* ═════════════════════════════════════════════════════════════════
166
+ * FLUENT API
167
+ * ═════════════════════════════════════════════════════════════════ */
54
168
 
55
169
  tabs(value: Tab[]): this {
56
170
  this.state.tabs = value;
@@ -62,9 +176,19 @@ export class Tabs extends BaseComponent<TabsState> {
62
176
  return this;
63
177
  }
64
178
 
179
+ removeTab(tabId: string): this {
180
+ this.state.tabs = this.state.tabs.filter(t => t.id !== tabId);
181
+
182
+ // If removing active tab, switch to first tab
183
+ if (this.state.activeTab === tabId && this.state.tabs.length > 0) {
184
+ this.state.activeTab = this.state.tabs[0].id;
185
+ }
186
+
187
+ return this;
188
+ }
189
+
65
190
  activeTab(value: string): this {
66
191
  this.state.activeTab = value;
67
- this._updateActiveTab();
68
192
  return this;
69
193
  }
70
194
 
@@ -73,24 +197,32 @@ export class Tabs extends BaseComponent<TabsState> {
73
197
  return this;
74
198
  }
75
199
 
76
- private _updateActiveTab(): void {
77
- if (!this.container) return;
78
- const wrapper = this.container.querySelector(`#${this._id}`);
79
- if (!wrapper) return;
80
-
81
- wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
82
- btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === this.state.activeTab);
83
- });
200
+ /**
201
+ * NEW: Update content of a specific tab
202
+ */
203
+ updateTabContent(tabId: string, content: string | HTMLElement | BaseComponent<any>): this {
204
+ const tab = this.state.tabs.find(t => t.id === tabId);
205
+ if (!tab) return this;
206
+
207
+ tab.content = content;
208
+
209
+ // Update DOM if tab is rendered
210
+ if (this._tabsWrapper) {
211
+ const panel = this._tabsWrapper.querySelector(`.jux-tabs-panel[data-tab="${tabId}"]`);
212
+ if (panel) {
213
+ panel.innerHTML = ''; // Clear existing content
214
+ this._renderTabContent(panel as HTMLElement, content);
215
+ }
216
+ }
84
217
 
85
- wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
86
- const isActive = panel.getAttribute('data-tab') === this.state.activeTab;
87
- panel.classList.toggle('jux-tabs-panel-active', isActive);
88
- (panel as HTMLElement).style.display = isActive ? 'block' : 'none';
89
- });
218
+ return this;
90
219
  }
91
220
 
92
- update(prop: string, value: any): void {
93
- // No reactive updates needed
221
+ /**
222
+ * NEW: Get current tab
223
+ */
224
+ getCurrentTab(): Tab | undefined {
225
+ return this.state.tabs.find(t => t.id === this.state.activeTab);
94
226
  }
95
227
 
96
228
  /* ═════════════════════════════════════════════════════════════════
@@ -101,7 +233,6 @@ export class Tabs extends BaseComponent<TabsState> {
101
233
  const container = this._setupContainer(targetId);
102
234
 
103
235
  const { tabs, activeTab, variant, style, class: className } = this.state;
104
- const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
105
236
 
106
237
  const wrapper = document.createElement('div');
107
238
  wrapper.className = `jux-tabs jux-tabs-${variant}`;
@@ -116,136 +247,56 @@ export class Tabs extends BaseComponent<TabsState> {
116
247
  tabPanels.className = 'jux-tabs-panels';
117
248
 
118
249
  tabs.forEach(tab => {
250
+ // Tab button
119
251
  const tabButton = document.createElement('button');
120
252
  tabButton.className = 'jux-tabs-button';
121
253
  tabButton.setAttribute('data-tab', tab.id);
122
- if (tab.id === activeTab) tabButton.classList.add('jux-tabs-button-active');
123
- tabButton.textContent = tab.label;
254
+ if (tab.id === activeTab) {
255
+ tabButton.classList.add('jux-tabs-button-active');
256
+ }
257
+
258
+ if (tab.icon) {
259
+ const icon = document.createElement('span');
260
+ icon.className = 'jux-tabs-icon';
261
+ icon.textContent = tab.icon;
262
+ tabButton.appendChild(icon);
263
+ }
264
+
265
+ tabButton.appendChild(document.createTextNode(tab.label));
266
+ tabButton.addEventListener('click', () => {
267
+ this.state.activeTab = tab.id;
268
+ this._triggerCallback('tabChange', tab.id);
269
+ });
270
+
124
271
  tabList.appendChild(tabButton);
125
272
 
273
+ // Tab panel
126
274
  const tabPanel = document.createElement('div');
127
275
  tabPanel.className = 'jux-tabs-panel';
128
276
  tabPanel.setAttribute('data-tab', tab.id);
277
+
129
278
  if (tab.id === activeTab) {
130
279
  tabPanel.classList.add('jux-tabs-panel-active');
131
280
  } else {
132
281
  tabPanel.style.display = 'none';
133
282
  }
134
- if (typeof tab.content === 'string') {
135
- tabPanel.innerHTML = tab.content;
136
- } else {
137
- tabPanel.appendChild(tab.content);
138
- }
283
+
284
+ // Render content (supports strings, DOM, and components)
285
+ this._renderTabContent(tabPanel, tab.content);
286
+
139
287
  tabPanels.appendChild(tabPanel);
140
288
  });
141
289
 
142
290
  wrapper.appendChild(tabList);
143
291
  wrapper.appendChild(tabPanels);
144
292
 
145
- if (!hasActiveTabSync) {
146
- tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
147
- button.addEventListener('click', () => {
148
- const tabId = button.getAttribute('data-tab')!;
149
- this.state.activeTab = tabId;
150
- this._switchTab(tabId, wrapper);
151
- });
152
- });
153
- }
154
-
155
293
  this._wireStandardEvents(wrapper);
156
-
157
- this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
158
- if (property === 'activeTab') {
159
- const transformToState = toState || ((v: any) => String(v));
160
- const transformToComponent = toComponent || ((v: any) => String(v));
161
-
162
- let isUpdating = false;
163
-
164
- stateObj.subscribe((val: any) => {
165
- if (isUpdating) return;
166
- const transformed = transformToComponent(val);
167
- this.state.activeTab = transformed;
168
- this._switchTab(transformed, wrapper);
169
- });
170
-
171
- tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
172
- button.addEventListener('click', () => {
173
- if (isUpdating) return;
174
- isUpdating = true;
175
-
176
- const tabId = button.getAttribute('data-tab')!;
177
- this.state.activeTab = tabId;
178
- this._switchTab(tabId, wrapper);
179
-
180
- const transformed = transformToState(tabId);
181
- stateObj.set(transformed);
182
-
183
- setTimeout(() => { isUpdating = false; }, 0);
184
- });
185
- });
186
- }
187
- else if (property === 'tabs') {
188
- const transform = toComponent || ((v: any) => v);
189
-
190
- stateObj.subscribe((val: any) => {
191
- const transformed = transform(val);
192
- this.state.tabs = transformed;
193
-
194
- tabList.innerHTML = '';
195
- tabPanels.innerHTML = '';
196
-
197
- transformed.forEach((tab: any) => {
198
- const tabButton = document.createElement('button');
199
- tabButton.className = 'jux-tabs-button';
200
- tabButton.setAttribute('data-tab', tab.id);
201
- if (tab.id === this.state.activeTab) tabButton.classList.add('jux-tabs-button-active');
202
- tabButton.textContent = tab.label;
203
- tabList.appendChild(tabButton);
204
-
205
- const tabPanel = document.createElement('div');
206
- tabPanel.className = 'jux-tabs-panel';
207
- tabPanel.setAttribute('data-tab', tab.id);
208
- if (tab.id === this.state.activeTab) {
209
- tabPanel.classList.add('jux-tabs-panel-active');
210
- } else {
211
- tabPanel.style.display = 'none';
212
- }
213
- if (typeof tab.content === 'string') {
214
- tabPanel.innerHTML = tab.content;
215
- } else {
216
- tabPanel.appendChild(tab.content);
217
- }
218
- tabPanels.appendChild(tabPanel);
219
- });
220
-
221
- tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
222
- button.addEventListener('click', () => {
223
- const tabId = button.getAttribute('data-tab')!;
224
- this.state.activeTab = tabId;
225
- this._switchTab(tabId, wrapper);
226
- });
227
- });
228
- });
229
- }
230
- });
294
+ this._wireAllSyncs();
231
295
 
232
296
  container.appendChild(wrapper);
233
- return this;
234
- }
235
-
236
- private _switchTab(tabId: string, wrapper: HTMLElement): void {
237
- wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
238
- btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === tabId);
239
- });
297
+ this._tabsWrapper = wrapper;
240
298
 
241
- wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
242
- const isActive = panel.getAttribute('data-tab') === tabId;
243
- panel.classList.toggle('jux-tabs-panel-active', isActive);
244
- (panel as HTMLElement).style.display = isActive ? 'block' : 'none';
245
- });
246
-
247
- // 🎯 Fire the tabChange callback event
248
- this._triggerCallback('tabChange', tabId);
299
+ return this;
249
300
  }
250
301
  }
251
302
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.31",
3
+ "version": "1.1.32",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",