@pajh/buldng 0.0.4 → 0.0.6

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.
package/src/buldng-web.ts CHANGED
@@ -1,76 +1,41 @@
1
1
  // buldng-web.ts
2
- // Pure runtime API for buldng. The optional renderer only writes text into a caller-owned element.
3
-
4
- let errorPage: string | null = null;
5
- let listenersInstalled = false;
6
- let redirectStarted = false;
7
-
8
- declare global {
9
- interface Window {
10
- __buldngErrorOverlay?: boolean;
11
- }
12
- }
13
-
14
- const ERROR_STORAGE_PREFIX = "buldng:error:";
15
-
16
- export type ErrorPayload = {
17
- id: string;
18
- type: "error" | "unhandledrejection";
19
- message: string;
20
- stack: string;
21
- time: string;
22
- url: string;
23
- userAgent: string;
24
- platform: string;
25
- language: string;
26
- };
27
-
28
- /**
29
- * Configure the global error handler.
30
- * @param url A full URL path like "/error/" or "/fatal/".
31
- */
32
- export function setErrorPage(url: string) {
33
- if (typeof url !== "string" || !url.length) {
34
- throw new Error("setErrorPage: URL must be a non-empty string");
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");
35
17
  }
36
18
 
37
- // Normalize: ensure no accidental double slashes
38
- errorPage = url.endsWith("/") ? url.slice(0, -1) : url;
39
-
40
- // Install global listeners once. Calling setErrorPage again only changes the destination.
41
- installGlobalErrorHandler();
42
- }
43
-
44
- /**
45
- * Internal: installs global error + unhandled rejection handlers.
46
- */
47
- function installGlobalErrorHandler() {
48
- if (!errorPage || listenersInstalled || isErrorPage() || window.__buldngErrorOverlay) return;
49
-
50
- listenersInstalled = true;
51
-
19
+ // Capture `page` in closure NOT stored on window
52
20
  window.addEventListener("error", (event) => {
53
- handleError({
54
- type: "error",
55
- reason: event.error ?? event.message,
56
- fallbackMessage: event.message,
57
- });
21
+ window.__error_handler?.(event, page);
58
22
  });
59
23
 
60
24
  window.addEventListener("unhandledrejection", (event) => {
61
- handleError({
62
- type: "unhandledrejection",
63
- reason: event.reason,
64
- fallbackMessage: "Unhandled promise rejection",
65
- });
25
+ window.__error_handler?.(event, page);
66
26
  });
27
+
28
+ console.log(`BULDR: Error handler listener installed, redirect ${page}`);
29
+
67
30
  }
68
31
 
