juxscript 1.1.31 → 1.1.33

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;
6
6
  icon?: string;
7
7
  }
8
8
  export interface TabsOptions {
@@ -12,25 +12,37 @@ 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;
26
28
  tabs(value: Tab[]): this;
27
29
  addTab(tab: Tab): this;
30
+ removeTab(tabId: string): this;
28
31
  activeTab(value: string): this;
29
32
  variant(value: 'default' | 'pills' | 'underline'): this;
30
- private _updateActiveTab;
31
- update(prop: string, value: any): void;
33
+ /**
34
+ * Update content of a specific tab by ID
35
+ */
36
+ updateTabContent(tabId: string, content: string): this;
37
+ /**
38
+ * Get tab by ID
39
+ */
40
+ getTab(tabId: string): Tab | undefined;
41
+ /**
42
+ * Get current active tab
43
+ */
44
+ getCurrentTab(): Tab | undefined;
32
45
  render(targetId?: string): this;
33
- private _switchTab;
34
46
  }
35
47
  export declare function tabs(id: string, options?: TabsOptions): Tabs;
36
48
  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,CAAC;IAChB,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;IAsBxB,OAAO,CAAC,YAAY;IA6DpB,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,IAAI;IAetD;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;IAItC;;OAEG;IACH,aAAa,IAAI,GAAG,GAAG,SAAS;IAQhC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;CAuEhC;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,98 @@ 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);
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.state.tabs.forEach(tab => {
49
+ const btn = document.getElementById(`${this._id}-${tab.id}-button`);
50
+ if (btn) {
51
+ btn.classList.toggle('jux-tabs-button-active', tab.id === tabId);
52
+ }
53
+ });
54
+ // Update panel visibility
55
+ this.state.tabs.forEach(tab => {
56
+ const panel = document.getElementById(`${this._id}-${tab.id}-panel`);
57
+ if (panel) {
58
+ const isActive = tab.id === tabId;
59
+ panel.classList.toggle('jux-tabs-panel-active', isActive);
60
+ panel.style.display = isActive ? 'block' : 'none';
61
+ }
62
+ });
63
+ }
64
+ _rebuildTabs() {
65
+ if (!this._tabsWrapper)
66
+ return;
67
+ const tabList = this._tabsWrapper.querySelector('.jux-tabs-list');
68
+ const tabPanels = this._tabsWrapper.querySelector('.jux-tabs-panels');
69
+ if (!tabList || !tabPanels)
70
+ return;
71
+ // Clear existing
72
+ tabList.innerHTML = '';
73
+ tabPanels.innerHTML = '';
74
+ // Rebuild
75
+ this.state.tabs.forEach(tab => {
76
+ // Button with unique ID
77
+ const tabButton = document.createElement('button');
78
+ tabButton.id = `${this._id}-${tab.id}-button`;
79
+ tabButton.className = 'jux-tabs-button';
80
+ tabButton.setAttribute('data-tab-id', tab.id);
81
+ if (tab.id === this.state.activeTab) {
82
+ tabButton.classList.add('jux-tabs-button-active');
83
+ }
84
+ if (tab.icon) {
85
+ const icon = document.createElement('span');
86
+ icon.className = 'jux-tabs-icon';
87
+ icon.textContent = tab.icon;
88
+ tabButton.appendChild(icon);
89
+ }
90
+ tabButton.appendChild(document.createTextNode(tab.label));
91
+ tabButton.addEventListener('click', () => {
92
+ this.state.activeTab = tab.id;
93
+ this._triggerCallback('tabChange', tab.id);
94
+ });
95
+ tabList.appendChild(tabButton);
96
+ // Panel with unique ID
97
+ const tabPanel = document.createElement('div');
98
+ tabPanel.id = `${this._id}-${tab.id}-panel`;
99
+ tabPanel.className = 'jux-tabs-panel';
100
+ tabPanel.setAttribute('data-tab-id', tab.id);
101
+ if (tab.id === this.state.activeTab) {
102
+ tabPanel.classList.add('jux-tabs-panel-active');
103
+ }
104
+ else {
105
+ tabPanel.style.display = 'none';
106
+ }
107
+ tabPanel.innerHTML = tab.content;
108
+ tabPanels.appendChild(tabPanel);
109
+ });
110
+ }
21
111
  /* ═════════════════════════════════════════════════════════════════
22
112
  * FLUENT API
23
113
  * ═════════════════════════════════════════════════════════════════ */
