@pajh/buldng 0.0.3 → 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
+ }
@@ -93,6 +93,12 @@ export function op_compile(config, children) {
93
93
  }
94
94
 
95
95
  const child = children[0];
96
+ const compileOptions = {
97
+ minify: config.flags?.minify ?? true,
98
+ sourcemap: config.flags?.sourcemap ?? true,
99
+ format: config.flags?.format ?? "esm",
100
+ target: config.flags?.target ?? "esnext",
101
+ };
96
102
 
97
103
  // If child is a file WorkFile, use its absolute path directly
98
104
  if (child.op === "file" && child.config.absPath) {
@@ -101,10 +107,7 @@ export function op_compile(config, children) {
101
107
  const result = esbuild.buildSync({
102
108
  entryPoints: [entry],
103
109
  bundle: true,
104
- minify: true,
105
- sourcemap: true,
106
- format: "esm",
107
- target: "esnext",
110
+ ...compileOptions,
108
111
  write: false
109
112
  });
110
113
 
@@ -125,10 +128,7 @@ export function op_compile(config, children) {
125
128
  sourcefile: "input.ts"
126
129
  },
127
130
  bundle: true,
128
- minify: true,
129
- sourcemap: true,
130
- format: "esm",
131
- target: "esnext",
131
+ ...compileOptions,
132
132
  write: false
133
133
  });
134
134
 
@@ -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
+ }
@@ -0,0 +1,142 @@
1
+ // buldng-web.ts
2
+ // Runtime-only listener installer for BULDR.
3
+ // This module contains NO production logic and NO module-level state.
4
+ // It is safe to be bundled multiple times.
5
+
6
+ export { showModal } from "./buldng-modal";
7
+ export type {
8
+ ModalIcon,
9
+ ModalResult,
10
+ ModalSettings,
11
+ ModalType,
12
+ } from "./buldng-modal";
13
+
14
+ export function setErrorPage(page: string) {
15
+ if (typeof page !== "string" || !page.length) {
16
+ throw new Error("setErrorPage: page must be a non-empty string");
17
+ }
18
+
19
+ // Capture `page` in closure — NOT stored on window
20
+ window.addEventListener("error", (event) => {
21
+ window.__error_handler?.(event, page);
22
+ });
23
+
24
+ window.addEventListener("unhandledrejection", (event) => {
25
+ window.__error_handler?.(event, page);
26
+ });
27
+
28
+ console.log(`BULDR: Error handler listener installed, redirect ${page}`);
29
+
30
+ }
31
+
32
+ // ------------------------------------------------------------
33
+ // ERROR PAGE SUPPORT — moved from layout-err.ts
34
+ // ------------------------------------------------------------
35
+
36
+ import type { ErrorPayload } from "./layout-err"; // or wherever you export it
37
+
38
+ export function readErrorPayload(id: string | null): ErrorPayload | null {
39
+ if (!id) return null;
40
+
41
+ try {
42
+ const raw = localStorage.getItem(storageKey(id));
43
+ if (!raw) return null;
44
+
45
+ const payload: unknown = JSON.parse(raw);
46
+ return isErrorPayload(payload) ? payload : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ export function consumeErrorPayload(id: string | null): ErrorPayload | null {
53
+ const payload = readErrorPayload(id);
54
+ if (id) {
55
+ try {
56
+ localStorage.removeItem(storageKey(id));
57
+ } catch {}
58
+ }
59
+ return payload;
60
+ }
61
+
62
+ export function renderErrorPayload(
63
+ elementId: string,
64
+ payload: ErrorPayload | null
65
+ ): ErrorPayload | null {
66
+ const container = document.getElementById(elementId);
67
+ if (!container) return null;
68
+
69
+ container.replaceChildren();
70
+
71
+ if (!payload) {
72
+ const empty = document.createElement("p");
73
+ empty.textContent = "Error details are no longer available.";
74
+ container.appendChild(empty);
75
+ return null;
76
+ }
77
+
78
+ const details: Array<[string, string]> = [
79
+ ["Type", payload.type],
80
+ ["Message", payload.message],
81
+ ["Time", payload.time],
82
+ ["Page", payload.url],
83
+ ["User agent", payload.userAgent],
84
+ ["Platform", payload.platform],
85
+ ["Language", payload.language],
86
+ ];
87
+
88
+ const list = document.createElement("dl");
89
+
90
+ for (const [label, value] of details) {
91
+ const term = document.createElement("dt");
92
+ term.textContent = label;
93
+
94
+ const description = document.createElement("dd");
95
+ description.textContent = value;
96
+
97
+ list.append(term, description);
98
+ }
99
+
100
+ if (payload.stack) {
101
+ const stackLabel = document.createElement("h3");
102
+ stackLabel.textContent = "Stack";
103
+
104
+ const stack = document.createElement("pre");
105
+ stack.textContent = payload.stack;
106
+
107
+ list.append(stackLabel, stack);
108
+ }
109
+
110
+ container.appendChild(list);
111
+ return payload;
112
+ }
113
+
114
+ export function getErrorId(): string | null {
115
+ return new URLSearchParams(location.search).get("errorId");
116
+ }
117
+
118
+ // ------------------------------------------------------------
119
+ // TYPE GUARDS & UTILITIES — also needed by error page
120
+ // ------------------------------------------------------------
121
+
122
+ function isErrorPayload(value: unknown): value is ErrorPayload {
123
+ if (!value || typeof value !== "object") return false;
124
+
125
+ const p = value as Partial<ErrorPayload>;
126
+
127
+ return (
128
+ typeof p.id === "string" &&
129
+ (p.type === "error" || p.type === "unhandledrejection") &&
130
+ typeof p.message === "string" &&
131
+ typeof p.stack === "string" &&
132
+ typeof p.time === "string" &&
133
+ typeof p.url === "string" &&
134
+ typeof p.userAgent === "string" &&
135
+ typeof p.platform === "string" &&
136
+ typeof p.language === "string"
137
+ );
138
+ }
139
+
140
+ function storageKey(id: string) {
141
+ return `buldng:error:${id}`;
142
+ }