round-core 0.0.1

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,345 @@
1
+ import { effect, untrack } from './signals.js';
2
+ import { runInContext, createComponentInstance, mountComponent, initLifecycleRoot } from './lifecycle.js';
3
+ import { reportErrorSafe } from './error-reporter.js';
4
+
5
+ let isObserverInitialized = false;
6
+
7
+ const warnedSignals = new Set();
8
+
9
+ function isPromiseLike(v) {
10
+ return v && (typeof v === 'object' || typeof v === 'function') && typeof v.then === 'function';
11
+ }
12
+
13
+ function warnSignalDirectUsage(fn, kind) {
14
+ try {
15
+ if (typeof fn !== 'function') return;
16
+ if (typeof fn.peek !== 'function') return;
17
+ if (!('value' in fn)) return;
18
+ // Using signals as dynamic children/props is a supported pattern.
19
+ if (kind === 'child') return;
20
+ if (typeof kind === 'string' && kind.startsWith('prop:')) return;
21
+ const key = `${kind}:${fn.name ?? 'signal'}`;
22
+ if (warnedSignals.has(key)) return;
23
+ warnedSignals.add(key);
24
+ console.warn(`[round] Prefer {signal()} (reactive) or {signal.value} (static). Direct {signal} usage is allowed but discouraged.`);
25
+ } catch {
26
+ }
27
+ }
28
+
29
+ export function createElement(tag, props = {}, ...children) {
30
+ if (typeof tag === 'function') {
31
+ const componentInstance = createComponentInstance();
32
+ const componentName = tag?.name ?? 'Anonymous';
33
+ componentInstance.name = componentName;
34
+
35
+ let node = runInContext(componentInstance, () => {
36
+ const componentProps = { ...props, children };
37
+ try {
38
+ const res = untrack(() => tag(componentProps));
39
+ if (isPromiseLike(res)) throw res;
40
+ return res;
41
+ } catch (e) {
42
+ if (isPromiseLike(e)) throw e;
43
+ reportErrorSafe(e, { phase: 'component.render', component: componentName });
44
+ return createElement('div', { style: { padding: '16px' } }, `Error in ${componentName}`);
45
+ }
46
+ });
47
+
48
+ if (Array.isArray(node)) {
49
+ const wrapper = document.createElement('span');
50
+ wrapper.style.display = 'contents';
51
+ node.forEach(n => appendChild(wrapper, n));
52
+ node = wrapper;
53
+ }
54
+
55
+ if (node instanceof Node) {
56
+ node._componentInstance = componentInstance;
57
+ componentInstance.nodes.push(node);
58
+
59
+ componentInstance.mountTimerId = setTimeout(() => {
60
+ componentInstance.mountTimerId = null;
61
+ mountComponent(componentInstance);
62
+ }, 0);
63
+ }
64
+
65
+ return node;
66
+ }
67
+
68
+ const element = document.createElement(tag);
69
+
70
+ if (props) {
71
+ Object.entries(props).forEach(([key, value]) => {
72
+ if (key === 'bind:value' || key === 'bind:checked') {
73
+ const isSignalLike = typeof value === 'function' && typeof value.peek === 'function' && ('value' in value);
74
+ const isBindable = isSignalLike && value.bind === true;
75
+
76
+ if (!isSignalLike) {
77
+ try {
78
+ console.warn('[round] bind:* expects a signal/bindable. Example: const name = bindable(\'\'); <input bind:value={name} />');
79
+ } catch {
80
+ }
81
+ return;
82
+ }
83
+
84
+ if (!isBindable) {
85
+ try {
86
+ console.warn('[round] bind:* is intended to be used with bindable(). Plain signal() is accepted but discouraged.');
87
+ } catch {
88
+ }
89
+ }
90
+
91
+ const isValueBinding = key === 'bind:value';
92
+ const isCheckedBinding = key === 'bind:checked';
93
+ const el = element;
94
+ const tagName = String(el.tagName ?? '').toLowerCase();
95
+ const type = String(el.getAttribute?.('type') ?? '').toLowerCase();
96
+
97
+ const isInput = tagName === 'input';
98
+ const isTextarea = tagName === 'textarea';
99
+ const isSelect = tagName === 'select';
100
+
101
+ if (isCheckedBinding && !(isInput && (type === 'checkbox' || type === 'radio'))) {
102
+ try {
103
+ console.warn(`[round] bind:checked is only supported on <input type="checkbox|radio">. Got <${tagName}${type ? ` type=\"${type}\"` : ''}>.`);
104
+ } catch {
105
+ }
106
+ return;
107
+ }
108
+
109
+ if (isValueBinding && !(isInput || isTextarea || isSelect)) {
110
+ try {
111
+ console.warn(`[round] bind:value is only supported on <input>, <textarea>, and <select>. Got <${tagName}>.`);
112
+ } catch {
113
+ }
114
+ return;
115
+ }
116
+
117
+ const coerceFromDom = () => {
118
+ if (isCheckedBinding) {
119
+ if (type === 'radio') {
120
+ return Boolean(el.checked);
121
+ }
122
+ return Boolean(el.checked);
123
+ }
124
+
125
+ if (isInput && type === 'number') {
126
+ const raw = el.value;
127
+ if (raw === '') return '';
128
+ const n = Number(raw);
129
+ return Number.isFinite(n) ? n : raw;
130
+ }
131
+
132
+ if (isSelect && el.multiple) {
133
+ try {
134
+ return Array.from(el.selectedOptions ?? []).map(o => o.value);
135
+ } catch {
136
+ return [];
137
+ }
138
+ }
139
+
140
+ return el.value;
141
+ };
142
+
143
+ const writeToDom = (v) => {
144
+ if (isCheckedBinding) {
145
+ const b = Boolean(v);
146
+ if (type === 'radio') {
147
+ el.checked = b;
148
+ } else {
149
+ el.checked = b;
150
+ }
151
+ return;
152
+ }
153
+
154
+ if (isSelect && el.multiple) {
155
+ const arr = Array.isArray(v) ? v.map(x => String(x)) : [];
156
+ try {
157
+ Array.from(el.options ?? []).forEach(opt => {
158
+ opt.selected = arr.includes(opt.value);
159
+ });
160
+ } catch {
161
+ }
162
+ return;
163
+ }
164
+
165
+ el.value = v ?? '';
166
+ };
167
+
168
+ const warnTypeMismatch = (next) => {
169
+ try {
170
+ if (isCheckedBinding && typeof next !== 'boolean') {
171
+ console.warn('[round] bind:checked expects a boolean signal value.');
172
+ }
173
+ if (isValueBinding && isSelect && el.multiple && !Array.isArray(next)) {
174
+ console.warn('[round] bind:value on <select multiple> expects an array signal value.');
175
+ }
176
+ if (isValueBinding && isInput && type === 'number' && !(typeof next === 'number' || typeof next === 'string')) {
177
+ console.warn('[round] bind:value on <input type="number"> expects number|string (empty string allowed).');
178
+ }
179
+ } catch {
180
+ }
181
+ };
182
+
183
+ effect(() => {
184
+ const v = value();
185
+ warnTypeMismatch(v);
186
+ writeToDom(v);
187
+ }, { onLoad: false });
188
+
189
+ const validateOn = isValueBinding && value && typeof value === 'function'
190
+ ? value.__round_validateOn
191
+ : null;
192
+ const valueEvent = (validateOn === 'blur') ? 'blur' : (isSelect ? 'change' : 'input');
193
+ const eventName = isCheckedBinding ? 'change' : valueEvent;
194
+ el.addEventListener(eventName, (e) => {
195
+ try {
196
+ const target = e.currentTarget;
197
+ if (!target) return;
198
+ const next = coerceFromDom();
199
+ value(next);
200
+ } catch {
201
+ }
202
+ });
203
+ return;
204
+ }
205
+
206
+ if (key.startsWith('on') && typeof value === 'function') {
207
+ element.addEventListener(key.toLowerCase().substring(2), value);
208
+ return;
209
+ }
210
+
211
+ if (key === 'dangerouslySetInnerHTML') {
212
+ if (typeof value === 'function') {
213
+ effect(() => {
214
+ const v = value();
215
+ if (v && typeof v === 'object' && '__html' in v) {
216
+ element.innerHTML = v.__html ?? '';
217
+ }
218
+ }, { onLoad: false });
219
+ } else if (value && typeof value === 'object' && '__html' in value) {
220
+ element.innerHTML = value.__html ?? '';
221
+ }
222
+ return;
223
+ }
224
+
225
+ if (key === 'style') {
226
+ if (typeof value === 'function') {
227
+ effect(() => {
228
+ const v = value();
229
+ if (v && typeof v === 'object') {
230
+ Object.assign(element.style, v);
231
+ }
232
+ }, { onLoad: false });
233
+ return;
234
+ }
235
+ if (value && typeof value === 'object') {
236
+ Object.assign(element.style, value);
237
+ return;
238
+ }
239
+ }
240
+
241
+ if (typeof value === 'function') {
242
+ warnSignalDirectUsage(value, `prop:${key}`);
243
+ effect(() => {
244
+ const val = value();
245
+ if (key === 'className') element.className = val;
246
+ else if (key === 'value') element.value = val;
247
+ else if (key === 'checked') element.checked = Boolean(val);
248
+ else element.setAttribute(key, val);
249
+ }, { onLoad: false });
250
+ return;
251
+ }
252
+
253
+ if (key === 'className') element.className = value;
254
+ else if (key === 'value') element.value = value;
255
+ else if (key === 'checked') element.checked = Boolean(value);
256
+ else element.setAttribute(key, value);
257
+ });
258
+ }
259
+
260
+ children.forEach(child => appendChild(element, child));
261
+
262
+ return element;
263
+ }
264
+
265
+ function appendChild(parent, child) {
266
+ if (child === null || child === undefined) return;
267
+
268
+ if (Array.isArray(child)) {
269
+ child.forEach(c => appendChild(parent, c));
270
+ return;
271
+ }
272
+
273
+ if (typeof child === 'string' || typeof child === 'number') {
274
+ parent.appendChild(document.createTextNode(child));
275
+ return;
276
+ }
277
+
278
+ if (typeof child === 'function') {
279
+ warnSignalDirectUsage(child, 'child');
280
+ const placeholder = document.createTextNode('');
281
+ parent.appendChild(placeholder);
282
+
283
+ let currentNode = placeholder;
284
+
285
+ effect(() => {
286
+ let val;
287
+ try {
288
+ val = child();
289
+ if (isPromiseLike(val)) throw val;
290
+ } catch (e) {
291
+ if (isPromiseLike(e)) throw e;
292
+ reportErrorSafe(e, { phase: 'child.dynamic' });
293
+ val = createElement('div', { style: { padding: '16px' } }, 'Error');
294
+ }
295
+
296
+ if (Array.isArray(val)) {
297
+ if (!(currentNode instanceof Element) || !currentNode._roundArrayWrapper) {
298
+ const wrapper = document.createElement('span');
299
+ wrapper.style.display = 'contents';
300
+ wrapper._roundArrayWrapper = true;
301
+ if (currentNode.parentNode) {
302
+ currentNode.parentNode.replaceChild(wrapper, currentNode);
303
+ currentNode = wrapper;
304
+ }
305
+ }
306
+
307
+ while (currentNode.firstChild) currentNode.removeChild(currentNode.firstChild);
308
+ val.forEach(v => appendChild(currentNode, v));
309
+ return;
310
+ }
311
+
312
+ if (val instanceof Node) {
313
+ if (currentNode !== val) {
314
+ if (currentNode.parentNode) {
315
+ currentNode.parentNode.replaceChild(val, currentNode);
316
+ currentNode = val;
317
+ }
318
+ }
319
+ }
320
+ else {
321
+ const textContent = (val === null || val === undefined) ? '' : val;
322
+
323
+ if (currentNode instanceof Element) {
324
+ const newText = document.createTextNode(textContent);
325
+ if (currentNode.parentNode) {
326
+ currentNode.parentNode.replaceChild(newText, currentNode);
327
+ currentNode = newText;
328
+ }
329
+ } else {
330
+ currentNode.textContent = textContent;
331
+ }
332
+ }
333
+ }, { onLoad: false });
334
+ return;
335
+ }
336
+
337
+ if (child instanceof Node) {
338
+ parent.appendChild(child);
339
+ return;
340
+ }
341
+ }
342
+
343
+ export function Fragment(props) {
344
+ return props.children;
345
+ }
@@ -0,0 +1,48 @@
1
+ import { signal } from './signals.js';
2
+ import { createElement } from './dom.js';
3
+ import { reportError } from './error-store.js';
4
+
5
+ export function ErrorBoundary(props = {}) {
6
+ const error = signal(null);
7
+
8
+ const name = props.name ?? 'ErrorBoundary';
9
+ const fallback = props.fallback;
10
+ const resetKey = props.resetKey;
11
+
12
+ let lastResetKey = resetKey;
13
+
14
+ return createElement('span', { style: { display: 'contents' } }, () => {
15
+ if (resetKey !== undefined && resetKey !== lastResetKey) {
16
+ lastResetKey = resetKey;
17
+ if (error()) error(null);
18
+ }
19
+
20
+ const err = error();
21
+ if (err) {
22
+ if (typeof fallback === 'function') {
23
+ try {
24
+ return fallback({ error: err });
25
+ } catch (e) {
26
+ reportError(e, { phase: 'ErrorBoundary.fallback', component: name });
27
+ return createElement('div', { style: { padding: '16px' } }, 'ErrorBoundary fallback crashed');
28
+ }
29
+ }
30
+ if (fallback !== undefined) return fallback;
31
+ return createElement('div', { style: { padding: '16px' } }, 'Something went wrong.');
32
+ }
33
+
34
+ const renderFn = (typeof props.render === 'function')
35
+ ? props.render
36
+ : (typeof props.children === 'function' ? props.children : null);
37
+
38
+ if (typeof renderFn !== 'function') return props.children ?? null;
39
+
40
+ try {
41
+ return renderFn();
42
+ } catch (e) {
43
+ if (!error() || error() !== e) error(e);
44
+ reportError(e, { phase: 'ErrorBoundary.render', component: name });
45
+ return null;
46
+ }
47
+ });
48
+ }
@@ -0,0 +1,13 @@
1
+ let reporter = null;
2
+
3
+ export function setErrorReporter(fn) {
4
+ reporter = typeof fn === 'function' ? fn : null;
5
+ }
6
+
7
+ export function reportErrorSafe(error, info) {
8
+ if (!reporter) return;
9
+ try {
10
+ reporter(error, info);
11
+ } catch {
12
+ }
13
+ }
@@ -0,0 +1,85 @@
1
+ import { signal } from './signals.js';
2
+ import { setErrorReporter } from './error-reporter.js';
3
+
4
+ const errors = signal([]);
5
+
6
+ let lastSentKey = null;
7
+ let lastSentAt = 0;
8
+
9
+ let lastStoredKey = null;
10
+ let lastStoredAt = 0;
11
+
12
+ export function reportError(error, info = {}) {
13
+ const err = error instanceof Error ? error : new Error(String(error));
14
+ const stack = err.stack ? String(err.stack) : '';
15
+ const message = err.message;
16
+ const phase = info.phase ?? null;
17
+ const component = info.component ?? null;
18
+ const key = `${message}|${component ?? ''}|${phase ?? ''}|${stack}`;
19
+ const now = Date.now();
20
+
21
+ if (lastStoredKey === key && (now - lastStoredAt) < 1500) {
22
+ return;
23
+ }
24
+ lastStoredKey = key;
25
+ lastStoredAt = now;
26
+
27
+ const entry = {
28
+ error: err,
29
+ message,
30
+ stack,
31
+ phase,
32
+ component,
33
+ time: now
34
+ };
35
+
36
+ const current = typeof errors.peek === 'function' ? errors.peek() : errors();
37
+ errors([entry, ...(Array.isArray(current) ? current : [])]);
38
+
39
+ try {
40
+ const where = entry.component ? ` in ${entry.component}` : '';
41
+ const phase = entry.phase ? ` (${entry.phase})` : '';
42
+ const label = `[round] Runtime error${where}${phase}`;
43
+
44
+ if (typeof console.groupCollapsed === 'function') {
45
+ console.groupCollapsed(label);
46
+ console.error(entry.error);
47
+ if (entry.stack) console.log(entry.stack);
48
+ if (info && Object.keys(info).length) console.log('info:', info);
49
+ console.groupEnd();
50
+ } else {
51
+ console.error(label);
52
+ console.error(entry.error);
53
+ if (entry.stack) console.log(entry.stack);
54
+ if (info && Object.keys(info).length) console.log('info:', info);
55
+ }
56
+ } catch {
57
+ }
58
+
59
+ try {
60
+ if (import.meta?.hot && typeof import.meta.hot.send === 'function') {
61
+ if (lastSentKey !== key || (now - lastSentAt) > 1500) {
62
+ lastSentKey = key;
63
+ lastSentAt = now;
64
+ import.meta.hot.send('round:runtime-error', {
65
+ message: entry.message,
66
+ stack: entry.stack ? String(entry.stack) : '',
67
+ phase: entry.phase,
68
+ component: entry.component,
69
+ time: entry.time
70
+ });
71
+ }
72
+ }
73
+ } catch {
74
+ }
75
+ }
76
+
77
+ export function clearErrors() {
78
+ errors([]);
79
+ }
80
+
81
+ export function useErrors() {
82
+ return errors;
83
+ }
84
+
85
+ setErrorReporter(reportError);
@@ -0,0 +1,152 @@
1
+ import { createElement } from './dom.js';
2
+ import { clearErrors, useErrors, reportError } from './error-store.js';
3
+
4
+ export { reportError } from './error-store.js';
5
+
6
+ export function ErrorProvider(props = {}) {
7
+ return createElement('span', { style: { display: 'contents' } }, () => {
8
+ const list = useErrors()();
9
+ if (!Array.isArray(list) || list.length === 0) return props.children ?? null;
10
+
11
+ const first = list[0];
12
+
13
+ return createElement(
14
+ 'div',
15
+ {
16
+ style: {
17
+ position: 'fixed',
18
+ inset: '0',
19
+ zIndex: 2147483647,
20
+ display: 'flex',
21
+ alignItems: 'center',
22
+ justifyContent: 'center',
23
+ padding: '24px',
24
+ background: 'rgba(17, 24, 39, 0.72)',
25
+ backdropFilter: 'blur(10px)',
26
+ WebkitBackdropFilter: 'blur(10px)'
27
+ }
28
+ },
29
+ createElement(
30
+ 'div',
31
+ {
32
+ style: {
33
+ width: 'min(900px, 100%)',
34
+ borderRadius: '14px',
35
+ border: '1px solid rgba(255,255,255,0.12)',
36
+ background: 'rgba(0,0,0,0.55)',
37
+ boxShadow: '0 30px 80px rgba(0,0,0,0.55)',
38
+ color: '#fff',
39
+ overflow: 'hidden'
40
+ }
41
+ },
42
+ createElement(
43
+ 'div',
44
+ {
45
+ style: {
46
+ padding: '14px 16px',
47
+ display: 'flex',
48
+ alignItems: 'center',
49
+ gap: '10px',
50
+ borderBottom: '1px solid rgba(255,255,255,0.10)',
51
+ background: 'linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0))'
52
+ }
53
+ },
54
+ createElement('div', {
55
+ style: {
56
+ width: '10px',
57
+ height: '10px',
58
+ borderRadius: '999px',
59
+ background: '#ef4444',
60
+ boxShadow: '0 0 0 4px rgba(239,68,68,0.18)'
61
+ }
62
+ }),
63
+ createElement('strong', { style: { fontFamily: 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial' } }, 'Round Error'),
64
+ createElement('span', { style: { opacity: 0.75, fontFamily: 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial', fontSize: '12px' } }, new Date(first.time).toLocaleString()),
65
+ createElement('button', {
66
+ style: {
67
+ marginLeft: 'auto',
68
+ border: '1px solid rgba(255,255,255,0.16)',
69
+ background: 'rgba(255,255,255,0.08)',
70
+ color: '#fff',
71
+ padding: '8px 10px',
72
+ borderRadius: '10px',
73
+ cursor: 'pointer'
74
+ },
75
+ onMouseOver: (e) => { try { e.currentTarget.style.background = 'rgba(255,255,255,0.12)'; } catch { } },
76
+ onMouseOut: (e) => { try { e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; } catch { } },
77
+ onClick: () => clearErrors()
78
+ }, 'Dismiss')
79
+ ),
80
+ createElement(
81
+ 'div',
82
+ {
83
+ style: {
84
+ padding: '16px',
85
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
86
+ }
87
+ },
88
+ createElement('div', { style: { fontSize: '14px', fontWeight: '700' } }, String(first.message ?? 'Error')),
89
+ createElement(
90
+ 'div',
91
+ { style: { marginTop: '10px', opacity: 0.85, fontSize: '12px', lineHeight: '18px' } },
92
+ first.component ? createElement('div', null, createElement('span', { style: { opacity: 0.75 } }, 'Component: '), String(first.component)) : null,
93
+ first.phase ? createElement('div', null, createElement('span', { style: { opacity: 0.75 } }, 'Phase: '), String(first.phase)) : null
94
+ ),
95
+ first.stack
96
+ ? createElement('pre', {
97
+ style: {
98
+ marginTop: '12px',
99
+ padding: '12px',
100
+ borderRadius: '12px',
101
+ background: 'rgba(0,0,0,0.55)',
102
+ border: '1px solid rgba(255,255,255,0.10)',
103
+ whiteSpace: 'pre-wrap',
104
+ fontSize: '12px',
105
+ lineHeight: '18px',
106
+ overflow: 'auto',
107
+ maxHeight: '55vh'
108
+ }
109
+ }, String(first.stack))
110
+ : null
111
+ )
112
+ )
113
+ );
114
+ });
115
+ }
116
+
117
+ export function initErrorHandling(container) {
118
+ if (typeof document === 'undefined') return;
119
+ if (!container || !(container instanceof Element)) return;
120
+
121
+ if (!document.querySelector('[data-round-error-style="1"]')) {
122
+ const style = document.createElement('style');
123
+ style.setAttribute('data-round-error-style', '1');
124
+ style.textContent = `
125
+ [data-round-error-root="1"] pre{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,0.28) rgba(255,255,255,0.06);}
126
+ [data-round-error-root="1"] pre::-webkit-scrollbar{width:10px;height:10px;}
127
+ [data-round-error-root="1"] pre::-webkit-scrollbar-track{background:rgba(255,255,255,0.06);border-radius:999px;}
128
+ [data-round-error-root="1"] pre::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.22);border-radius:999px;border:2px solid rgba(0,0,0,0.35);}
129
+ [data-round-error-root="1"] pre::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.32);}
130
+ `.trim();
131
+ document.head.appendChild(style);
132
+ }
133
+
134
+ if (!document.querySelector('[data-round-error-root="1"]')) {
135
+ const root = document.createElement('div');
136
+ root.setAttribute('data-round-error-root', '1');
137
+ container.appendChild(root);
138
+ root.appendChild(createElement(ErrorProvider, null));
139
+ }
140
+
141
+ if (!window.__round_error_handlers_installed) {
142
+ window.__round_error_handlers_installed = true;
143
+
144
+ window.addEventListener('error', (e) => {
145
+ reportError(e?.error ?? e?.message ?? e, { phase: 'window.error' });
146
+ });
147
+
148
+ window.addEventListener('unhandledrejection', (e) => {
149
+ reportError(e?.reason ?? e, { phase: 'window.unhandledrejection' });
150
+ });
151
+ }
152
+ }