24
- // ✅ Inherited from BaseComponent
25
114
  tabs(value) {
26
115
  this.state.tabs = value;
27
116
  return this;
@@ -30,32 +119,48 @@ export class Tabs extends BaseComponent {
30
119
  this.state.tabs = [...this.state.tabs, tab];
31
120
  return this;
32
121
  }
122
+ removeTab(tabId) {
123
+ this.state.tabs = this.state.tabs.filter(t => t.id !== tabId);
124
+ // If removing active tab, switch to first tab
125
+ if (this.state.activeTab === tabId && this.state.tabs.length > 0) {
126
+ this.state.activeTab = this.state.tabs[0].id;
127
+ }
128
+ return this;
129
+ }
33
130
  activeTab(value) {
34
131
  this.state.activeTab = value;
35
- this._updateActiveTab();
36
132
  return this;
37
133
  }
38
134
  variant(value) {
39
135
  this.state.variant = value;
40
136
  return this;
41
137
  }
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
- });
138
+ /**
139
+ * Update content of a specific tab by ID
140
+ */
141
+ updateTabContent(tabId, content) {
142
+ const tab = this.state.tabs.find(t => t.id === tabId);
143
+ if (!tab)
144
+ return this;
145
+ tab.content = content;
146
+ // Update DOM if rendered
147
+ const panel = document.getElementById(`${this._id}-${tabId}-panel`);
148
+ if (panel) {
149
+ panel.innerHTML = content;
150
+ }
151
+ return this;
56
152
  }
