@pajh/buldng 0.0.4 → 0.0.5

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,348 @@
1
+ export type ModalType =
2
+ | "info"
3
+ | "confirmation"
4
+ | "destructive"
5
+ | "error";
6
+
7
+ export type ModalIcon = "info" | "warning" | "error" | null;
8
+
9
+ export type ModalResult = "ok" | "cancel" | "dismiss";
10
+
11
+ export interface ModalSettings {
12
+ title?: string;
13
+ icon?: ModalIcon;
14
+ autoDismiss?: number;
15
+ dismissOnEsc?: boolean;
16
+ dismissOnBackdrop?: boolean;
17
+ }
18
+
19
+ const MODAL_ROOT_ID = "b-modal-root";
20
+ let modalId = 0;
21
+
22
+ interface ModalRoot {
23
+ element: HTMLElement;
24
+ created: boolean;
25
+ }
26
+
27
+ function getModalRoot(): ModalRoot {
28
+ const existingRoot = document.getElementById(MODAL_ROOT_ID);
29
+ if (existingRoot instanceof HTMLElement) {
30
+ return { element: existingRoot, created: false };
31
+ }
32
+
33
+ const createdRoot = document.createElement("div");
34
+ createdRoot.id = MODAL_ROOT_ID;
35
+ document.body.appendChild(createdRoot);
36
+ return { element: createdRoot, created: true };
37
+ }
38
+
39
+ function getIconText(icon: ModalIcon): string {
40
+ if (icon === "info") {
41
+ return "i";
42
+ }
43
+ if (icon === "warning") {
44
+ return "!";
45
+ }
46
+ if (icon === "error") {
47
+ return "!";
48
+ }
49
+ return "";
50
+ }
51
+
52
+ function getDefaultIcon(type: ModalType): ModalIcon {
53
+ if (type === "destructive") {
54
+ return "warning";
55
+ }
56
+ if (type === "error") {
57
+ return "error";
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function getButtonLabels(type: ModalType): { primary: string; secondary: string | null } {
63
+ if (type === "confirmation") {
64
+ return { primary: "OK", secondary: "Cancel" };
65
+ }
66
+ if (type === "destructive") {
67
+ return { primary: "Delete", secondary: "Cancel" };
68
+ }
69
+ return { primary: "OK", secondary: null };
70
+ }
71
+
72
+ function getDefaultDismissal(type: ModalType): boolean {
73
+ return type !== "destructive";
74
+ }
75
+
76
+ function getInitialFocus(dialog: HTMLElement, primaryButton: HTMLButtonElement): HTMLElement {
77
+ const focusable = dialog.querySelector<HTMLElement>(
78
+ "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])"
79
+ );
80
+ if (focusable !== null) {
81
+ return focusable;
82
+ }
83
+ return primaryButton;
84
+ }
85
+
86
+ function isInsideDialog(target: EventTarget | null, dialog: HTMLElement): boolean {
87
+ return target instanceof Node && dialog.contains(target);
88
+ }
89
+
90
+ class ModalController {
91
+ private readonly type: ModalType;
92
+ private readonly content: string;
93
+ private readonly settings: ModalSettings;
94
+ private readonly root: HTMLElement;
95
+ private readonly ownsRoot: boolean;
96
+ private readonly previousFocus: HTMLElement | null;
97
+ private readonly dialog: HTMLElement;
98
+ private readonly backdrop: HTMLElement;
99
+ private readonly primaryButton: HTMLButtonElement;
100
+ private readonly secondaryButton: HTMLButtonElement | null;
101
+ private readonly keydownHandler: (event: KeyboardEvent) => void;
102
+ private timeoutId: number | null;
103
+ private settled: boolean;
104
+ private resolveResult: ((result: ModalResult) => void) | null;
105
+
106
+ public constructor(
107
+ type: ModalType,
108
+ content: string,
109
+ settings: ModalSettings
110
+ ) {
111
+ this.type = type;
112
+ this.content = content;
113
+ this.settings = settings;
114
+ const modalRoot = getModalRoot();
115
+ this.root = modalRoot.element;
116
+ this.ownsRoot = modalRoot.created;
117
+ this.previousFocus = document.activeElement instanceof HTMLElement
118
+ ? document.activeElement
119
+ : null;
120
+ this.dialog = this.createDialog();
121
+ this.backdrop = this.createBackdrop();
122
+ this.primaryButton = this.createPrimaryButton();
123
+ this.secondaryButton = this.createSecondaryButton();
124
+ this.addHeader();
125
+ this.addContent();
126
+ this.addFooter();
127
+ this.timeoutId = null;
128
+ this.settled = false;
129
+ this.resolveResult = null;
130
+ this.keydownHandler = (event: KeyboardEvent) => {
131
+ this.handleKeydown(event);
132
+ };
133
+ }
134
+
135
+ public open(): Promise<ModalResult> {
136
+ this.root.appendChild(this.backdrop);
137
+ this.backdrop.appendChild(this.dialog);
138
+ this.registerListeners();
139
+
140
+ const initialFocus = getInitialFocus(this.dialog, this.primaryButton);
141
+ initialFocus.focus();
142
+
143
+ if (this.settings.autoDismiss !== undefined && this.settings.autoDismiss > 0) {
144
+ this.timeoutId = window.setTimeout(() => {
145
+ this.finish("dismiss");
146
+ }, this.settings.autoDismiss);
147
+ }
148
+
149
+ return new Promise<ModalResult>((resolve) => {
150
+ this.resolveResult = resolve;
151
+ });
152
+ }
153
+
154
+ private createDialog(): HTMLElement {
155
+ const dialog = document.createElement("div");
156
+ dialog.className = `buldng-modal ${this.type}`;
157
+ dialog.setAttribute("role", "dialog");
158
+ dialog.setAttribute("aria-modal", "true");
159
+ dialog.tabIndex = -1;
160
+ return dialog;
161
+ }
162
+
163
+ private createBackdrop(): HTMLElement {
164
+ const backdrop = document.createElement("div");
165
+ backdrop.className = "buldng-modal-backdrop";
166
+ backdrop.setAttribute("aria-hidden", "true");
167
+ return backdrop;
168
+ }
169
+
170
+ private createPrimaryButton(): HTMLButtonElement {
171
+ const button = document.createElement("button");
172
+ button.type = "button";
173
+ button.className = "buldng-modal-button buldng-modal-button-primary";
174
+ button.textContent = getButtonLabels(this.type).primary;
175
+ return button;
176
+ }
177
+
178
+ private createSecondaryButton(): HTMLButtonElement | null {
179
+ const labels = getButtonLabels(this.type);
180
+ if (labels.secondary === null) {
181
+ return null;
182
+ }
183
+
184
+ const button = document.createElement("button");
185
+ button.type = "button";
186
+ button.className = "buldng-modal-button buldng-modal-button-secondary";
187
+ button.textContent = labels.secondary;
188
+ return button;
189
+ }
190
+
191
+ private addHeader(): void {
192
+ const hasTitle = this.settings.title !== undefined && this.settings.title.length > 0;
193
+ const icon = this.settings.icon === undefined
194
+ ? getDefaultIcon(this.type)
195
+ : this.settings.icon;
196
+ const hasIcon = icon !== null;
197
+
198
+ if (!hasTitle && !hasIcon) {
199
+ return;
200
+ }
201
+
202
+ const header = document.createElement("div");
203
+ header.className = "buldng-modal-header";
204
+
205
+ if (hasIcon) {
206
+ const iconElement = document.createElement("span");
207
+ iconElement.className = "buldng-modal-icon";
208
+ iconElement.setAttribute("aria-hidden", "true");
209
+ iconElement.textContent = getIconText(icon);
210
+ header.appendChild(iconElement);
211
+ }
212
+
213
+ if (hasTitle) {
214
+ const title = document.createElement("h2");
215
+ title.className = "buldng-modal-title";
216
+ title.textContent = this.settings.title as string;
217
+ modalId += 1;
218
+ const titleId = `buldng-modal-title-${modalId}`;
219
+ title.id = titleId;
220
+ this.dialog.setAttribute("aria-labelledby", titleId);
221
+ header.appendChild(title);
222
+ this.dialog.appendChild(header);
223
+ return;
224
+ }
225
+
226
+ this.dialog.appendChild(header);
227
+ }
228
+
229
+ private addContent(): void {
230
+ const content = document.createElement("div");
231
+ content.className = "buldng-modal-content";
232
+ content.innerHTML = this.content;
233
+ this.dialog.appendChild(content);
234
+ }
235
+
236
+ private addFooter(): void {
237
+ const footer = document.createElement("div");
238
+ footer.className = "buldng-modal-footer";
239
+
240
+ if (this.secondaryButton !== null) {
241
+ footer.appendChild(this.secondaryButton);
242
+ }
243
+ footer.appendChild(this.primaryButton);
244
+ this.dialog.appendChild(footer);
245
+ }
246
+
247
+ private registerListeners(): void {
248
+ this.primaryButton.addEventListener("click", () => {
249
+ this.finish("ok");
250
+ });
251
+
252
+ if (this.secondaryButton !== null) {
253
+ this.secondaryButton.addEventListener("click", () => {
254
+ this.finish("cancel");
255
+ });
256
+ }
257
+
258
+ this.backdrop.addEventListener("click", (event: MouseEvent) => {
259
+ if (isInsideDialog(event.target, this.dialog)) {
260
+ return;
261
+ }
262
+ const dismissOnBackdrop = this.settings.dismissOnBackdrop === undefined
263
+ ? getDefaultDismissal(this.type)
264
+ : this.settings.dismissOnBackdrop;
265
+ if (dismissOnBackdrop === true) {
266
+ this.finish("dismiss");
267
+ }
268
+ });
269
+
270
+ document.addEventListener("keydown", this.keydownHandler);
271
+ }
272
+
273
+ private handleKeydown(event: KeyboardEvent): void {
274
+ if (event.key === "Escape") {
275
+ const dismissOnEsc = this.settings.dismissOnEsc === undefined
276
+ ? getDefaultDismissal(this.type)
277
+ : this.settings.dismissOnEsc;
278
+ if (dismissOnEsc === true) {
279
+ event.preventDefault();
280
+ this.finish("dismiss");
281
+ }
282
+ return;
283
+ }
284
+
285
+ if (event.key === "Tab") {
286
+ this.keepFocusInside(event);
287
+ }
288
+ }
289
+
290
+ private keepFocusInside(event: KeyboardEvent): void {
291
+ const focusable = Array.from(this.dialog.querySelectorAll<HTMLElement>(
292
+ "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])"
293
+ ));
294
+ if (focusable.length === 0) {
295
+ event.preventDefault();
296
+ this.dialog.focus();
297
+ return;
298
+ }
299
+
300
+ const first = focusable[0];
301
+ const last = focusable[focusable.length - 1];
302
+ const active = document.activeElement;
303
+
304
+ if (event.shiftKey === true && active === first) {
305
+ event.preventDefault();
306
+ last.focus();
307
+ } else if (event.shiftKey === false && active === last) {
308
+ event.preventDefault();
309
+ first.focus();
310
+ }
311
+ }
312
+
313
+ private finish(result: ModalResult): void {
314
+ if (this.settled === true) {
315
+ return;
316
+ }
317
+ this.settled = true;
318
+
319
+ if (this.timeoutId !== null) {
320
+ window.clearTimeout(this.timeoutId);
321
+ this.timeoutId = null;
322
+ }
323
+
324
+ document.removeEventListener("keydown", this.keydownHandler);
325
+ this.backdrop.remove();
326
+ if (this.ownsRoot === true && this.root.childElementCount === 0) {
327
+ this.root.remove();
328
+ }
329
+
330
+ if (this.previousFocus !== null && document.contains(this.previousFocus)) {
331
+ this.previousFocus.focus();
332
+ }
333
+
334
+ if (this.resolveResult !== null) {
335
+ this.resolveResult(result);
336
+ this.resolveResult = null;
337
+ }
338
+ }
339
+ }
340
+
341
+ export function showModal(
342
+ type: ModalType,
343
+ content: string,
344
+ settings: ModalSettings = {}
345
+ ): Promise<ModalResult> {
346
+ const controller = new ModalController(type, content, settings);
347
+ return controller.open();
348
+ }
@@ -0,0 +1,77 @@
1
+ // buldng-typescript.js
2
+ // typescript build support for buldng
3
+ //
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { createRequire } from "module";
8
+ import { execSync } from "child_process";
9
+
10
+ function findTSCFromResolved(tsPath) {
11
+ // tsPath → /.../typescript/lib/typescript.js
12
+ let dir = path.dirname(tsPath); // /.../typescript/lib
13
+ dir = path.dirname(dir); // /.../typescript
14
+
15
+ while (true) {
16
+ const candidate = path.join(dir, ".bin", "tsc");
17
+ if (fs.existsSync(candidate)) {
18
+ return candidate;
19
+ }
20
+
21
+ const parent = path.dirname(dir);
22
+ if (parent === dir) break; // reached filesystem root
23
+ dir = parent;
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ function findGlobalTSC() {
30
+ try {
31
+ const globalTSC = execSync("which tsc").toString().trim();
32
+ return globalTSC || null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export function typescriptCheck() {
39
+ const require = createRequire(import.meta.url);
40
+
41
+ let tsPath = null;
42
+
43
+ try {
44
+ tsPath = require.resolve("typescript");
45
+ } catch {
46
+ tsPath = null;
47
+ }
48
+
49
+ let tscPath = null;
50
+
51
+ if (tsPath) {
52
+ // Local / nested / linked TypeScript
53
+ tscPath = findTSCFromResolved(tsPath);
54
+ } else {
55
+ // Try global TypeScript
56
+ tscPath = findGlobalTSC();
57
+ }
58
+
59
+ if (!tscPath) {
60
+ console.log("TypeScript not found — skipping TS checks");
61
+ return;
62
+ }
63
+
64
+ console.log("Using TypeScript compiler:", tscPath);
65
+
66
+ try {
67
+ execSync(`${tscPath} --noEmit`, { stdio: "inherit" });
68
+ } catch (err) {
69
+ // If tsc exists but cannot run → internal error
70
+ if (err.code === "ENOENT") {
71
+ throw new Error("Internal TypeScript error — 'tsc' binary exists but cannot be executed");
72
+ }
73
+
74
+ // If tsc runs but reports TS errors → build failure
75
+ throw new Error("TypeScript check failed");
76
+ }
77
+ }