@finggujadhav/js-helper 0.9.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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Focus Trap Utility
3
+ * Ensures tab focus remains within a specific container.
4
+ */
5
+ declare const focusTrap: (container: HTMLElement) => () => void;
6
+
7
+ declare const lockScroll: () => void;
8
+ declare const unlockScroll: () => void;
9
+
10
+ /**
11
+ * ARIA Helper
12
+ * Managed attribute toggling for accessibility.
13
+ */
14
+ declare const toggleAria: (element: HTMLElement, attribute: string, force?: boolean) => boolean;
15
+ declare const setAriaHidden: (selector: string, isHidden: boolean) => void;
16
+
17
+ declare const onToggle: (type: string, callback: (data: any) => void) => void;
18
+ declare const initEvents: () => void;
19
+
20
+ /**
21
+ * Tabs Helper
22
+ * Handles accessible keyboard navigation and state management for tabs.
23
+ */
24
+ declare const initTabs: () => void;
25
+ declare const activateTab: (tab: HTMLElement) => void;
26
+
27
+ /**
28
+ * Dropdown Helper
29
+ * Handles accessible keyboard navigation, outside-click detection, and state management.
30
+ */
31
+ declare const initDropdowns: () => void;
32
+
33
+ /**
34
+ * FingguFlux Theme Helper
35
+ * Provides runtime logic for theme switching and system preference synchronization.
36
+ */
37
+ type FingguTheme = 'light' | 'dark' | 'system';
38
+ /**
39
+ * Apply a theme to the document or a specific element.
40
+ */
41
+ declare function setTheme(theme: FingguTheme, target?: HTMLElement): void;
42
+ /**
43
+ * Watch for system theme changes and sync (if in system mode).
44
+ */
45
+ declare function watchSystemTheme(callback: (isDark: boolean) => void): () => void;
46
+ /**
47
+ * Get the current effective theme.
48
+ */
49
+ declare function getEffectiveTheme(target?: HTMLElement): 'light' | 'dark';
50
+
51
+ export { type FingguTheme, activateTab, focusTrap, getEffectiveTheme, initDropdowns, initEvents, initTabs, lockScroll, onToggle, setAriaHidden, setTheme, toggleAria, unlockScroll, watchSystemTheme };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Focus Trap Utility
3
+ * Ensures tab focus remains within a specific container.
4
+ */
5
+ declare const focusTrap: (container: HTMLElement) => () => void;
6
+
7
+ declare const lockScroll: () => void;
8
+ declare const unlockScroll: () => void;
9
+
10
+ /**
11
+ * ARIA Helper
12
+ * Managed attribute toggling for accessibility.
13
+ */
14
+ declare const toggleAria: (element: HTMLElement, attribute: string, force?: boolean) => boolean;
15
+ declare const setAriaHidden: (selector: string, isHidden: boolean) => void;
16
+
17
+ declare const onToggle: (type: string, callback: (data: any) => void) => void;
18
+ declare const initEvents: () => void;
19
+
20
+ /**
21
+ * Tabs Helper
22
+ * Handles accessible keyboard navigation and state management for tabs.
23
+ */
24
+ declare const initTabs: () => void;
25
+ declare const activateTab: (tab: HTMLElement) => void;
26
+
27
+ /**
28
+ * Dropdown Helper
29
+ * Handles accessible keyboard navigation, outside-click detection, and state management.
30
+ */
31
+ declare const initDropdowns: () => void;
32
+
33
+ /**
34
+ * FingguFlux Theme Helper
35
+ * Provides runtime logic for theme switching and system preference synchronization.
36
+ */
37
+ type FingguTheme = 'light' | 'dark' | 'system';
38
+ /**
39
+ * Apply a theme to the document or a specific element.
40
+ */
41
+ declare function setTheme(theme: FingguTheme, target?: HTMLElement): void;
42
+ /**
43
+ * Watch for system theme changes and sync (if in system mode).
44
+ */
45
+ declare function watchSystemTheme(callback: (isDark: boolean) => void): () => void;
46
+ /**
47
+ * Get the current effective theme.
48
+ */
49
+ declare function getEffectiveTheme(target?: HTMLElement): 'light' | 'dark';
50
+
51
+ export { type FingguTheme, activateTab, focusTrap, getEffectiveTheme, initDropdowns, initEvents, initTabs, lockScroll, onToggle, setAriaHidden, setTheme, toggleAria, unlockScroll, watchSystemTheme };
package/dist/index.js ADDED
@@ -0,0 +1,288 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.ts
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ activateTab: () => activateTab,
23
+ focusTrap: () => focusTrap,
24
+ getEffectiveTheme: () => getEffectiveTheme,
25
+ initDropdowns: () => initDropdowns,
26
+ initEvents: () => initEvents,
27
+ initTabs: () => initTabs,
28
+ lockScroll: () => lockScroll,
29
+ onToggle: () => onToggle,
30
+ setAriaHidden: () => setAriaHidden,
31
+ setTheme: () => setTheme,
32
+ toggleAria: () => toggleAria,
33
+ unlockScroll: () => unlockScroll,
34
+ watchSystemTheme: () => watchSystemTheme
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/focus-trap.ts
39
+ var focusTrap = (container) => {
40
+ const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
41
+ const focusableElements = container.querySelectorAll(focusableSelectors);
42
+ const firstFocusable = focusableElements[0];
43
+ const lastFocusable = focusableElements[focusableElements.length - 1];
44
+ const handleTrap = (e) => {
45
+ if (e.key !== "Tab") return;
46
+ if (e.shiftKey) {
47
+ if (document.activeElement === firstFocusable) {
48
+ lastFocusable.focus();
49
+ e.preventDefault();
50
+ }
51
+ } else {
52
+ if (document.activeElement === lastFocusable) {
53
+ firstFocusable.focus();
54
+ e.preventDefault();
55
+ }
56
+ }
57
+ };
58
+ container.addEventListener("keydown", handleTrap);
59
+ return () => container.removeEventListener("keydown", handleTrap);
60
+ };
61
+
62
+ // src/scroll-lock.ts
63
+ var scrollPosition = 0;
64
+ var lockScroll = () => {
65
+ scrollPosition = window.scrollY || window.pageYOffset;
66
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
67
+ document.body.setAttribute("data-ff-scroll-lock", "true");
68
+ document.documentElement.style.setProperty("--ff-scrollbar-width", `${scrollbarWidth}px`);
69
+ };
70
+ var unlockScroll = () => {
71
+ document.body.removeAttribute("data-ff-scroll-lock");
72
+ document.documentElement.style.removeProperty("--ff-scrollbar-width");
73
+ window.scrollTo(0, scrollPosition);
74
+ };
75
+
76
+ // src/aria.ts
77
+ var toggleAria = (element, attribute, force) => {
78
+ const current = element.getAttribute(attribute);
79
+ const next = force !== void 0 ? force ? "true" : "false" : current === "true" ? "false" : "true";
80
+ element.setAttribute(attribute, next);
81
+ return next === "true";
82
+ };
83
+ var setAriaHidden = (selector, isHidden) => {
84
+ const elements = document.querySelectorAll(selector);
85
+ elements.forEach((el) => el.setAttribute("aria-hidden", isHidden.toString()));
86
+ };
87
+
88
+ // src/events.ts
89
+ var registry = /* @__PURE__ */ new Map();
90
+ var onToggle = (type, callback) => {
91
+ if (!registry.has(type)) registry.set(type, []);
92
+ registry.get(type).push(callback);
93
+ };
94
+ var initEvents = () => {
95
+ document.addEventListener("click", (e) => {
96
+ const trigger = e.target.closest("[data-ff-toggle]");
97
+ if (!trigger) return;
98
+ const type = trigger.getAttribute("data-ff-toggle") || "default";
99
+ const targetId = trigger.getAttribute("data-ff-target");
100
+ const target = targetId ? document.getElementById(targetId) : null;
101
+ if (target) {
102
+ const isExpanded = toggleAria(trigger, "aria-expanded");
103
+ target.setAttribute("data-ff-state", isExpanded ? "open" : "closed");
104
+ if (registry.has(type)) {
105
+ registry.get(type).forEach((cb) => cb({ trigger, target, isExpanded }));
106
+ }
107
+ }
108
+ });
109
+ document.addEventListener("keydown", (e) => {
110
+ if (e.key === "Escape") {
111
+ const openTargets = document.querySelectorAll('[data-ff-state="open"]');
112
+ openTargets.forEach((target) => {
113
+ target.setAttribute("data-ff-state", "closed");
114
+ const trigger = document.querySelector(`[data-ff-target="${target.id}"]`);
115
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
116
+ });
117
+ }
118
+ });
119
+ };
120
+
121
+ // src/tabs.ts
122
+ var initTabs = () => {
123
+ document.addEventListener("click", (e) => {
124
+ const tab = e.target.closest("[data-ff-tab]");
125
+ if (!tab) return;
126
+ activateTab(tab);
127
+ });
128
+ document.addEventListener("keydown", (e) => {
129
+ const tab = e.target.closest("[data-ff-tab]");
130
+ if (!tab) return;
131
+ const tablist = tab.closest('[role="tablist"]');
132
+ if (!tablist) return;
133
+ const tabs = Array.from(tablist.querySelectorAll("[data-ff-tab]"));
134
+ const index = tabs.indexOf(tab);
135
+ let nextTab;
136
+ switch (e.key) {
137
+ case "ArrowLeft":
138
+ nextTab = tabs[index - 1] || tabs[tabs.length - 1];
139
+ break;
140
+ case "ArrowRight":
141
+ nextTab = tabs[index + 1] || tabs[0];
142
+ break;
143
+ case "Home":
144
+ nextTab = tabs[0];
145
+ break;
146
+ case "End":
147
+ nextTab = tabs[tabs.length - 1];
148
+ break;
149
+ default:
150
+ return;
151
+ }
152
+ if (nextTab) {
153
+ nextTab.focus();
154
+ e.preventDefault();
155
+ }
156
+ });
157
+ };
158
+ var activateTab = (tab) => {
159
+ const tablist = tab.closest('[role="tablist"]');
160
+ if (!tablist) return;
161
+ const tabId = tab.getAttribute("data-ff-tab");
162
+ const allTabs = tablist.querySelectorAll("[data-ff-tab]");
163
+ allTabs.forEach((t) => {
164
+ const isActive = t === tab;
165
+ t.setAttribute("data-ff-state", isActive ? "active" : "inactive");
166
+ t.setAttribute("aria-selected", isActive ? "true" : "false");
167
+ t.setAttribute("tabindex", isActive ? "0" : "-1");
168
+ });
169
+ const allPanels = document.querySelectorAll("[data-ff-tab-panel]");
170
+ allPanels.forEach((panel) => {
171
+ const panelId = panel.getAttribute("data-ff-tab-panel");
172
+ const isTarget = panelId === tabId;
173
+ panel.setAttribute("data-ff-state", isTarget ? "active" : "inactive");
174
+ });
175
+ };
176
+
177
+ // src/dropdown.ts
178
+ var initDropdowns = () => {
179
+ document.addEventListener("click", (e) => {
180
+ const trigger = e.target.closest(".ff-dropdown-trigger");
181
+ const dropdown = e.target.closest(".ff-dropdown");
182
+ if (trigger && dropdown) {
183
+ const currentState = dropdown.getAttribute("data-ff-state");
184
+ const newState = currentState === "open" ? "closed" : "open";
185
+ setDropdownState(dropdown, newState);
186
+ return;
187
+ }
188
+ if (!dropdown) {
189
+ closeAllDropdowns();
190
+ }
191
+ });
192
+ document.addEventListener("keydown", (e) => {
193
+ const activeElement = document.activeElement;
194
+ const dropdown = activeElement ? activeElement.closest(".ff-dropdown") : null;
195
+ if (!dropdown) {
196
+ if (e.key === "Escape") closeAllDropdowns();
197
+ return;
198
+ }
199
+ const state = dropdown.getAttribute("data-ff-state");
200
+ const trigger = dropdown.querySelector(".ff-dropdown-trigger");
201
+ const items = Array.from(dropdown.querySelectorAll(".ff-dropdown-item"));
202
+ const currentIndex = items.indexOf(activeElement);
203
+ if (e.key === "Escape") {
204
+ setDropdownState(dropdown, "closed");
205
+ if (trigger) trigger.focus();
206
+ e.preventDefault();
207
+ return;
208
+ }
209
+ if (state === "closed") {
210
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
211
+ setDropdownState(dropdown, "open");
212
+ if (items.length > 0) items[0].focus();
213
+ e.preventDefault();
214
+ }
215
+ return;
216
+ }
217
+ switch (e.key) {
218
+ case "ArrowDown":
219
+ const nextIndex = (currentIndex + 1) % items.length;
220
+ items[nextIndex].focus();
221
+ e.preventDefault();
222
+ break;
223
+ case "ArrowUp":
224
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
225
+ items[prevIndex].focus();
226
+ e.preventDefault();
227
+ break;
228
+ case "Tab":
229
+ setDropdownState(dropdown, "closed");
230
+ break;
231
+ }
232
+ });
233
+ };
234
+ var setDropdownState = (dropdown, state) => {
235
+ dropdown.setAttribute("data-ff-state", state);
236
+ const trigger = dropdown.querySelector(".ff-dropdown-trigger");
237
+ if (trigger) {
238
+ trigger.setAttribute("aria-expanded", state === "open" ? "true" : "false");
239
+ }
240
+ };
241
+ var closeAllDropdowns = () => {
242
+ const openDropdowns = document.querySelectorAll('.ff-dropdown[data-ff-state="open"]');
243
+ openDropdowns.forEach((dropdown) => setDropdownState(dropdown, "closed"));
244
+ };
245
+
246
+ // src/theme.ts
247
+ function setTheme(theme, target = document.documentElement) {
248
+ if (theme === "system") {
249
+ target.removeAttribute("data-ff-theme");
250
+ } else {
251
+ target.setAttribute("data-ff-theme", theme);
252
+ }
253
+ }
254
+ function watchSystemTheme(callback) {
255
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
256
+ const listener = (e) => callback(e.matches);
257
+ mediaQuery.addEventListener("change", listener);
258
+ return () => mediaQuery.removeEventListener("change", listener);
259
+ }
260
+ function getEffectiveTheme(target = document.documentElement) {
261
+ const manual = target.getAttribute("data-ff-theme");
262
+ if (manual) return manual;
263
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
264
+ }
265
+
266
+ // src/index.ts
267
+ if (typeof document !== "undefined") {
268
+ initEvents();
269
+ initTabs();
270
+ initDropdowns();
271
+ console.log("FingguFlux JS Helpers initialized.");
272
+ }
273
+ // Annotate the CommonJS export names for ESM import in node:
274
+ 0 && (module.exports = {
275
+ activateTab,
276
+ focusTrap,
277
+ getEffectiveTheme,
278
+ initDropdowns,
279
+ initEvents,
280
+ initTabs,
281
+ lockScroll,
282
+ onToggle,
283
+ setAriaHidden,
284
+ setTheme,
285
+ toggleAria,
286
+ unlockScroll,
287
+ watchSystemTheme
288
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,250 @@
1
+ // src/focus-trap.ts
2
+ var focusTrap = (container) => {
3
+ const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
4
+ const focusableElements = container.querySelectorAll(focusableSelectors);
5
+ const firstFocusable = focusableElements[0];
6
+ const lastFocusable = focusableElements[focusableElements.length - 1];
7
+ const handleTrap = (e) => {
8
+ if (e.key !== "Tab") return;
9
+ if (e.shiftKey) {
10
+ if (document.activeElement === firstFocusable) {
11
+ lastFocusable.focus();
12
+ e.preventDefault();
13
+ }
14
+ } else {
15
+ if (document.activeElement === lastFocusable) {
16
+ firstFocusable.focus();
17
+ e.preventDefault();
18
+ }
19
+ }
20
+ };
21
+ container.addEventListener("keydown", handleTrap);
22
+ return () => container.removeEventListener("keydown", handleTrap);
23
+ };
24
+
25
+ // src/scroll-lock.ts
26
+ var scrollPosition = 0;
27
+ var lockScroll = () => {
28
+ scrollPosition = window.scrollY || window.pageYOffset;
29
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
30
+ document.body.setAttribute("data-ff-scroll-lock", "true");
31
+ document.documentElement.style.setProperty("--ff-scrollbar-width", `${scrollbarWidth}px`);
32
+ };
33
+ var unlockScroll = () => {
34
+ document.body.removeAttribute("data-ff-scroll-lock");
35
+ document.documentElement.style.removeProperty("--ff-scrollbar-width");
36
+ window.scrollTo(0, scrollPosition);
37
+ };
38
+
39
+ // src/aria.ts
40
+ var toggleAria = (element, attribute, force) => {
41
+ const current = element.getAttribute(attribute);
42
+ const next = force !== void 0 ? force ? "true" : "false" : current === "true" ? "false" : "true";
43
+ element.setAttribute(attribute, next);
44
+ return next === "true";
45
+ };
46
+ var setAriaHidden = (selector, isHidden) => {
47
+ const elements = document.querySelectorAll(selector);
48
+ elements.forEach((el) => el.setAttribute("aria-hidden", isHidden.toString()));
49
+ };
50
+
51
+ // src/events.ts
52
+ var registry = /* @__PURE__ */ new Map();
53
+ var onToggle = (type, callback) => {
54
+ if (!registry.has(type)) registry.set(type, []);
55
+ registry.get(type).push(callback);
56
+ };
57
+ var initEvents = () => {
58
+ document.addEventListener("click", (e) => {
59
+ const trigger = e.target.closest("[data-ff-toggle]");
60
+ if (!trigger) return;
61
+ const type = trigger.getAttribute("data-ff-toggle") || "default";
62
+ const targetId = trigger.getAttribute("data-ff-target");
63
+ const target = targetId ? document.getElementById(targetId) : null;
64
+ if (target) {
65
+ const isExpanded = toggleAria(trigger, "aria-expanded");
66
+ target.setAttribute("data-ff-state", isExpanded ? "open" : "closed");
67
+ if (registry.has(type)) {
68
+ registry.get(type).forEach((cb) => cb({ trigger, target, isExpanded }));
69
+ }
70
+ }
71
+ });
72
+ document.addEventListener("keydown", (e) => {
73
+ if (e.key === "Escape") {
74
+ const openTargets = document.querySelectorAll('[data-ff-state="open"]');
75
+ openTargets.forEach((target) => {
76
+ target.setAttribute("data-ff-state", "closed");
77
+ const trigger = document.querySelector(`[data-ff-target="${target.id}"]`);
78
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
79
+ });
80
+ }
81
+ });
82
+ };
83
+
84
+ // src/tabs.ts
85
+ var initTabs = () => {
86
+ document.addEventListener("click", (e) => {
87
+ const tab = e.target.closest("[data-ff-tab]");
88
+ if (!tab) return;
89
+ activateTab(tab);
90
+ });
91
+ document.addEventListener("keydown", (e) => {
92
+ const tab = e.target.closest("[data-ff-tab]");
93
+ if (!tab) return;
94
+ const tablist = tab.closest('[role="tablist"]');
95
+ if (!tablist) return;
96
+ const tabs = Array.from(tablist.querySelectorAll("[data-ff-tab]"));
97
+ const index = tabs.indexOf(tab);
98
+ let nextTab;
99
+ switch (e.key) {
100
+ case "ArrowLeft":
101
+ nextTab = tabs[index - 1] || tabs[tabs.length - 1];
102
+ break;
103
+ case "ArrowRight":
104
+ nextTab = tabs[index + 1] || tabs[0];
105
+ break;
106
+ case "Home":
107
+ nextTab = tabs[0];
108
+ break;
109
+ case "End":
110
+ nextTab = tabs[tabs.length - 1];
111
+ break;
112
+ default:
113
+ return;
114
+ }
115
+ if (nextTab) {
116
+ nextTab.focus();
117
+ e.preventDefault();
118
+ }
119
+ });
120
+ };
121
+ var activateTab = (tab) => {
122
+ const tablist = tab.closest('[role="tablist"]');
123
+ if (!tablist) return;
124
+ const tabId = tab.getAttribute("data-ff-tab");
125
+ const allTabs = tablist.querySelectorAll("[data-ff-tab]");
126
+ allTabs.forEach((t) => {
127
+ const isActive = t === tab;
128
+ t.setAttribute("data-ff-state", isActive ? "active" : "inactive");
129
+ t.setAttribute("aria-selected", isActive ? "true" : "false");
130
+ t.setAttribute("tabindex", isActive ? "0" : "-1");
131
+ });
132
+ const allPanels = document.querySelectorAll("[data-ff-tab-panel]");
133
+ allPanels.forEach((panel) => {
134
+ const panelId = panel.getAttribute("data-ff-tab-panel");
135
+ const isTarget = panelId === tabId;
136
+ panel.setAttribute("data-ff-state", isTarget ? "active" : "inactive");
137
+ });
138
+ };
139
+
140
+ // src/dropdown.ts
141
+ var initDropdowns = () => {
142
+ document.addEventListener("click", (e) => {
143
+ const trigger = e.target.closest(".ff-dropdown-trigger");
144
+ const dropdown = e.target.closest(".ff-dropdown");
145
+ if (trigger && dropdown) {
146
+ const currentState = dropdown.getAttribute("data-ff-state");
147
+ const newState = currentState === "open" ? "closed" : "open";
148
+ setDropdownState(dropdown, newState);
149
+ return;
150
+ }
151
+ if (!dropdown) {
152
+ closeAllDropdowns();
153
+ }
154
+ });
155
+ document.addEventListener("keydown", (e) => {
156
+ const activeElement = document.activeElement;
157
+ const dropdown = activeElement ? activeElement.closest(".ff-dropdown") : null;
158
+ if (!dropdown) {
159
+ if (e.key === "Escape") closeAllDropdowns();
160
+ return;
161
+ }
162
+ const state = dropdown.getAttribute("data-ff-state");
163
+ const trigger = dropdown.querySelector(".ff-dropdown-trigger");
164
+ const items = Array.from(dropdown.querySelectorAll(".ff-dropdown-item"));
165
+ const currentIndex = items.indexOf(activeElement);
166
+ if (e.key === "Escape") {
167
+ setDropdownState(dropdown, "closed");
168
+ if (trigger) trigger.focus();
169
+ e.preventDefault();
170
+ return;
171
+ }
172
+ if (state === "closed") {
173
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
174
+ setDropdownState(dropdown, "open");
175
+ if (items.length > 0) items[0].focus();
176
+ e.preventDefault();
177
+ }
178
+ return;
179
+ }
180
+ switch (e.key) {
181
+ case "ArrowDown":
182
+ const nextIndex = (currentIndex + 1) % items.length;
183
+ items[nextIndex].focus();
184
+ e.preventDefault();
185
+ break;
186
+ case "ArrowUp":
187
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
188
+ items[prevIndex].focus();
189
+ e.preventDefault();
190
+ break;
191
+ case "Tab":
192
+ setDropdownState(dropdown, "closed");
193
+ break;
194
+ }
195
+ });
196
+ };
197
+ var setDropdownState = (dropdown, state) => {
198
+ dropdown.setAttribute("data-ff-state", state);
199
+ const trigger = dropdown.querySelector(".ff-dropdown-trigger");
200
+ if (trigger) {
201
+ trigger.setAttribute("aria-expanded", state === "open" ? "true" : "false");
202
+ }
203
+ };
204
+ var closeAllDropdowns = () => {
205
+ const openDropdowns = document.querySelectorAll('.ff-dropdown[data-ff-state="open"]');
206
+ openDropdowns.forEach((dropdown) => setDropdownState(dropdown, "closed"));
207
+ };
208
+
209
+ // src/theme.ts
210
+ function setTheme(theme, target = document.documentElement) {
211
+ if (theme === "system") {
212
+ target.removeAttribute("data-ff-theme");
213
+ } else {
214
+ target.setAttribute("data-ff-theme", theme);
215
+ }
216
+ }
217
+ function watchSystemTheme(callback) {
218
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
219
+ const listener = (e) => callback(e.matches);
220
+ mediaQuery.addEventListener("change", listener);
221
+ return () => mediaQuery.removeEventListener("change", listener);
222
+ }
223
+ function getEffectiveTheme(target = document.documentElement) {
224
+ const manual = target.getAttribute("data-ff-theme");
225
+ if (manual) return manual;
226
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
227
+ }
228
+
229
+ // src/index.ts
230
+ if (typeof document !== "undefined") {
231
+ initEvents();
232
+ initTabs();
233
+ initDropdowns();
234
+ console.log("FingguFlux JS Helpers initialized.");
235
+ }
236
+ export {
237
+ activateTab,
238
+ focusTrap,
239
+ getEffectiveTheme,
240
+ initDropdowns,
241
+ initEvents,
242
+ initTabs,
243
+ lockScroll,
244
+ onToggle,
245
+ setAriaHidden,
246
+ setTheme,
247
+ toggleAria,
248
+ unlockScroll,
249
+ watchSystemTheme
250
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@finggujadhav/js-helper",
3
+ "version": "0.9.0",
4
+ "description": "Shared runtime logic for FingguFlux themes and motion state management.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts"
20
+ },
21
+ "devDependencies": {
22
+ "tsup": "^8.0.0",
23
+ "typescript": "^5.0.0"
24
+ },
25
+ "keywords": [
26
+ "runtime",
27
+ "theme-switching",
28
+ "motion-logic"
29
+ ],
30
+ "author": "Finggu Architecture Team",
31
+ "license": "MIT"
32
+ }
package/src/aria.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * ARIA Helper
3
+ * Managed attribute toggling for accessibility.
4
+ */
5
+ export const toggleAria = (element: HTMLElement, attribute: string, force?: boolean): boolean => {
6
+ const current = element.getAttribute(attribute);
7
+ const next = force !== undefined ? (force ? 'true' : 'false') : (current === 'true' ? 'false' : 'true');
8
+ element.setAttribute(attribute, next);
9
+ return next === 'true';
10
+ };
11
+
12
+ export const setAriaHidden = (selector: string, isHidden: boolean) => {
13
+ const elements = document.querySelectorAll(selector);
14
+ elements.forEach(el => el.setAttribute('aria-hidden', isHidden.toString()));
15
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Dropdown Helper
3
+ * Handles accessible keyboard navigation, outside-click detection, and state management.
4
+ */
5
+
6
+ export const initDropdowns = () => {
7
+ // Global Click Listener for Event Delegation
8
+ document.addEventListener('click', (e: MouseEvent) => {
9
+ const trigger = (e.target as HTMLElement).closest('.ff-dropdown-trigger') as HTMLElement;
10
+ const dropdown = (e.target as HTMLElement).closest('.ff-dropdown') as HTMLElement;
11
+
12
+ // Handle clicks on triggers
13
+ if (trigger && dropdown) {
14
+ const currentState = dropdown.getAttribute('data-ff-state');
15
+ const newState = currentState === 'open' ? 'closed' : 'open';
16
+ setDropdownState(dropdown, newState);
17
+ return;
18
+ }
19
+
20
+ // Outside Click Detection
21
+ if (!dropdown) {
22
+ closeAllDropdowns();
23
+ }
24
+ });
25
+
26
+ // Global Keydown Listener
27
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
28
+ const activeElement = document.activeElement as HTMLElement;
29
+ const dropdown = activeElement ? activeElement.closest('.ff-dropdown') as HTMLElement : null;
30
+
31
+ if (!dropdown) {
32
+ if (e.key === 'Escape') closeAllDropdowns();
33
+ return;
34
+ }
35
+
36
+ const state = dropdown.getAttribute('data-ff-state');
37
+ const trigger = dropdown.querySelector('.ff-dropdown-trigger') as HTMLElement;
38
+ const items = Array.from(dropdown.querySelectorAll('.ff-dropdown-item')) as HTMLElement[];
39
+ const currentIndex = items.indexOf(activeElement);
40
+
41
+ if (e.key === 'Escape') {
42
+ setDropdownState(dropdown, 'closed');
43
+ if (trigger) trigger.focus(); // Return focus to trigger
44
+ e.preventDefault();
45
+ return;
46
+ }
47
+
48
+ if (state === 'closed') {
49
+ if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
50
+ setDropdownState(dropdown, 'open');
51
+ if (items.length > 0) items[0].focus();
52
+ e.preventDefault();
53
+ }
54
+ return;
55
+ }
56
+
57
+ // Navigation within open menu
58
+ switch (e.key) {
59
+ case 'ArrowDown':
60
+ const nextIndex = (currentIndex + 1) % items.length;
61
+ items[nextIndex].focus();
62
+ e.preventDefault();
63
+ break;
64
+ case 'ArrowUp':
65
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
66
+ items[prevIndex].focus();
67
+ e.preventDefault();
68
+ break;
69
+ case 'Tab':
70
+ setDropdownState(dropdown, 'closed');
71
+ break;
72
+ }
73
+ });
74
+ };
75
+
76
+ const setDropdownState = (dropdown: HTMLElement, state: string) => {
77
+ dropdown.setAttribute('data-ff-state', state);
78
+ const trigger = dropdown.querySelector('.ff-dropdown-trigger');
79
+ if (trigger) {
80
+ trigger.setAttribute('aria-expanded', state === 'open' ? 'true' : 'false');
81
+ }
82
+ };
83
+
84
+ const closeAllDropdowns = () => {
85
+ const openDropdowns = document.querySelectorAll('.ff-dropdown[data-ff-state="open"]');
86
+ openDropdowns.forEach(dropdown => setDropdownState(dropdown as HTMLElement, 'closed'));
87
+ };
package/src/events.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Event Delegation System
3
+ * Handles global listeners for better performance and dynamic elements.
4
+ */
5
+ import { toggleAria } from './aria';
6
+
7
+ const registry = new Map<string, any[]>();
8
+
9
+ export const onToggle = (type: string, callback: (data: any) => void) => {
10
+ if (!registry.has(type)) registry.set(type, []);
11
+ registry.get(type)!.push(callback);
12
+ };
13
+
14
+ export const initEvents = () => {
15
+ // Global Click Listener
16
+ document.addEventListener('click', (e: MouseEvent) => {
17
+ const trigger = (e.target as HTMLElement).closest('[data-ff-toggle]') as HTMLElement;
18
+ if (!trigger) return;
19
+
20
+ const type = trigger.getAttribute('data-ff-toggle') || 'default';
21
+ const targetId = trigger.getAttribute('data-ff-target');
22
+ const target = targetId ? document.getElementById(targetId) : null;
23
+
24
+ // Default state toggle
25
+ if (target) {
26
+ const isExpanded = toggleAria(trigger, 'aria-expanded');
27
+ target.setAttribute('data-ff-state', isExpanded ? 'open' : 'closed');
28
+
29
+ // Notify custom listeners
30
+ if (registry.has(type)) {
31
+ registry.get(type)!.forEach(cb => cb({ trigger, target, isExpanded }));
32
+ }
33
+ }
34
+ });
35
+
36
+ // Global Keydown Listener
37
+ document.addEventListener('keydown', (e) => {
38
+ if (e.key === 'Escape') {
39
+ // Find all open toggles and close them
40
+ const openTargets = document.querySelectorAll('[data-ff-state="open"]');
41
+ openTargets.forEach(target => {
42
+ target.setAttribute('data-ff-state', 'closed');
43
+ const trigger = document.querySelector(`[data-ff-target="${target.id}"]`);
44
+ if (trigger) trigger.setAttribute('aria-expanded', 'false');
45
+ });
46
+ }
47
+ });
48
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Focus Trap Utility
3
+ * Ensures tab focus remains within a specific container.
4
+ */
5
+ export const focusTrap = (container: HTMLElement) => {
6
+ const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
7
+ const focusableElements = container.querySelectorAll(focusableSelectors) as NodeListOf<HTMLElement>;
8
+ const firstFocusable = focusableElements[0];
9
+ const lastFocusable = focusableElements[focusableElements.length - 1];
10
+
11
+ const handleTrap = (e: KeyboardEvent) => {
12
+ if (e.key !== 'Tab') return;
13
+
14
+ if (e.shiftKey) { // Back tab
15
+ if (document.activeElement === firstFocusable) {
16
+ lastFocusable.focus();
17
+ e.preventDefault();
18
+ }
19
+ } else { // Forward tab
20
+ if (document.activeElement === lastFocusable) {
21
+ firstFocusable.focus();
22
+ e.preventDefault();
23
+ }
24
+ }
25
+ };
26
+
27
+ container.addEventListener('keydown', handleTrap);
28
+
29
+ // Return cleanup function
30
+ return () => container.removeEventListener('keydown', handleTrap);
31
+ };
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * FingguFlux JS Helper - Entry Point
3
+ */
4
+ export * from './focus-trap';
5
+ export * from './scroll-lock';
6
+ export * from './aria';
7
+ export * from './events';
8
+ export * from './tabs';
9
+ export * from './dropdown';
10
+ export * from './theme';
11
+
12
+ import { initEvents } from './events';
13
+ import { initTabs } from './tabs';
14
+ import { initDropdowns } from './dropdown';
15
+
16
+ // Auto-initialize if running in a standard environment
17
+ if (typeof document !== 'undefined') {
18
+ initEvents();
19
+ initTabs();
20
+ initDropdowns();
21
+ console.log('FingguFlux JS Helpers initialized.');
22
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Scroll Lock Utility
3
+ * Prevents body scrolling without layout shift.
4
+ */
5
+ let scrollPosition = 0;
6
+
7
+ export const lockScroll = () => {
8
+ scrollPosition = window.scrollY || window.pageYOffset;
9
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
10
+
11
+ // Use a data attribute for styling instead of inline styles
12
+ document.body.setAttribute('data-ff-scroll-lock', 'true');
13
+ document.documentElement.style.setProperty('--ff-scrollbar-width', `${scrollbarWidth}px`);
14
+ };
15
+
16
+ export const unlockScroll = () => {
17
+ document.body.removeAttribute('data-ff-scroll-lock');
18
+ document.documentElement.style.removeProperty('--ff-scrollbar-width');
19
+ window.scrollTo(0, scrollPosition);
20
+ };
package/src/tabs.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Tabs Helper
3
+ * Handles accessible keyboard navigation and state management for tabs.
4
+ */
5
+
6
+ export const initTabs = () => {
7
+ document.addEventListener('click', (e: MouseEvent) => {
8
+ const tab = (e.target as HTMLElement).closest('[data-ff-tab]') as HTMLElement;
9
+ if (!tab) return;
10
+ activateTab(tab);
11
+ });
12
+
13
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
14
+ const tab = (e.target as HTMLElement).closest('[data-ff-tab]') as HTMLElement;
15
+ if (!tab) return;
16
+
17
+ const tablist = tab.closest('[role="tablist"]');
18
+ if (!tablist) return;
19
+
20
+ const tabs = Array.from(tablist.querySelectorAll('[data-ff-tab]')) as HTMLElement[];
21
+ const index = tabs.indexOf(tab);
22
+
23
+ let nextTab: HTMLElement | undefined;
24
+
25
+ switch (e.key) {
26
+ case 'ArrowLeft':
27
+ nextTab = tabs[index - 1] || tabs[tabs.length - 1];
28
+ break;
29
+ case 'ArrowRight':
30
+ nextTab = tabs[index + 1] || tabs[0];
31
+ break;
32
+ case 'Home':
33
+ nextTab = tabs[0];
34
+ break;
35
+ case 'End':
36
+ nextTab = tabs[tabs.length - 1];
37
+ break;
38
+ default:
39
+ return;
40
+ }
41
+
42
+ if (nextTab) {
43
+ nextTab.focus();
44
+ e.preventDefault();
45
+ }
46
+ });
47
+ };
48
+
49
+ export const activateTab = (tab: HTMLElement) => {
50
+ const tablist = tab.closest('[role="tablist"]');
51
+ if (!tablist) return;
52
+
53
+ const tabId = tab.getAttribute('data-ff-tab');
54
+ const allTabs = tablist.querySelectorAll('[data-ff-tab]');
55
+
56
+ // Update Tabs
57
+ allTabs.forEach(t => {
58
+ const isActive = t === tab;
59
+ t.setAttribute('data-ff-state', isActive ? 'active' : 'inactive');
60
+ t.setAttribute('aria-selected', isActive ? 'true' : 'false');
61
+ t.setAttribute('tabindex', isActive ? '0' : '-1');
62
+ });
63
+
64
+ // Update Panels
65
+ const allPanels = document.querySelectorAll('[data-ff-tab-panel]');
66
+ allPanels.forEach(panel => {
67
+ const panelId = panel.getAttribute('data-ff-tab-panel');
68
+ const isTarget = panelId === tabId;
69
+ panel.setAttribute('data-ff-state', isTarget ? 'active' : 'inactive');
70
+ });
71
+ };
package/src/theme.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * FingguFlux Theme Helper
3
+ * Provides runtime logic for theme switching and system preference synchronization.
4
+ */
5
+
6
+ export type FingguTheme = 'light' | 'dark' | 'system';
7
+
8
+ /**
9
+ * Apply a theme to the document or a specific element.
10
+ */
11
+ export function setTheme(theme: FingguTheme, target: HTMLElement = document.documentElement) {
12
+ if (theme === 'system') {
13
+ target.removeAttribute('data-ff-theme');
14
+ } else {
15
+ target.setAttribute('data-ff-theme', theme);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Watch for system theme changes and sync (if in system mode).
21
+ */
22
+ export function watchSystemTheme(callback: (isDark: boolean) => void) {
23
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
24
+ const listener = (e: MediaQueryListEvent) => callback(e.matches);
25
+
26
+ mediaQuery.addEventListener('change', listener);
27
+ return () => mediaQuery.removeEventListener('change', listener);
28
+ }
29
+
30
+ /**
31
+ * Get the current effective theme.
32
+ */
33
+ export function getEffectiveTheme(target: HTMLElement = document.documentElement): 'light' | 'dark' {
34
+ const manual = target.getAttribute('data-ff-theme') as 'light' | 'dark' | null;
35
+ if (manual) return manual;
36
+
37
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
38
+ }