57
- update(prop, value) {
58
- // No reactive updates needed
153
+ /**
154
+ * Get tab by ID
155
+ */
156
+ getTab(tabId) {
157
+ return this.state.tabs.find(t => t.id === tabId);
158
+ }
159
+ /**
160
+ * Get current active tab
161
+ */
162
+ getCurrentTab() {
163
+ return this.state.tabs.find(t => t.id === this.state.activeTab);
59
164
  }
60
165
  /* ═════════════════════════════════════════════════════════════════
61
166
  * RENDER
@@ -63,7 +168,6 @@ export class Tabs extends BaseComponent {
63
168
  render(targetId) {
64
169
  const container = this._setupContainer(targetId);
65
170
  const { tabs, activeTab, variant, style, class: className } = this.state;
66
- const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
67
171
  const wrapper = document.createElement('div');
68
172
  wrapper.className = `jux-tabs jux-tabs-${variant}`;
69
173
  wrapper.id = this._id;
@@ -76,125 +180,48 @@ export class Tabs extends BaseComponent {
76
180
  const tabPanels = document.createElement('div');
77
181
  tabPanels.className = 'jux-tabs-panels';
78
182
  tabs.forEach(tab => {
183
+ // Tab button with unique ID
79
184
  const tabButton = document.createElement('button');
185
+ tabButton.id = `${this._id}-${tab.id}-button`;
80
186
  tabButton.className = 'jux-tabs-button';
81
- tabButton.setAttribute('data-tab', tab.id);
82
- if (tab.id === activeTab)
187
+ tabButton.setAttribute('data-tab-id', tab.id);
188
+ if (tab.id === activeTab) {
83
189
  tabButton.classList.add('jux-tabs-button-active');
84
- tabButton.textContent = tab.label;
190
+ }
191
+ if (tab.icon) {
192
+ const icon = document.createElement('span');
193
+ icon.className = 'jux-tabs-icon';
194
+ icon.textContent = tab.icon;
195
+ tabButton.appendChild(icon);
196
+ }
197
+ tabButton.appendChild(document.createTextNode(tab.label));
198
+ tabButton.addEventListener('click', () => {
199
+ this.state.activeTab = tab.id;
200
+ this._triggerCallback('tabChange', tab.id);
201
+ });
85
202
  tabList.appendChild(tabButton);
203
+ // Tab panel with unique ID
86
204
  const tabPanel = document.createElement('div');
205
+ tabPanel.id = `${this._id}-${tab.id}-panel`;
87
206
  tabPanel.className = 'jux-tabs-panel';
88
- tabPanel.setAttribute('data-tab', tab.id);
207
+ tabPanel.setAttribute('data-tab-id', tab.id);
89
208
  if (tab.id === activeTab) {
90
209
  tabPanel.classList.add('jux-tabs-panel-active');
91
210
  }
92
211
  else {
93
212
  tabPanel.style.display = 'none';
94
213
  }
95
- if (typeof tab.content === 'string') {
96
- tabPanel.innerHTML = tab.content;
97
- }
98
- else {
99
- tabPanel.appendChild(tab.content);
100
- }
214
+ tabPanel.innerHTML = tab.content;
101
215
  tabPanels.appendChild(tabPanel);
102
216
  });
103
217
  wrapper.appendChild(tabList);
104
218
  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
219
  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
- });
220
+ this._wireAllSyncs();
183
221
  container.appendChild(wrapper);
222
+ this._tabsWrapper = wrapper;
184
223
  return this;
185
224
  }
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
225
  }
199
226
  export function tabs(id, options = {}) {
200
227
  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; // ✅ Only static HTML strings for now
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,114 @@ 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);
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.state.tabs.forEach(tab => {
81
+ const btn = document.getElementById(`${this._id}-${tab.id}-button`);
82
+ if (btn) {
83
+ btn.classList.toggle('jux-tabs-button-active', tab.id === tabId);
84
+ }
85
+ });
86
+
87
+ // Update panel visibility
88
+ this.state.tabs.forEach(tab => {
89
+ const panel = document.getElementById(`${this._id}-${tab.id}-panel`);
90
+ if (panel) {
91
+ const isActive = tab.id === tabId;
92
+ panel.classList.toggle('jux-tabs-panel-active', isActive);
93
+ panel.style.display = isActive ? 'block' : 'none';
94
+ }
95
+ });
96
+ }
97
+
98
+ private _rebuildTabs(): void {
99
+ if (!this._tabsWrapper) return;
100
+
101
+ const tabList = this._tabsWrapper.querySelector('.jux-tabs-list');
102
+ const tabPanels = this._tabsWrapper.querySelector('.jux-tabs-panels');
103
+
104
+ if (!tabList || !tabPanels) return;
105
+
106
+ // Clear existing
107
+ tabList.innerHTML = '';
108
+ tabPanels.innerHTML = '';
109
+
110
+ // Rebuild
111
+ this.state.tabs.forEach(tab => {
112
+ // Button with unique ID
113
+ const tabButton = document.createElement('button');
114
+ tabButton.id = `${this._id}-${tab.id}-button`;
115
+ tabButton.className = 'jux-tabs-button';
116
+ tabButton.setAttribute('data-tab-id', tab.id);
117
+
118
+ if (tab.id === this.state.activeTab) {
119
+ tabButton.classList.add('jux-tabs-button-active');
120
+ }
121
+
122
+ if (tab.icon) {
123
+ const icon = document.createElement('span');
124
+ icon.className = 'jux-tabs-icon';
125
+ icon.textContent = tab.icon;
126
+ tabButton.appendChild(icon);
127
+ }
128
+
129
+ tabButton.appendChild(document.createTextNode(tab.label));
130
+
131
+ tabButton.addEventListener('click', () => {
132
+ this.state.activeTab = tab.id;
133
+ this._triggerCallback('tabChange', tab.id);
134
+ });
135
+
136
+ tabList.appendChild(tabButton);
137
+
138
+ // Panel with unique ID
139
+ const tabPanel = document.createElement('div');
140
+ tabPanel.id = `${this._id}-${tab.id}-panel`;
141
+ tabPanel.className = 'jux-tabs-panel';
142
+ tabPanel.setAttribute('data-tab-id', tab.id);
143
+
144
+ if (tab.id === this.state.activeTab) {
145
+ tabPanel.classList.add('jux-tabs-panel-active');
146
+ } else {
147
+ tabPanel.style.display = 'none';
148
+ }
149
+
150
+ tabPanel.innerHTML = tab.content;
151
+ tabPanels.appendChild(tabPanel);
152
+ });
153
+ }
154
+
155
+ /* ═════════════════════════════════════════════════════════════════
156
+ * FLUENT API
157
+ * ═════════════════════════════════════════════════════════════════ */
54
158
 
55
159
  tabs(value: Tab[]): this {
56
160
  this.state.tabs = value;
@@ -62,9 +166,19 @@ export class Tabs extends BaseComponent<TabsState> {
62
166
  return this;
63
167
  }
64
168
 
169
+ removeTab(tabId: string): this {
170
+ this.state.tabs = this.state.tabs.filter(t => t.id !== tabId);
171
+
172
+ // If removing active tab, switch to first tab
173
+ if (this.state.activeTab === tabId && this.state.tabs.length > 0) {
174
+ this.state.activeTab = this.state.tabs[0].id;
175
+ }
176
+
177
+ return this;
178
+ }
179
+
65
180
  activeTab(value: string): this {
66
181
  this.state.activeTab = value;
67
- this._updateActiveTab();
68
182
  return this;
69
183
  }
70
184
 
@@ -73,24 +187,36 @@ export class Tabs extends BaseComponent<TabsState> {
73
187
  return this;
74
188
  }
75
189
 
76
- private _updateActiveTab(): void {
77
- if (!this.container) return;
78
- const wrapper = this.container.querySelector(`#${this._id}`);
79
- if (!wrapper) return;
190
+ /**
191
+ * Update content of a specific tab by ID
192
+ */
193
+ updateTabContent(tabId: string, content: string): this {
194
+ const tab = this.state.tabs.find(t => t.id === tabId);
195
+ if (!tab) return this;
80
196
 
81
- wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
82
- btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === this.state.activeTab);
83
- });
197
+ tab.content = content;
84
198
 
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
- });
199
+ // Update DOM if rendered
200
+ const panel = document.getElementById(`${this._id}-${tabId}-panel`);
201
+ if (panel) {
202
+ panel.innerHTML = content;
203
+ }
204
+
205
+ return this;
90
206
  }