69
- /**
70
- * Read an error payload previously saved by the global handler.
71
- * Defaults to the errorId query parameter on the current page.
72
- */
73
- export function readErrorPayload(id = getErrorId()): ErrorPayload | null {
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 {
74
39
  if (!id) return null;
75
40
 
76
41
  try {
@@ -84,33 +49,25 @@ export function readErrorPayload(id = getErrorId()): ErrorPayload | null {
84
49
  }
85
50
  }
86
51
 
87
- /**
88
- * Read and remove an error payload. Useful for one-shot error pages.
89
- */
90
- export function consumeErrorPayload(id = getErrorId()): ErrorPayload | null {
52
+ export function consumeErrorPayload(id: string | null): ErrorPayload | null {
91
53
  const payload = readErrorPayload(id);
92
54
  if (id) {
93
55
  try {
94
56
  localStorage.removeItem(storageKey(id));
95
- } catch {
96
- // Storage may be unavailable or blocked; the payload was still safely read if possible.
97
- }
57
+ } catch {}
98
58
  }
99
59
  return payload;
100
60
  }
101
61
 
102
- /**
103
- * Render a payload as safe text inside a caller-owned element.
104
- * Returns the payload that was rendered, or null when it was unavailable.
105
- */
106
62
  export function renderErrorPayload(
107
63
  elementId: string,
108
- payload = readErrorPayload()
64
+ payload: ErrorPayload | null
109
65
  ): ErrorPayload | null {
110
66
  const container = document.getElementById(elementId);
111
67
  if (!container) return null;
112
68
 
113
69
  container.replaceChildren();
70
+
114
71
  if (!payload) {
115
72
  const empty = document.createElement("p");
116
73
  empty.textContent = "Error details are no longer available.";
@@ -129,19 +86,24 @@ export function renderErrorPayload(
129
86
  ];
130
87
 
131
88
  const list = document.createElement("dl");
89
+
132
90
  for (const [label, value] of details) {
133
91
  const term = document.createElement("dt");
134
92
  term.textContent = label;
93
+
135
94
  const description = document.createElement("dd");
136
95
  description.textContent = value;
96
+
137
97
  list.append(term, description);
138
98
  }
139
99
 
140
100
  if (payload.stack) {
141
101
  const stackLabel = document.createElement("h3");
142
102
  stackLabel.textContent = "Stack";
103
+
143
104
  const stack = document.createElement("pre");
144
105
  stack.textContent = payload.stack;
106
+
145
107
  list.append(stackLabel, stack);
146
108
  }
147
109
 
@@ -149,119 +111,32 @@ export function renderErrorPayload(
149
111
  return payload;
150
112
  }
151
113
 
152
- /**
153
- * Return the error record ID from the current URL.
154
- */
155
114
  export function getErrorId(): string | null {
156
115
  return new URLSearchParams(location.search).get("errorId");
157
116
  }
158
117
 
159
- function handleError(info: {
160
- type: ErrorPayload["type"];
161
- reason: unknown;
162
- fallbackMessage: string;
163
- }) {
164
- if (redirectStarted || !errorPage) return;
165
- redirectStarted = true;
166
-
167
- const payload = buildPayload(info);
168
- const stored = saveErrorPayload(payload);
169
- redirect(payload, stored);
170
- }
171
-
172
- function buildPayload(info: {
173
- type: ErrorPayload["type"];
174
- reason: unknown;
175
- fallbackMessage: string;
176
- }): ErrorPayload {
177
- const id = createId();
178
- const error = info.reason instanceof Error ? info.reason : null;
118
+ // ------------------------------------------------------------
119
+ // TYPE GUARDS & UTILITIES — also needed by error page
120
+ // ------------------------------------------------------------
179
121
 
180
- return {
181
- id,
182
- type: info.type,
183
- message: getErrorMessage(info.reason, info.fallbackMessage),
184
- stack: error?.stack ?? getErrorStack(info.reason),
185
- time: new Date().toISOString(),
186
- url: location.href,
187
- userAgent: navigator.userAgent,
188
- platform: navigator.platform,
189
- language: navigator.language,
190
- };
191
- }
192
-
193
- /**
194
- * Redirect to the configured error page with a storage key, plus a small fallback.
195
- */
196
- function redirect(payload: ErrorPayload, stored: boolean) {
197
- if (!errorPage) return;
198
-
199
- const destination = new URL(errorPage, location.href);
200
- if (stored) {
201
- destination.searchParams.set("errorId", payload.id);
202
- } else {
203
- destination.searchParams.set("type", payload.type);
204
- destination.searchParams.set("message", payload.message.slice(0, 500));
205
- }
122
+ function isErrorPayload(value: unknown): value is ErrorPayload {
123
+ if (!value || typeof value !== "object") return false;
206
124
 
207
- window.location.href = destination.href;
208
- }
125
+ const p = value as Partial<ErrorPayload>;
209
126
 
210
- function saveErrorPayload(payload: ErrorPayload): boolean {
211
- try {
212
- localStorage.setItem(storageKey(payload.id), JSON.stringify(payload));
213
- return true;
214
- } catch {
215
- return false;
216
- }
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
+ );
217
138
  }
218
139
 
219
140
  function storageKey(id: string) {
220
- return `${ERROR_STORAGE_PREFIX}${id}`;
221
- }
222
-
223
- function createId() {
224
- if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
225
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
226
- }
227
-
228
- function isErrorPage() {
229
- const destination = new URL(errorPage!, location.href);
230
- const destinationPath = destination.pathname.replace(/\/$/, "") || "/";
231
- const currentPath = location.pathname.replace(/\/$/, "") || "/";
232
- return destination.origin === location.origin && destinationPath === currentPath;
233
- }
234
-
235
- function getErrorMessage(reason: unknown, fallback: string) {
236
- if (reason instanceof Error && reason.message) return reason.message;
237
- if (typeof reason === "string" && reason) return reason;
238
- if (reason !== null && reason !== undefined) {
239
- try {
240
- const text = JSON.stringify(reason);
241
- if (text) return text;
242
- } catch {
243
- // Fall through to the stable message below.
244
- }
245
- }
246
- return fallback;
247
- }
248
-
249
- function getErrorStack(reason: unknown) {
250
- if (!reason || typeof reason !== "object") return "";
251
- const stack = (reason as { stack?: unknown }).stack;
252
- return typeof stack === "string" ? stack : "";
253
- }
254
-
255
- function isErrorPayload(value: unknown): value is ErrorPayload {
256
- if (!value || typeof value !== "object") return false;
257
- const payload = value as Partial<ErrorPayload>;
258
- return typeof payload.id === "string"
259
- && (payload.type === "error" || payload.type === "unhandledrejection")
260
- && typeof payload.message === "string"
261
- && typeof payload.stack === "string"
262
- && typeof payload.time === "string"
263
- && typeof payload.url === "string"
264
- && typeof payload.userAgent === "string"
265
- && typeof payload.platform === "string"
266
- && typeof payload.language === "string";
141
+ return `buldng:error:${id}`;
267
142
  }
package/src/buldng.css CHANGED
@@ -146,3 +146,133 @@ button {
146
146
  background: var(--button);
147
147
  }
148
148
 
149
+ /* --------------------------------------------------
150
+ MODAL SYSTEM
151
+ -------------------------------------------------- */
152
+
153
+ #b-modal-root {
154
+ position: fixed;
155
+ inset: 0;
156
+ z-index: 1000;
157
+ pointer-events: none;
158
+ }
159
+
160
+ #b-modal-root .buldng-modal-backdrop {
161
+ position: absolute;
162
+ inset: 0;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ padding: 20px;
167
+ background: var(--modal-backdrop-color, rgba(0, 0, 0, 0.5));
168
+ pointer-events: auto;
169
+ }
170
+ #b-modal-root .buldng-modal {
171
+ display: flex;
172
+ flex-direction: column;
173
+ width: min(100%, 480px);
174
+ max-height: min(100%, 720px);
175
+ overflow: auto;
176
+ color: var(--text, #222222);
177
+ background: var(--modal-bg, var(--panel, #ffffff));
178
+ border: 2px solid var(--modal-accent-color, #0066cc);
179
+ border-radius: var(--modal-radius, 12px);
180
+ box-shadow: var(--modal-shadow, 0 4px 20px rgba(0, 0, 0, 0.3));
181
+ }
182
+
183
+ #b-modal-root .buldng-modal-header {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 12px;
187
+ padding: var(--modal-padding, 20px) var(--modal-padding, 20px) 0;
188
+ }
189
+
190
+ #b-modal-root .buldng-modal-icon {
191
+ display: inline-flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+ flex: 0 0 32px;
195
+ width: 32px;
196
+ height: 32px;
197
+ color: var(--modal-accent-color, #0066cc);
198
+ font-weight: 700;
199
+ border: 2px solid currentColor;
200
+ border-radius: 50%;
201
+ }
202
+
203
+ #b-modal-root .buldng-modal-title {
204
+ margin: 0;
205
+ color: inherit;
206
+ font-size: 1.2em;
207
+ }
208
+
209
+ #b-modal-root .buldng-modal-content {
210
+ padding: var(--modal-padding, 20px);
211
+ overflow: auto;
212
+ }
213
+
214
+ #b-modal-root .buldng-modal-footer {
215
+ display: flex;
216
+ justify-content: flex-end;
217
+ gap: 10px;
218
+ padding: 0 var(--modal-padding, 20px) var(--modal-padding, 20px);
219
+ }
220
+
221
+ #b-modal-root .buldng-modal-button {
222
+ min-width: 88px;
223
+ min-height: 44px;
224
+ padding: 8px 16px;
225
+ color: var(--text, #222222);
226
+ font: inherit;
227
+ font-weight: 600;
228
+ background: var(--button, #f0f0f0);
229
+ border: 1px solid var(--line, #9c9c9c);
230
+ border-radius: 4px;
231
+ cursor: pointer;
232
+ }
233
+
234
+ #b-modal-root .buldng-modal-button-primary {
235
+ color: #ffffff;
236
+ background: var(--modal-accent-color, #0066cc);
237
+ border-color: var(--modal-accent-color, #0066cc);
238
+ }
239
+
240
+ #b-modal-root .buldng-modal-button:focus-visible {
241
+ outline: 3px solid var(--highlight1, #4aa3ff);
242
+ outline-offset: 2px;
243
+ }
244
+
245
+ #b-modal-root .buldng-modal.info {
246
+ --modal-accent-color: #0066cc;
247
+ }
248
+
249
+ #b-modal-root .buldng-modal.confirmation {
250
+ --modal-accent-color: #444444;
251
+ }
252
+
253
+ #b-modal-root .buldng-modal.destructive,
254
+ #b-modal-root .buldng-modal.error {
255
+ --modal-accent-color: #cc0000;
256
+ }
257
+
258
+ @media (max-width: 520px) {
259
+ #b-modal-root .buldng-modal-backdrop {
260
+ align-items: flex-end;
261
+ padding: 0;
262
+ }
263
+
264
+ #b-modal-root .buldng-modal {
265
+ width: 100%;
266
+ max-height: 90%;
267
+ border-radius: var(--modal-radius, 12px) var(--modal-radius, 12px) 0 0;
268
+ }
269
+
270
+ #b-modal-root .buldng-modal-footer {
271
+ flex-direction: column-reverse;
272
+ }
273
+
274
+ #b-modal-root .buldng-modal-button {
275
+ width: 100%;
276
+ }
277
+ }
278
+
@@ -0,0 +1,16 @@
1
+ // Development-only override. setErrorPage() owns the event listeners.
2
+ console.log("[dev-errors] overriding window.__error_handler");
3
+
4
+ window.__error_handler = (event, page) => {
5
+ const reason =
6
+ event.type === "error"
7
+ ? event.error ?? event.message
8
+ : event.reason;
9
+
10
+ const message =
11
+ reason instanceof Error
12
+ ? reason.message
13
+ : String(reason || "Unknown error");
14
+
15
+ alert(`BULDR DEV ERROR:\n\n${message}\n\nCheck DevTools for full details.`);
16
+ };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * ============================================================
3
+ * BULDR PRODUCTION ERROR SUBSYSTEM — layout-err.ts
4
+ * ============================================================
5
+ *
6
+ * This module is bundled ONLY by layout.ts.
7
+ * It owns ALL production error logic:
8
+ * - capturing errors
9
+ * - building payloads
10
+ * - storing payloads
11
+ * - redirecting to the error page
12
+ *
13
+ * buldng-web.ts owns:
14
+ * - reading payloads
15
+ * - consuming payloads
16
+ * - rendering payloads
17
+ * - runtime utilities
18
+ *
19
+ * error-main.ts owns:
20
+ * - UI for the error page
21
+ *
22
+ * ============================================================
23
+ */
24
+
25
+ const ERROR_STORAGE_PREFIX = "buldng:error:";
26
+
27
+ // ------------------------------------------------------------
28
+ // PUBLIC API
29
+ // ------------------------------------------------------------
30
+
31
+ /**
32
+ * Install the production error handler wrapper.
33
+ * layout.ts calls this once during startup.
34
+ */
35
+ export function installErrorHandler() {
36
+ window.__error_handler = (event, page) => {
37
+ handleError(event, page);
38
+ };
39
+ console.log("BULDR: Production error handler installed");
40
+ }
41
+
42
+ // ------------------------------------------------------------
43
+ // CORE PRODUCTION LOGIC
44
+ // ------------------------------------------------------------
45
+
46
+ function handleError(
47
+ event: ErrorEvent | PromiseRejectionEvent,
48
+ page: string
49
+ ) {
50
+ if (window.__buldngErrorOverlay) return;
51
+
52
+ const info =
53
+ event instanceof ErrorEvent
54
+ ? {
55
+ type: "error" as const,
56
+ reason: event.error ?? event.message,
57
+ fallbackMessage: event.message,
58
+ }
59
+ : {
60
+ type: "unhandledrejection" as const,
61
+ reason: event.reason,
62
+ fallbackMessage: "Unhandled promise rejection",
63
+ };
64
+
65
+ const payload = buildPayload(info);
66
+ const stored = saveErrorPayload(payload);
67
+
68
+ redirect(payload, stored, page);
69
+ }
70
+
71
+ // ------------------------------------------------------------
72
+ // PAYLOAD BUILDING
73
+ // ------------------------------------------------------------
74
+
75
+ export type ErrorPayload = {
76
+ id: string;
77
+ type: "error" | "unhandledrejection";
78
+ message: string;
79
+ stack: string;
80
+ time: string;
81
+ url: string;
82
+ userAgent: string;
83
+ platform: string;
84
+ language: string;
85
+ };
86
+
87
+ function buildPayload(info: {
88
+ type: ErrorPayload["type"];
89
+ reason: unknown;
90
+ fallbackMessage: string;
91
+ }): ErrorPayload {
92
+ const id = createId();
93
+ const error = info.reason instanceof Error ? info.reason : null;
94
+
95
+ return {
96
+ id,
97
+ type: info.type,
98
+ message: getErrorMessage(info.reason, info.fallbackMessage),
99
+ stack: error?.stack ?? getErrorStack(info.reason),
100
+ time: new Date().toISOString(),
101
+ url: location.href,
102
+ userAgent: navigator.userAgent,
103
+ platform: navigator.platform,
104
+ language: navigator.language,
105
+ };
106
+ }
107
+
108
+ // ------------------------------------------------------------
109
+ // REDIRECT
110
+ // ------------------------------------------------------------
111
+
112
+ function redirect(payload: ErrorPayload, stored: boolean, page: string) {
113
+ const destination = new URL(page, location.href);
114
+
115
+ if (stored) {
116
+ destination.searchParams.set("errorId", payload.id);
117
+ } else {
118
+ destination.searchParams.set("type", payload.type);
119
+ destination.searchParams.set("message", payload.message.slice(0, 500));
120
+ }
121
+
122
+ window.location.href = destination.href;
123
+ }
124
+
125
+ // ------------------------------------------------------------
126
+ // STORAGE (production only)
127
+ // ------------------------------------------------------------
128
+
129
+ function saveErrorPayload(payload: ErrorPayload): boolean {
130
+ try {
131
+ localStorage.setItem(storageKey(payload.id), JSON.stringify(payload));
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ function storageKey(id: string) {
139
+ return `${ERROR_STORAGE_PREFIX}${id}`;
140
+ }
141
+
142
+ function createId() {
143
+ if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
144
+ return `${Date.now().toString(36)}-${Math.random()
145
+ .toString(36)
146
+ .slice(2)}`;
147
+ }
148
+
149
+ // ------------------------------------------------------------
150
+ // UTILITIES (production only)
151
+ // ------------------------------------------------------------
152
+
153
+ function getErrorMessage(reason: unknown, fallback: string) {
154
+ if (reason instanceof Error && reason.message) return reason.message;
155
+ if (typeof reason === "string" && reason) return reason;
156
+
157
+ if (reason !== null && reason !== undefined) {
158
+ try {
159
+ const text = JSON.stringify(reason);
160
+ if (text) return text;
161
+ } catch {}
162
+ }
163
+
164
+ return fallback;
165
+ }
166
+
167
+ function getErrorStack(reason: unknown) {
168
+ if (!reason || typeof reason !== "object") return "";
169
+ const stack = (reason as { stack?: unknown }).stack;
170
+ return typeof stack === "string" ? stack : "";
171
+ }
package/src/layout.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { installErrorHandler } from "./layout-err";
2
+
3
+ installErrorHandler();
4
+
1
5
  /* --------------------------------------------------
2
6
  GLOBAL VIEWPORT STATE
3
7
  -------------------------------------------------- */