@ossido-labs/ossido-ui 0.1.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.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/esm/components/BaseStyles.d.ts +8 -0
- package/dist/esm/components/BaseStyles.js +17 -0
- package/dist/esm/components/BaseStyles.js.map +1 -0
- package/dist/esm/components/DefaultError.d.ts +9 -0
- package/dist/esm/components/DefaultError.js +30 -0
- package/dist/esm/components/DefaultError.js.map +1 -0
- package/dist/esm/components/DefaultLoading.d.ts +6 -0
- package/dist/esm/components/DefaultLoading.js +13 -0
- package/dist/esm/components/DefaultLoading.js.map +1 -0
- package/dist/esm/components/DefaultScreen.d.ts +19 -0
- package/dist/esm/components/DefaultScreen.js +96 -0
- package/dist/esm/components/DefaultScreen.js.map +1 -0
- package/dist/esm/components/DevBuildErrorContent.d.ts +11 -0
- package/dist/esm/components/DevBuildErrorContent.js +56 -0
- package/dist/esm/components/DevBuildErrorContent.js.map +1 -0
- package/dist/esm/components/DevErrorContent.d.ts +4 -0
- package/dist/esm/components/DevErrorContent.js +192 -0
- package/dist/esm/components/DevErrorContent.js.map +1 -0
- package/dist/esm/components/DevErrorOverlay.d.ts +13 -0
- package/dist/esm/components/DevErrorOverlay.js +53 -0
- package/dist/esm/components/DevErrorOverlay.js.map +1 -0
- package/dist/esm/components/DevErrorOverlayHost.d.ts +2 -0
- package/dist/esm/components/DevErrorOverlayHost.js +295 -0
- package/dist/esm/components/DevErrorOverlayHost.js.map +1 -0
- package/dist/esm/components/DevErrorReporter.d.ts +13 -0
- package/dist/esm/components/DevErrorReporter.js +28 -0
- package/dist/esm/components/DevErrorReporter.js.map +1 -0
- package/dist/esm/components/base.js +6 -0
- package/dist/esm/components/base.js.map +1 -0
- package/dist/esm/components/devErrorSource.d.ts +96 -0
- package/dist/esm/components/devErrorSource.js +321 -0
- package/dist/esm/components/devErrorSource.js.map +1 -0
- package/dist/esm/components/devErrorStore.d.ts +105 -0
- package/dist/esm/components/devErrorStore.js +186 -0
- package/dist/esm/components/devErrorStore.js.map +1 -0
- package/dist/esm/components/devErrorStyles.d.ts +6 -0
- package/dist/esm/components/devErrorStyles.js +426 -0
- package/dist/esm/components/devErrorStyles.js.map +1 -0
- package/dist/esm/index.d.ts +11 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/types.d.ts +26 -0
- package/dist/esm/vite/error-overlay.d.ts +32 -0
- package/dist/esm/vite/error-overlay.js +49 -0
- package/dist/esm/vite/error-overlay.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BaseStyles } from "./BaseStyles.js";
|
|
2
|
+
import { DevErrorContent } from "./DevErrorContent.js";
|
|
3
|
+
import { DEV_ERROR_STYLES } from "./devErrorStyles.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/components/DevErrorOverlay.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Standalone single-error overlay window, presented as a floating window over a
|
|
9
|
+
* dimmed backdrop. The live dev app uses {@link DevErrorOverlayHost} instead —
|
|
10
|
+
* it is store-driven and browses every kind of dev error with a pager — but
|
|
11
|
+
* this renders a single error directly, which is handy for stories and any
|
|
12
|
+
* caller that already has one `Error` in hand.
|
|
13
|
+
*
|
|
14
|
+
* Never used in production: {@link RouteMatch} selects the detail-free
|
|
15
|
+
* production fallback, so source and internals are not leaked to end users.
|
|
16
|
+
*/
|
|
17
|
+
function DevErrorOverlay({ error, reset }) {
|
|
18
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
19
|
+
className: "ossido-err-backdrop",
|
|
20
|
+
role: "alertdialog",
|
|
21
|
+
"aria-modal": "true",
|
|
22
|
+
children: [
|
|
23
|
+
/* @__PURE__ */ jsx(BaseStyles, {}),
|
|
24
|
+
/* @__PURE__ */ jsx("style", { children: DEV_ERROR_STYLES }),
|
|
25
|
+
/* @__PURE__ */ jsx("div", {
|
|
26
|
+
className: "ossido-err-window",
|
|
27
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
28
|
+
className: "ossido-err-body",
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ jsx(DevErrorContent, { error }),
|
|
31
|
+
/* @__PURE__ */ jsx("div", {
|
|
32
|
+
className: "ossido-err-actions",
|
|
33
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
34
|
+
type: "button",
|
|
35
|
+
className: "ossido-err-btn",
|
|
36
|
+
onClick: reset,
|
|
37
|
+
children: "Try again"
|
|
38
|
+
})
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ jsx("p", {
|
|
41
|
+
className: "ossido-err-hint",
|
|
42
|
+
children: "This overlay is only shown in development."
|
|
43
|
+
})
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { DevErrorOverlay };
|
|
53
|
+
//# sourceMappingURL=DevErrorOverlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevErrorOverlay.js","names":[],"sources":["../../../src/components/DevErrorOverlay.tsx"],"sourcesContent":["import type { JSX } from 'react';\n\nimport type { OssidoErrorProps } from '../types';\n\nimport { BaseStyles } from './BaseStyles';\nimport { DevErrorContent } from './DevErrorContent';\nimport { DEV_ERROR_STYLES } from './devErrorStyles';\n\n/**\n * Standalone single-error overlay window, presented as a floating window over a\n * dimmed backdrop. The live dev app uses {@link DevErrorOverlayHost} instead —\n * it is store-driven and browses every kind of dev error with a pager — but\n * this renders a single error directly, which is handy for stories and any\n * caller that already has one `Error` in hand.\n *\n * Never used in production: {@link RouteMatch} selects the detail-free\n * production fallback, so source and internals are not leaked to end users.\n */\nexport function DevErrorOverlay({\n error,\n reset,\n}: OssidoErrorProps): JSX.Element {\n return (\n <div className=\"ossido-err-backdrop\" role=\"alertdialog\" aria-modal=\"true\">\n <BaseStyles />\n <style>{DEV_ERROR_STYLES}</style>\n <div className=\"ossido-err-window\">\n <div className=\"ossido-err-body\">\n <DevErrorContent error={error} />\n <div className=\"ossido-err-actions\">\n <button type=\"button\" className=\"ossido-err-btn\" onClick={reset}>\n Try again\n </button>\n </div>\n <p className=\"ossido-err-hint\">\n This overlay is only shown in development.\n </p>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,SAAgB,gBAAgB,EAC9B,OACA,SACgC;CAChC,OACE,qBAAC,OAAD;EAAK,WAAU;EAAsB,MAAK;EAAc,cAAW;YAAnE;GACE,oBAAC,YAAD,CAAa;GACb,oBAAC,SAAD,YAAQ,iBAAwB;GAChC,oBAAC,OAAD;IAAK,WAAU;cACb,qBAAC,OAAD;KAAK,WAAU;eAAf;MACE,oBAAC,iBAAD,EAAwB,MAAQ;MAChC,oBAAC,OAAD;OAAK,WAAU;iBACb,oBAAC,UAAD;QAAQ,MAAK;QAAS,WAAU;QAAiB,SAAS;kBAAO;OAEzD;MACL;MACL,oBAAC,KAAD;OAAG,WAAU;iBAAkB;MAE5B;KACA;;GACF;EACF;;AAET"}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { BaseStyles } from "./BaseStyles.js";
|
|
2
|
+
import { DevErrorContent } from "./DevErrorContent.js";
|
|
3
|
+
import { DEV_ERROR_STYLES } from "./devErrorStyles.js";
|
|
4
|
+
import { DevBuildErrorContent } from "./DevBuildErrorContent.js";
|
|
5
|
+
import { DEV_OVERLAY_CORNERS, devErrorStore } from "./devErrorStore.js";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
8
|
+
import { createPortal } from "react-dom";
|
|
9
|
+
|
|
10
|
+
//#region src/components/DevErrorOverlayHost.tsx
|
|
11
|
+
/**
|
|
12
|
+
* Development-only host for the unified error overlay and its persistent dev
|
|
13
|
+
* indicator. Mounted once at the router root (dev only), it subscribes to
|
|
14
|
+
* {@link devErrorStore} and renders:
|
|
15
|
+
*
|
|
16
|
+
* - a persistent corner **indicator** (always visible in dev) whose menu offers
|
|
17
|
+
* "View errors", a corner picker (persisted), and "Hide until reload", and
|
|
18
|
+
* - the **error overlay** itself — a browsable window for every kind of dev
|
|
19
|
+
* error (React render errors, SSR/Rust panics, uncaught errors, unhandled
|
|
20
|
+
* rejections, Vite build errors), auto-opened when a new error arrives.
|
|
21
|
+
*
|
|
22
|
+
* Never rendered in production: {@link RouteMatch} only mounts it in dev, so no
|
|
23
|
+
* source or internals leak to end users.
|
|
24
|
+
*/
|
|
25
|
+
const KIND_LABEL = {
|
|
26
|
+
runtime: "Runtime error",
|
|
27
|
+
uncaught: "Uncaught error",
|
|
28
|
+
unhandledrejection: "Unhandled rejection",
|
|
29
|
+
build: "Build error"
|
|
30
|
+
};
|
|
31
|
+
const CORNER_LABEL = {
|
|
32
|
+
"top-left": "Top left",
|
|
33
|
+
"top-right": "Top right",
|
|
34
|
+
"bottom-left": "Bottom left",
|
|
35
|
+
"bottom-right": "Bottom right"
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* React dispatches these to `window` when server rendering fails and it recovers
|
|
39
|
+
* by client-rendering (or when hydration falls back). They carry no stack and
|
|
40
|
+
* aren't actionable — the underlying error is already reported by the route
|
|
41
|
+
* boundary — so they are dropped to avoid a duplicate, contentless entry.
|
|
42
|
+
*/
|
|
43
|
+
const IGNORED_UNCAUGHT_MESSAGES = [
|
|
44
|
+
"could not finish this Suspense boundary",
|
|
45
|
+
"Switched to client rendering",
|
|
46
|
+
"error during server rendering",
|
|
47
|
+
"hydrating but received"
|
|
48
|
+
];
|
|
49
|
+
function isIgnorableUncaught(error) {
|
|
50
|
+
return IGNORED_UNCAUGHT_MESSAGES.some((needle) => error.message.includes(needle));
|
|
51
|
+
}
|
|
52
|
+
/** Normalize an unhandled rejection reason into an `Error`. */
|
|
53
|
+
function toError(reason) {
|
|
54
|
+
if (reason instanceof Error) return reason;
|
|
55
|
+
const error = new Error(typeof reason === "string" ? reason : JSON.stringify(reason));
|
|
56
|
+
error.name = "UnhandledRejection";
|
|
57
|
+
return error;
|
|
58
|
+
}
|
|
59
|
+
function Styles() {
|
|
60
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(BaseStyles, {}), /* @__PURE__ */ jsx("style", { children: DEV_ERROR_STYLES })] });
|
|
61
|
+
}
|
|
62
|
+
function EntryBody({ entry }) {
|
|
63
|
+
if (entry.kind === "build" && entry.build) return /* @__PURE__ */ jsx(DevBuildErrorContent, { build: entry.build });
|
|
64
|
+
if (entry.error) return /* @__PURE__ */ jsx(DevErrorContent, { error: entry.error }, entry.id);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function ChevronLeftIcon() {
|
|
68
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
69
|
+
viewBox: "0 0 24 24",
|
|
70
|
+
fill: "none",
|
|
71
|
+
stroke: "currentColor",
|
|
72
|
+
strokeWidth: 2,
|
|
73
|
+
strokeLinecap: "round",
|
|
74
|
+
strokeLinejoin: "round",
|
|
75
|
+
"aria-hidden": true,
|
|
76
|
+
children: /* @__PURE__ */ jsx("path", { d: "M15 18l-6-6 6-6" })
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function ChevronRightIcon() {
|
|
80
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
81
|
+
viewBox: "0 0 24 24",
|
|
82
|
+
fill: "none",
|
|
83
|
+
stroke: "currentColor",
|
|
84
|
+
strokeWidth: 2,
|
|
85
|
+
strokeLinecap: "round",
|
|
86
|
+
strokeLinejoin: "round",
|
|
87
|
+
"aria-hidden": true,
|
|
88
|
+
children: /* @__PURE__ */ jsx("path", { d: "M9 6l6 6-6 6" })
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** The same lightning bolt Ossido prints in the console. */
|
|
92
|
+
function BoltIcon() {
|
|
93
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
94
|
+
viewBox: "0 0 24 24",
|
|
95
|
+
fill: "currentColor",
|
|
96
|
+
"aria-hidden": true,
|
|
97
|
+
children: /* @__PURE__ */ jsx("path", { d: "M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z" })
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function DevErrorOverlay({ count }) {
|
|
101
|
+
const state = devErrorStore.getSnapshot();
|
|
102
|
+
const activeIndex = Math.min(state.activeIndex, count - 1);
|
|
103
|
+
const entry = state.entries[activeIndex];
|
|
104
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
105
|
+
className: "ossido-err-backdrop",
|
|
106
|
+
role: "alertdialog",
|
|
107
|
+
"aria-modal": "true",
|
|
108
|
+
onClick: () => devErrorStore.closeOverlay(),
|
|
109
|
+
children: [/* @__PURE__ */ jsx(Styles, {}), /* @__PURE__ */ jsxs("div", {
|
|
110
|
+
className: "ossido-err-window",
|
|
111
|
+
onClick: (event) => event.stopPropagation(),
|
|
112
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
113
|
+
className: "ossido-err-pager",
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsx("button", {
|
|
116
|
+
type: "button",
|
|
117
|
+
className: "ossido-err-nav",
|
|
118
|
+
"aria-label": "Previous error",
|
|
119
|
+
disabled: count < 2,
|
|
120
|
+
onClick: () => devErrorStore.prev(),
|
|
121
|
+
children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
|
|
122
|
+
}),
|
|
123
|
+
/* @__PURE__ */ jsx("button", {
|
|
124
|
+
type: "button",
|
|
125
|
+
className: "ossido-err-nav",
|
|
126
|
+
"aria-label": "Next error",
|
|
127
|
+
disabled: count < 2,
|
|
128
|
+
onClick: () => devErrorStore.next(),
|
|
129
|
+
children: /* @__PURE__ */ jsx(ChevronRightIcon, {})
|
|
130
|
+
}),
|
|
131
|
+
/* @__PURE__ */ jsxs("span", {
|
|
132
|
+
className: "ossido-err-pager-count",
|
|
133
|
+
children: [
|
|
134
|
+
activeIndex + 1,
|
|
135
|
+
" of ",
|
|
136
|
+
count
|
|
137
|
+
]
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ jsx("span", {
|
|
140
|
+
className: "ossido-err-pager-kind",
|
|
141
|
+
children: KIND_LABEL[entry.kind]
|
|
142
|
+
}),
|
|
143
|
+
entry.occurrences > 1 && /* @__PURE__ */ jsxs("span", {
|
|
144
|
+
className: "ossido-err-pager-occurrences",
|
|
145
|
+
title: `Reported ${entry.occurrences} times`,
|
|
146
|
+
children: ["×", entry.occurrences]
|
|
147
|
+
}),
|
|
148
|
+
/* @__PURE__ */ jsx("span", { className: "ossido-err-pager-spacer" }),
|
|
149
|
+
/* @__PURE__ */ jsx("button", {
|
|
150
|
+
type: "button",
|
|
151
|
+
className: "ossido-err-dismiss",
|
|
152
|
+
"aria-label": "Dismiss",
|
|
153
|
+
onClick: () => devErrorStore.closeOverlay(),
|
|
154
|
+
children: "✕"
|
|
155
|
+
})
|
|
156
|
+
]
|
|
157
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
158
|
+
className: "ossido-err-body",
|
|
159
|
+
children: [
|
|
160
|
+
/* @__PURE__ */ jsx(EntryBody, { entry }),
|
|
161
|
+
entry.reset && /* @__PURE__ */ jsx("div", {
|
|
162
|
+
className: "ossido-err-actions",
|
|
163
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
164
|
+
type: "button",
|
|
165
|
+
className: "ossido-err-btn",
|
|
166
|
+
onClick: entry.reset,
|
|
167
|
+
children: "Try again"
|
|
168
|
+
})
|
|
169
|
+
}),
|
|
170
|
+
/* @__PURE__ */ jsxs("p", {
|
|
171
|
+
className: "ossido-err-hint",
|
|
172
|
+
children: [
|
|
173
|
+
"This overlay is only shown in development. Press ",
|
|
174
|
+
/* @__PURE__ */ jsx("kbd", { children: "Esc" }),
|
|
175
|
+
" or click outside to dismiss."
|
|
176
|
+
]
|
|
177
|
+
})
|
|
178
|
+
]
|
|
179
|
+
})]
|
|
180
|
+
})]
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function DevIndicator({ count, menuOpen, position }) {
|
|
184
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [menuOpen && /* @__PURE__ */ jsx("div", {
|
|
185
|
+
className: "ossido-err-menu-scrim",
|
|
186
|
+
onClick: () => devErrorStore.closeMenu()
|
|
187
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
188
|
+
className: `ossido-err-indicator ossido-err-indicator--${position}`,
|
|
189
|
+
children: [menuOpen && /* @__PURE__ */ jsxs("div", {
|
|
190
|
+
className: `ossido-err-menu ossido-err-menu--${position}`,
|
|
191
|
+
role: "menu",
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ jsxs("button", {
|
|
194
|
+
type: "button",
|
|
195
|
+
role: "menuitem",
|
|
196
|
+
className: "ossido-err-menu-item",
|
|
197
|
+
disabled: count === 0,
|
|
198
|
+
onClick: () => devErrorStore.openOverlay(),
|
|
199
|
+
children: [/* @__PURE__ */ jsx("span", { children: "View errors" }), count > 0 ? /* @__PURE__ */ jsx("span", {
|
|
200
|
+
className: "ossido-err-menu-count",
|
|
201
|
+
children: count
|
|
202
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
203
|
+
className: "ossido-err-menu-muted",
|
|
204
|
+
children: "None"
|
|
205
|
+
})]
|
|
206
|
+
}),
|
|
207
|
+
/* @__PURE__ */ jsx("div", { className: "ossido-err-menu-sep" }),
|
|
208
|
+
/* @__PURE__ */ jsxs("div", {
|
|
209
|
+
className: "ossido-err-menu-section",
|
|
210
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
211
|
+
className: "ossido-err-menu-label",
|
|
212
|
+
children: "Position"
|
|
213
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
214
|
+
className: "ossido-err-menu-corners",
|
|
215
|
+
children: DEV_OVERLAY_CORNERS.map((corner) => /* @__PURE__ */ jsx("button", {
|
|
216
|
+
type: "button",
|
|
217
|
+
className: corner === position ? "ossido-err-corner ossido-err-corner--active" : "ossido-err-corner",
|
|
218
|
+
"aria-label": CORNER_LABEL[corner],
|
|
219
|
+
"aria-pressed": corner === position,
|
|
220
|
+
onClick: () => devErrorStore.setPosition(corner),
|
|
221
|
+
children: /* @__PURE__ */ jsx("span", { className: `ossido-err-corner-dot ossido-err-corner-dot--${corner}` })
|
|
222
|
+
}, corner))
|
|
223
|
+
})]
|
|
224
|
+
}),
|
|
225
|
+
/* @__PURE__ */ jsx("div", { className: "ossido-err-menu-sep" }),
|
|
226
|
+
/* @__PURE__ */ jsx("button", {
|
|
227
|
+
type: "button",
|
|
228
|
+
role: "menuitem",
|
|
229
|
+
className: "ossido-err-menu-item",
|
|
230
|
+
onClick: () => devErrorStore.hideBadge(),
|
|
231
|
+
children: /* @__PURE__ */ jsx("span", { children: "Hide until reload" })
|
|
232
|
+
})
|
|
233
|
+
]
|
|
234
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
235
|
+
type: "button",
|
|
236
|
+
className: "ossido-err-fab",
|
|
237
|
+
"aria-label": "Ossido dev tools",
|
|
238
|
+
"aria-haspopup": "menu",
|
|
239
|
+
"aria-expanded": menuOpen,
|
|
240
|
+
onClick: () => devErrorStore.toggleMenu(),
|
|
241
|
+
children: [/* @__PURE__ */ jsx(BoltIcon, {}), count > 0 && /* @__PURE__ */ jsx("span", {
|
|
242
|
+
className: "ossido-err-fab-count",
|
|
243
|
+
children: count
|
|
244
|
+
})]
|
|
245
|
+
})]
|
|
246
|
+
})] });
|
|
247
|
+
}
|
|
248
|
+
function DevErrorOverlayHost() {
|
|
249
|
+
const state = useSyncExternalStore(devErrorStore.subscribe, devErrorStore.getSnapshot, devErrorStore.getServerSnapshot);
|
|
250
|
+
const [mounted, setMounted] = useState(false);
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
setMounted(true);
|
|
253
|
+
}, []);
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
const onError = (event) => {
|
|
256
|
+
const error = event.error instanceof Error ? event.error : new Error(event.message);
|
|
257
|
+
if (isIgnorableUncaught(error)) return;
|
|
258
|
+
devErrorStore.addJsError("uncaught", error);
|
|
259
|
+
};
|
|
260
|
+
const onRejection = (event) => {
|
|
261
|
+
devErrorStore.addJsError("unhandledrejection", toError(event.reason));
|
|
262
|
+
};
|
|
263
|
+
window.addEventListener("error", onError);
|
|
264
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
265
|
+
return () => {
|
|
266
|
+
window.removeEventListener("error", onError);
|
|
267
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
268
|
+
};
|
|
269
|
+
}, []);
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (!state.overlayOpen && !state.menuOpen) return;
|
|
272
|
+
const onKeyDown = (event) => {
|
|
273
|
+
if (event.key !== "Escape") return;
|
|
274
|
+
if (state.overlayOpen) devErrorStore.closeOverlay();
|
|
275
|
+
else devErrorStore.closeMenu();
|
|
276
|
+
};
|
|
277
|
+
document.addEventListener("keydown", onKeyDown);
|
|
278
|
+
return () => {
|
|
279
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
280
|
+
};
|
|
281
|
+
}, [state.overlayOpen, state.menuOpen]);
|
|
282
|
+
if (typeof document === "undefined" || !mounted) return null;
|
|
283
|
+
const count = state.entries.length;
|
|
284
|
+
if (state.overlayOpen && count > 0) return createPortal(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Styles, {}), /* @__PURE__ */ jsx(DevErrorOverlay, { count })] }), document.body);
|
|
285
|
+
if (state.badgeHidden && count === 0) return null;
|
|
286
|
+
return createPortal(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Styles, {}), /* @__PURE__ */ jsx(DevIndicator, {
|
|
287
|
+
count,
|
|
288
|
+
menuOpen: state.menuOpen,
|
|
289
|
+
position: state.position
|
|
290
|
+
})] }), document.body);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
//#endregion
|
|
294
|
+
export { DevErrorOverlayHost };
|
|
295
|
+
//# sourceMappingURL=DevErrorOverlayHost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevErrorOverlayHost.js","names":[],"sources":["../../../src/components/DevErrorOverlayHost.tsx"],"sourcesContent":["import { useEffect, useState, useSyncExternalStore } from 'react';\nimport type { JSX } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { BaseStyles } from './BaseStyles';\nimport { DevErrorContent } from './DevErrorContent';\nimport { DevBuildErrorContent } from './DevBuildErrorContent';\nimport { DEV_ERROR_STYLES } from './devErrorStyles';\nimport type {\n DevErrorEntry,\n DevErrorKind,\n DevOverlayCorner,\n} from './devErrorStore';\nimport { DEV_OVERLAY_CORNERS, devErrorStore } from './devErrorStore';\n\n/**\n * Development-only host for the unified error overlay and its persistent dev\n * indicator. Mounted once at the router root (dev only), it subscribes to\n * {@link devErrorStore} and renders:\n *\n * - a persistent corner **indicator** (always visible in dev) whose menu offers\n * \"View errors\", a corner picker (persisted), and \"Hide until reload\", and\n * - the **error overlay** itself — a browsable window for every kind of dev\n * error (React render errors, SSR/Rust panics, uncaught errors, unhandled\n * rejections, Vite build errors), auto-opened when a new error arrives.\n *\n * Never rendered in production: {@link RouteMatch} only mounts it in dev, so no\n * source or internals leak to end users.\n */\n\nconst KIND_LABEL: Record<DevErrorKind, string> = {\n runtime: 'Runtime error',\n uncaught: 'Uncaught error',\n unhandledrejection: 'Unhandled rejection',\n build: 'Build error',\n};\n\nconst CORNER_LABEL: Record<DevOverlayCorner, string> = {\n 'top-left': 'Top left',\n 'top-right': 'Top right',\n 'bottom-left': 'Bottom left',\n 'bottom-right': 'Bottom right',\n};\n\n/**\n * React dispatches these to `window` when server rendering fails and it recovers\n * by client-rendering (or when hydration falls back). They carry no stack and\n * aren't actionable — the underlying error is already reported by the route\n * boundary — so they are dropped to avoid a duplicate, contentless entry.\n */\nconst IGNORED_UNCAUGHT_MESSAGES = [\n 'could not finish this Suspense boundary',\n 'Switched to client rendering',\n 'error during server rendering',\n 'hydrating but received',\n];\n\nfunction isIgnorableUncaught(error: Error): boolean {\n return IGNORED_UNCAUGHT_MESSAGES.some((needle) =>\n error.message.includes(needle),\n );\n}\n\n/** Normalize an unhandled rejection reason into an `Error`. */\nfunction toError(reason: unknown): Error {\n if (reason instanceof Error) return reason;\n const error = new Error(\n typeof reason === 'string' ? reason : JSON.stringify(reason),\n );\n error.name = 'UnhandledRejection';\n return error;\n}\n\nfunction Styles(): JSX.Element {\n return (\n <>\n <BaseStyles />\n <style>{DEV_ERROR_STYLES}</style>\n </>\n );\n}\n\nfunction EntryBody({ entry }: { entry: DevErrorEntry }): JSX.Element | null {\n if (entry.kind === 'build' && entry.build) {\n return <DevBuildErrorContent build={entry.build} />;\n }\n if (entry.error) {\n // Keyed by entry id so paging to a different error remounts with fresh\n // resolution state (but a re-render of the same entry does not).\n return <DevErrorContent key={entry.id} error={entry.error} />;\n }\n return null;\n}\n\nfunction ChevronLeftIcon(): JSX.Element {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n >\n <path d=\"M15 18l-6-6 6-6\" />\n </svg>\n );\n}\n\nfunction ChevronRightIcon(): JSX.Element {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n >\n <path d=\"M9 6l6 6-6 6\" />\n </svg>\n );\n}\n\n/** The same lightning bolt Ossido prints in the console. */\nfunction BoltIcon(): JSX.Element {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>\n <path d=\"M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z\" />\n </svg>\n );\n}\n\nfunction DevErrorOverlay({ count }: { count: number }): JSX.Element {\n const state = devErrorStore.getSnapshot();\n const activeIndex = Math.min(state.activeIndex, count - 1);\n const entry = state.entries[activeIndex] as DevErrorEntry;\n\n return (\n <div\n className=\"ossido-err-backdrop\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n onClick={(): void => devErrorStore.closeOverlay()}\n >\n <Styles />\n {/* Clicks inside the window must not fall through to the backdrop. */}\n <div\n className=\"ossido-err-window\"\n onClick={(event): void => event.stopPropagation()}\n >\n <div className=\"ossido-err-pager\">\n <button\n type=\"button\"\n className=\"ossido-err-nav\"\n aria-label=\"Previous error\"\n disabled={count < 2}\n onClick={(): void => devErrorStore.prev()}\n >\n <ChevronLeftIcon />\n </button>\n <button\n type=\"button\"\n className=\"ossido-err-nav\"\n aria-label=\"Next error\"\n disabled={count < 2}\n onClick={(): void => devErrorStore.next()}\n >\n <ChevronRightIcon />\n </button>\n <span className=\"ossido-err-pager-count\">\n {activeIndex + 1} of {count}\n </span>\n <span className=\"ossido-err-pager-kind\">\n {KIND_LABEL[entry.kind]}\n </span>\n {entry.occurrences > 1 && (\n <span\n className=\"ossido-err-pager-occurrences\"\n title={`Reported ${entry.occurrences} times`}\n >\n ×{entry.occurrences}\n </span>\n )}\n <span className=\"ossido-err-pager-spacer\" />\n <button\n type=\"button\"\n className=\"ossido-err-dismiss\"\n aria-label=\"Dismiss\"\n onClick={(): void => devErrorStore.closeOverlay()}\n >\n ✕\n </button>\n </div>\n\n <div className=\"ossido-err-body\">\n <EntryBody entry={entry} />\n\n {entry.reset && (\n <div className=\"ossido-err-actions\">\n <button\n type=\"button\"\n className=\"ossido-err-btn\"\n onClick={entry.reset}\n >\n Try again\n </button>\n </div>\n )}\n <p className=\"ossido-err-hint\">\n This overlay is only shown in development. Press <kbd>Esc</kbd> or\n click outside to dismiss.\n </p>\n </div>\n </div>\n </div>\n );\n}\n\nfunction DevIndicator({\n count,\n menuOpen,\n position,\n}: {\n count: number;\n menuOpen: boolean;\n position: DevOverlayCorner;\n}): JSX.Element {\n return (\n <>\n {menuOpen && (\n <div\n className=\"ossido-err-menu-scrim\"\n onClick={(): void => devErrorStore.closeMenu()}\n />\n )}\n <div className={`ossido-err-indicator ossido-err-indicator--${position}`}>\n {menuOpen && (\n <div\n className={`ossido-err-menu ossido-err-menu--${position}`}\n role=\"menu\"\n >\n <button\n type=\"button\"\n role=\"menuitem\"\n className=\"ossido-err-menu-item\"\n disabled={count === 0}\n onClick={(): void => devErrorStore.openOverlay()}\n >\n <span>View errors</span>\n {count > 0 ? (\n <span className=\"ossido-err-menu-count\">{count}</span>\n ) : (\n <span className=\"ossido-err-menu-muted\">None</span>\n )}\n </button>\n\n <div className=\"ossido-err-menu-sep\" />\n\n <div className=\"ossido-err-menu-section\">\n <span className=\"ossido-err-menu-label\">Position</span>\n <div className=\"ossido-err-menu-corners\">\n {DEV_OVERLAY_CORNERS.map((corner) => (\n <button\n key={corner}\n type=\"button\"\n className={\n corner === position\n ? 'ossido-err-corner ossido-err-corner--active'\n : 'ossido-err-corner'\n }\n aria-label={CORNER_LABEL[corner]}\n aria-pressed={corner === position}\n onClick={(): void => devErrorStore.setPosition(corner)}\n >\n <span\n className={`ossido-err-corner-dot ossido-err-corner-dot--${corner}`}\n />\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"ossido-err-menu-sep\" />\n\n <button\n type=\"button\"\n role=\"menuitem\"\n className=\"ossido-err-menu-item\"\n onClick={(): void => devErrorStore.hideBadge()}\n >\n <span>Hide until reload</span>\n </button>\n </div>\n )}\n\n <button\n type=\"button\"\n className=\"ossido-err-fab\"\n aria-label=\"Ossido dev tools\"\n aria-haspopup=\"menu\"\n aria-expanded={menuOpen}\n onClick={(): void => devErrorStore.toggleMenu()}\n >\n <BoltIcon />\n {count > 0 && <span className=\"ossido-err-fab-count\">{count}</span>}\n </button>\n </div>\n </>\n );\n}\n\nexport function DevErrorOverlayHost(): JSX.Element | null {\n const state = useSyncExternalStore(\n devErrorStore.subscribe,\n devErrorStore.getSnapshot,\n devErrorStore.getServerSnapshot,\n );\n\n // The indicator is a client-only portal, so it renders nothing until after\n // mount. This keeps the hydration render identical to the server (which\n // renders nothing), avoiding a hydration mismatch now that the badge is shown\n // even with zero errors.\n const [mounted, setMounted] = useState(false);\n useEffect(() => {\n setMounted(true);\n }, []);\n\n // Collect uncaught errors and unhandled rejections (React render errors and\n // build errors arrive through their own channels). The source-highlighting\n // libraries are prefetched at dev bootstrap (see the hydration entry).\n useEffect(() => {\n const onError = (event: ErrorEvent): void => {\n const error =\n event.error instanceof Error ? event.error : new Error(event.message);\n if (isIgnorableUncaught(error)) return;\n devErrorStore.addJsError('uncaught', error);\n };\n const onRejection = (event: PromiseRejectionEvent): void => {\n devErrorStore.addJsError('unhandledrejection', toError(event.reason));\n };\n window.addEventListener('error', onError);\n window.addEventListener('unhandledrejection', onRejection);\n return (): void => {\n window.removeEventListener('error', onError);\n window.removeEventListener('unhandledrejection', onRejection);\n };\n }, []);\n\n // Esc closes the overlay (or the indicator menu).\n useEffect(() => {\n if (!state.overlayOpen && !state.menuOpen) return;\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== 'Escape') return;\n if (state.overlayOpen) devErrorStore.closeOverlay();\n else devErrorStore.closeMenu();\n };\n document.addEventListener('keydown', onKeyDown);\n return (): void => {\n document.removeEventListener('keydown', onKeyDown);\n };\n }, [state.overlayOpen, state.menuOpen]);\n\n if (typeof document === 'undefined' || !mounted) return null;\n\n const count = state.entries.length;\n\n // A new error auto-opens the full overlay.\n if (state.overlayOpen && count > 0) {\n return createPortal(\n <>\n <Styles />\n <DevErrorOverlay count={count} />\n </>,\n document.body,\n );\n }\n\n // Otherwise the persistent indicator — hidden only when the user hid it for\n // this session AND there are no errors to surface.\n if (state.badgeHidden && count === 0) return null;\n\n return createPortal(\n <>\n <Styles />\n <DevIndicator\n count={count}\n menuOpen={state.menuOpen}\n position={state.position}\n />\n </>,\n document.body,\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OssidoErrorProps } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Development-only error-boundary fallback. Instead of rendering the overlay in
|
|
4
|
+
* place of the failed subtree (which reads as a full-page replacement), it
|
|
5
|
+
* reports the caught render error — including SSR/Rust panics surfaced through
|
|
6
|
+
* a rejected data resource — to the shared {@link devErrorStore}. The floating
|
|
7
|
+
* {@link DevErrorOverlayHost} then shows it over the app, with the boundary's
|
|
8
|
+
* `reset` wired to the overlay's "Try again".
|
|
9
|
+
*
|
|
10
|
+
* Renders nothing itself; the route area stays blank behind the overlay until
|
|
11
|
+
* the error is fixed or retried (matching Vite/Next).
|
|
12
|
+
*/
|
|
13
|
+
export declare function DevErrorReporter({ error, reset }: OssidoErrorProps): null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { devErrorStore } from "./devErrorStore.js";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/components/DevErrorReporter.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Development-only error-boundary fallback. Instead of rendering the overlay in
|
|
7
|
+
* place of the failed subtree (which reads as a full-page replacement), it
|
|
8
|
+
* reports the caught render error — including SSR/Rust panics surfaced through
|
|
9
|
+
* a rejected data resource — to the shared {@link devErrorStore}. The floating
|
|
10
|
+
* {@link DevErrorOverlayHost} then shows it over the app, with the boundary's
|
|
11
|
+
* `reset` wired to the overlay's "Try again".
|
|
12
|
+
*
|
|
13
|
+
* Renders nothing itself; the route area stays blank behind the overlay until
|
|
14
|
+
* the error is fixed or retried (matching Vite/Next).
|
|
15
|
+
*/
|
|
16
|
+
function DevErrorReporter({ error, reset }) {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const id = devErrorStore.addJsError("runtime", error, reset);
|
|
19
|
+
return () => {
|
|
20
|
+
devErrorStore.removeById(id);
|
|
21
|
+
};
|
|
22
|
+
}, [error, reset]);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { DevErrorReporter };
|
|
28
|
+
//# sourceMappingURL=DevErrorReporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevErrorReporter.js","names":[],"sources":["../../../src/components/DevErrorReporter.tsx"],"sourcesContent":["import { useEffect } from 'react';\n\nimport type { OssidoErrorProps } from '../types';\n\nimport { devErrorStore } from './devErrorStore';\n\n/**\n * Development-only error-boundary fallback. Instead of rendering the overlay in\n * place of the failed subtree (which reads as a full-page replacement), it\n * reports the caught render error — including SSR/Rust panics surfaced through\n * a rejected data resource — to the shared {@link devErrorStore}. The floating\n * {@link DevErrorOverlayHost} then shows it over the app, with the boundary's\n * `reset` wired to the overlay's \"Try again\".\n *\n * Renders nothing itself; the route area stays blank behind the overlay until\n * the error is fixed or retried (matching Vite/Next).\n */\nexport function DevErrorReporter({ error, reset }: OssidoErrorProps): null {\n useEffect(() => {\n const id = devErrorStore.addJsError('runtime', error, reset);\n return (): void => {\n devErrorStore.removeById(id);\n };\n }, [error, reset]);\n\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,EAAE,OAAO,SAAiC;CACzE,gBAAgB;EACd,MAAM,KAAK,cAAc,WAAW,WAAW,OAAO,KAAK;EAC3D,aAAmB;GACjB,cAAc,WAAW,EAAE;EAC7B;CACF,GAAG,CAAC,OAAO,KAAK,CAAC;CAEjB,OAAO;AACT"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/components/base.css?raw-css
|
|
2
|
+
var base_default = "/*\n * Design tokens shared by the framework's default screens (the dev error\n * overlay, the production error fallback, the not-found page, …). Injected as a\n * `<style>` element by `BaseStyles` — the router library has no CSS bundling\n * step, so styles ship as strings.\n *\n * The Google Fonts import MUST stay the first rule in the sheet (CSS ignores\n * `@import` that follows other rules). Noto Sans is the default body font;\n * JetBrains Mono is used for code and stack traces.\n */\n@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Noto+Sans:wght@400;500;600;700&display=swap');\n\n:root {\n /* Typography ------------------------------------------------------------ */\n --ossido-font-sans:\n 'Noto Sans', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial,\n sans-serif;\n --ossido-font-mono:\n 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas,\n 'Liberation Mono', monospace;\n\n --ossido-font-size-xs: 12px;\n --ossido-font-size-sm: 13px;\n --ossido-font-size: 14px;\n --ossido-font-size-lg: 18px;\n --ossido-font-size-xl: 22px;\n\n --ossido-font-weight-regular: 400;\n --ossido-font-weight-medium: 500;\n --ossido-font-weight-semibold: 600;\n --ossido-font-weight-bold: 700;\n\n --ossido-line-height: 1.5;\n --ossido-line-height-tight: 1.25;\n --ossido-letter-spacing-wide: 0.04em;\n\n /* Colours (dark theme) -------------------------------------------------- */\n --ossido-color-bg: #0a0a0e;\n --ossido-color-surface: #101014;\n --ossido-color-surface-hover: #1c1c22;\n --ossido-color-surface-raised: #151519;\n --ossido-color-border: rgba(255, 255, 255, 0.1);\n --ossido-color-border-strong: #3a3a42;\n\n --ossido-color-text: #e6e6e6;\n --ossido-color-text-muted: #c7c7cf;\n --ossido-color-text-subtle: #8a8a94;\n --ossido-color-text-faint: #6f6f78;\n\n --ossido-color-accent: #f43f5e;\n --ossido-color-accent-text: #ff8fa3;\n --ossido-color-on-accent: #ffffff;\n --ossido-color-danger-surface: #4a1d27;\n\n --ossido-color-success: #22c55e;\n --ossido-color-success-text: #4ade80;\n --ossido-color-success-surface: #16331f;\n\n /* Radii ----------------------------------------------------------------- */\n --ossido-radius-sm: 6px;\n --ossido-radius: 8px;\n --ossido-radius-pill: 999px;\n\n /* Spacing --------------------------------------------------------------- */\n --ossido-space-1: 0.25rem;\n --ossido-space-2: 0.5rem;\n --ossido-space-3: 0.75rem;\n --ossido-space-4: 1rem;\n --ossido-space-6: 1.5rem;\n --ossido-space-8: 2rem;\n}\n";
|
|
3
|
+
|
|
4
|
+
//#endregion
|
|
5
|
+
export { base_default as default };
|
|
6
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","names":[],"sources":["../../../src/components/base.css?raw-css"],"sourcesContent":["export default \"/*\\n * Design tokens shared by the framework's default screens (the dev error\\n * overlay, the production error fallback, the not-found page, …). Injected as a\\n * `<style>` element by `BaseStyles` — the router library has no CSS bundling\\n * step, so styles ship as strings.\\n *\\n * The Google Fonts import MUST stay the first rule in the sheet (CSS ignores\\n * `@import` that follows other rules). Noto Sans is the default body font;\\n * JetBrains Mono is used for code and stack traces.\\n */\\n@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Noto+Sans:wght@400;500;600;700&display=swap');\\n\\n:root {\\n /* Typography ------------------------------------------------------------ */\\n --ossido-font-sans:\\n 'Noto Sans', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial,\\n sans-serif;\\n --ossido-font-mono:\\n 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas,\\n 'Liberation Mono', monospace;\\n\\n --ossido-font-size-xs: 12px;\\n --ossido-font-size-sm: 13px;\\n --ossido-font-size: 14px;\\n --ossido-font-size-lg: 18px;\\n --ossido-font-size-xl: 22px;\\n\\n --ossido-font-weight-regular: 400;\\n --ossido-font-weight-medium: 500;\\n --ossido-font-weight-semibold: 600;\\n --ossido-font-weight-bold: 700;\\n\\n --ossido-line-height: 1.5;\\n --ossido-line-height-tight: 1.25;\\n --ossido-letter-spacing-wide: 0.04em;\\n\\n /* Colours (dark theme) -------------------------------------------------- */\\n --ossido-color-bg: #0a0a0e;\\n --ossido-color-surface: #101014;\\n --ossido-color-surface-hover: #1c1c22;\\n --ossido-color-surface-raised: #151519;\\n --ossido-color-border: rgba(255, 255, 255, 0.1);\\n --ossido-color-border-strong: #3a3a42;\\n\\n --ossido-color-text: #e6e6e6;\\n --ossido-color-text-muted: #c7c7cf;\\n --ossido-color-text-subtle: #8a8a94;\\n --ossido-color-text-faint: #6f6f78;\\n\\n --ossido-color-accent: #f43f5e;\\n --ossido-color-accent-text: #ff8fa3;\\n --ossido-color-on-accent: #ffffff;\\n --ossido-color-danger-surface: #4a1d27;\\n\\n --ossido-color-success: #22c55e;\\n --ossido-color-success-text: #4ade80;\\n --ossido-color-success-surface: #16331f;\\n\\n /* Radii ----------------------------------------------------------------- */\\n --ossido-radius-sm: 6px;\\n --ossido-radius: 8px;\\n --ossido-radius-pill: 999px;\\n\\n /* Spacing --------------------------------------------------------------- */\\n --ossido-space-1: 0.25rem;\\n --ossido-space-2: 0.5rem;\\n --ossido-space-3: 0.75rem;\\n --ossido-space-4: 1rem;\\n --ossido-space-6: 1.5rem;\\n --ossido-space-8: 2rem;\\n}\\n\""],"mappings":";AAAA,mBAAe"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only pipeline for the error overlay. Resolves a browser stack trace to
|
|
3
|
+
* original source locations (via each module's sourcemap), then extracts and
|
|
4
|
+
* syntax-highlights (Shiki) the source excerpt around the throwing expression.
|
|
5
|
+
* Everything here is dynamically imported so it never ships in the production
|
|
6
|
+
* client bundle.
|
|
7
|
+
*/
|
|
8
|
+
interface RawFrame {
|
|
9
|
+
fn?: string;
|
|
10
|
+
file?: string;
|
|
11
|
+
line?: number;
|
|
12
|
+
column?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ResolvedFrame {
|
|
15
|
+
fn?: string;
|
|
16
|
+
file?: string;
|
|
17
|
+
line?: number;
|
|
18
|
+
column?: number;
|
|
19
|
+
isApp: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface HighlightedLine {
|
|
22
|
+
number: number;
|
|
23
|
+
/** Pre-highlighted HTML for the line (from Shiki, inline-styled). */
|
|
24
|
+
html: string;
|
|
25
|
+
isErrorLine: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** A single source line as plain text — shown before highlighting is ready. */
|
|
28
|
+
export interface PlainLine {
|
|
29
|
+
number: number;
|
|
30
|
+
text: string;
|
|
31
|
+
isErrorLine: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface SourceExcerpt {
|
|
34
|
+
file: string;
|
|
35
|
+
line: number;
|
|
36
|
+
column: number;
|
|
37
|
+
lines: Array<HighlightedLine>;
|
|
38
|
+
}
|
|
39
|
+
/** The excerpt in plain text, before Shiki has highlighted it. */
|
|
40
|
+
export interface PlainExcerpt {
|
|
41
|
+
file: string;
|
|
42
|
+
line: number;
|
|
43
|
+
column: number;
|
|
44
|
+
lines: Array<PlainLine>;
|
|
45
|
+
}
|
|
46
|
+
/** Source content around a throwing location — the input to an excerpt. */
|
|
47
|
+
export interface RawExcerptSource {
|
|
48
|
+
content: string;
|
|
49
|
+
file: string;
|
|
50
|
+
line: number;
|
|
51
|
+
column: number;
|
|
52
|
+
}
|
|
53
|
+
export declare function parseStack(stack: string | undefined): Array<RawFrame>;
|
|
54
|
+
/**
|
|
55
|
+
* The most specific label for an error: its constructor (class) name when it is
|
|
56
|
+
* more descriptive than the generic `Error`. A subclass like `TestError` keeps
|
|
57
|
+
* `error.name === 'Error'` unless it explicitly sets `name`, but its
|
|
58
|
+
* constructor name is still `TestError`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function errorLabel(error: Error): string;
|
|
61
|
+
/** Strip origin and query for a readable path. */
|
|
62
|
+
export declare function prettyPath(file: string): string;
|
|
63
|
+
/** Immediate (unmapped) frames for the first paint before async resolution. */
|
|
64
|
+
export declare function initialFrames(stack: string | undefined): Array<ResolvedFrame>;
|
|
65
|
+
/**
|
|
66
|
+
* Slice the source into the context window around the throwing line, as plain
|
|
67
|
+
* text. Synchronous and dependency-free, so the excerpt can render immediately;
|
|
68
|
+
* syntax highlighting then swaps in over the same layout (see
|
|
69
|
+
* {@link highlightExcerpt}).
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractExcerpt(src: RawExcerptSource): PlainExcerpt;
|
|
72
|
+
/**
|
|
73
|
+
* Eagerly kick off the heavy, dev-only imports (Shiki core + engine + grammars,
|
|
74
|
+
* trace-mapping, hast-util-to-html) so the first error's source excerpt renders
|
|
75
|
+
* without waiting on library loading. Safe to call repeatedly — the imports are
|
|
76
|
+
* cached. Call it when the overlay host mounts.
|
|
77
|
+
*/
|
|
78
|
+
export declare function warmDevErrorSource(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Syntax-highlight a source excerpt (Shiki picks the grammar from the file
|
|
81
|
+
* extension, e.g. `.tsx` or `.rs`). Returns `null` if the dev-only libraries
|
|
82
|
+
* fail to load — the caller keeps showing the plain excerpt.
|
|
83
|
+
*/
|
|
84
|
+
export declare function highlightExcerpt(src: RawExcerptSource): Promise<SourceExcerpt | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Resolve stack frames to original locations (via each module's sourcemap) and
|
|
87
|
+
* return the raw source content of the top application frame — WITHOUT
|
|
88
|
+
* highlighting it, so the caller can render a plain excerpt immediately and
|
|
89
|
+
* highlight it asynchronously. Falls back to unmapped frames if trace-mapping
|
|
90
|
+
* fails to load.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveDevErrorFrames(stack: string | undefined): Promise<{
|
|
93
|
+
frames: Array<ResolvedFrame>;
|
|
94
|
+
source: RawExcerptSource | null;
|
|
95
|
+
}>;
|
|
96
|
+
export {};
|