91
207
 
92
- update(prop: string, value: any): void {
93
- // No reactive updates needed
208
+ /**
209
+ * Get tab by ID
210
+ */
211
+ getTab(tabId: string): Tab | undefined {
212
+ return this.state.tabs.find(t => t.id === tabId);
213
+ }
214
+
215
+ /**
216
+ * Get current active tab
217
+ */
218
+ getCurrentTab(): Tab | undefined {
219
+ return this.state.tabs.find(t => t.id === this.state.activeTab);
94
220
  }
95
221
 
96
222
  /* ═════════════════════════════════════════════════════════════════
@@ -101,7 +227,6 @@ export class Tabs extends BaseComponent<TabsState> {
101
227
  const container = this._setupContainer(targetId);
102
228
 
103
229
  const { tabs, activeTab, variant, style, class: className } = this.state;
104
- const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
105
230
 
106
231
  const wrapper = document.createElement('div');
107
232
  wrapper.className = `jux-tabs jux-tabs-${variant}`;
@@ -116,136 +241,58 @@ export class Tabs extends BaseComponent<TabsState> {
116
241
  tabPanels.className = 'jux-tabs-panels';
117
242
 
118
243
  tabs.forEach(tab => {
244
+ // Tab button with unique ID
119
245
  const tabButton = document.createElement('button');
246
+ tabButton.id = `${this._id}-${tab.id}-button`;
120
247
  tabButton.className = 'jux-tabs-button';
121
- tabButton.setAttribute('data-tab', tab.id);
122
- if (tab.id === activeTab) tabButton.classList.add('jux-tabs-button-active');
123
- tabButton.textContent = tab.label;
248
+ tabButton.setAttribute('data-tab-id', tab.id);
249
+
250
+ if (tab.id === activeTab) {
251
+ tabButton.classList.add('jux-tabs-button-active');
252
+ }
253
+
254
+ if (tab.icon) {
255
+ const icon = document.createElement('span');
256
+ icon.className = 'jux-tabs-icon';
257
+ icon.textContent = tab.icon;
258
+ tabButton.appendChild(icon);
259
+ }
260
+
261
+ tabButton.appendChild(document.createTextNode(tab.label));
262
+
263
+ tabButton.addEventListener('click', () => {
264
+ this.state.activeTab = tab.id;
265
+ this._triggerCallback('tabChange', tab.id);
266
+ });
267
+
124
268
  tabList.appendChild(tabButton);
125
269
 
270
+ // Tab panel with unique ID
126
271
  const tabPanel = document.createElement('div');
272
+ tabPanel.id = `${this._id}-${tab.id}-panel`;
127
273
  tabPanel.className = 'jux-tabs-panel';
128
- tabPanel.setAttribute('data-tab', tab.id);
274
+ tabPanel.setAttribute('data-tab-id', tab.id);
275
+
129
276
  if (tab.id === activeTab) {
130
277
  tabPanel.classList.add('jux-tabs-panel-active');
131
278
  } else {
132
279
  tabPanel.style.display = 'none';
133
280
  }
134
- if (typeof tab.content === 'string') {
135
- tabPanel.innerHTML = tab.content;
136
- } else {
137
- tabPanel.appendChild(tab.content);
138
- }
281
+
282
+ tabPanel.innerHTML = tab.content;
139
283
  tabPanels.appendChild(tabPanel);
140
284
  });
141
285
 
142
286
  wrapper.appendChild(tabList);
143
287
  wrapper.appendChild(tabPanels);
144
288
 
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
289
  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
- });
290
+ this._wireAllSyncs();
231
291
 
232
292
  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
- });
293
+ this._tabsWrapper = wrapper;
240
294
 
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);
295
+ return this;
249
296
  }
250
297
  }
251
298
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.31",
3
+ "version": "1.1.33",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",