what-core 0.4.1 → 0.5.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/render.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export {
2
+ template,
3
+ insert,
4
+ mapArray,
5
+ spread,
6
+ delegateEvents,
7
+ on,
8
+ classList,
9
+ effect,
10
+ untrack,
11
+ } from './index';
package/src/a11y.js CHANGED
@@ -24,6 +24,30 @@ export function useFocus() {
24
24
  };
25
25
  }
26
26
 
27
+ // Capture/restore focus around transient UI (dialogs, popovers, drawers).
28
+ // Parent components can call capture() before opening and restore() on close.
29
+ export function useFocusRestore() {
30
+ const previousFocusRef = { current: null };
31
+
32
+ function capture(target) {
33
+ if (typeof document === 'undefined') return;
34
+ previousFocusRef.current = target || document.activeElement || null;
35
+ }
36
+
37
+ function restore(fallbackTarget) {
38
+ const target = previousFocusRef.current || fallbackTarget;
39
+ if (target && typeof target.focus === 'function') {
40
+ target.focus();
41
+ }
42
+ }
43
+
44
+ return {
45
+ capture,
46
+ restore,
47
+ previous: () => previousFocusRef.current,
48
+ };
49
+ }
50
+
27
51
  // --- Focus Trap ---
28
52
  // Keep focus within a container (for modals, dialogs, etc.)
29
53
 
@@ -36,7 +60,7 @@ export function useFocusTrap(containerRef) {
36
60
  previousFocus = document.activeElement;
37
61
  const container = containerRef.current || containerRef;
38
62
 
39
- if (!container) return;
63
+ if (!container || typeof container.querySelectorAll !== 'function') return;
40
64
 
41
65
  // Find all focusable elements
42
66
  const focusables = getFocusableElements(container);
@@ -104,14 +128,31 @@ function getFocusableElements(container) {
104
128
 
105
129
  export function FocusTrap({ children, active = true }) {
106
130
  const containerRef = { current: null };
131
+ const refVersion = signal(0);
107
132
  const trap = useFocusTrap(containerRef);
133
+ let trapCleanup = null;
134
+
135
+ const setRef = (el) => {
136
+ containerRef.current = el;
137
+ refVersion.set(v => v + 1);
138
+ };
108
139
 
109
140
  // Scope the effect to the component lifecycle so it disposes on unmount
110
141
  const dispose = effect(() => {
111
- if (active) {
112
- const cleanup = trap.activate();
142
+ // Re-run activation after the ref element is attached/updated.
143
+ refVersion();
144
+
145
+ if (trapCleanup) {
146
+ trapCleanup();
147
+ trapCleanup = null;
148
+ trap.deactivate();
149
+ }
150
+
151
+ if (active && containerRef.current) {
152
+ trapCleanup = trap.activate();
113
153
  return () => {
114
- cleanup?.();
154
+ trapCleanup?.();
155
+ trapCleanup = null;
115
156
  trap.deactivate();
116
157
  };
117
158
  }
@@ -121,10 +162,15 @@ export function FocusTrap({ children, active = true }) {
121
162
  const ctx = getCurrentComponent?.();
122
163
  if (ctx) {
123
164
  ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
124
- ctx._cleanupCallbacks.push(dispose);
165
+ ctx._cleanupCallbacks.push(() => {
166
+ dispose();
167
+ trapCleanup?.();
168
+ trapCleanup = null;
169
+ trap.deactivate();
170
+ });
125
171
  }
126
172
 
127
- return h('div', { ref: containerRef }, children);
173
+ return h('div', { ref: setRef }, children);
128
174
  }
129
175
 
130
176
  // --- Screen Reader Announcements ---
package/src/dom.js CHANGED
@@ -33,6 +33,16 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
33
33
  // Track all mounted component contexts for disposal
34
34
  const mountedComponents = new Set();
35
35
 
36
+ function isDomNode(value) {
37
+ if (!value || typeof value !== 'object') return false;
38
+ if (typeof Node !== 'undefined' && value instanceof Node) return true;
39
+ return typeof value.nodeType === 'number' && typeof value.nodeName === 'string';
40
+ }
41
+
42
+ function isVNode(value) {
43
+ return !!value && typeof value === 'object' && (value._vnode === true || 'tag' in value);
44
+ }
45
+
36
46
  // Dispose a component: run effect cleanups, hook cleanups, onCleanup callbacks
37
47
  function disposeComponent(ctx) {
38
48
  if (ctx.disposed) return;
@@ -60,12 +70,16 @@ function disposeComponent(ctx) {
60
70
  mountedComponents.delete(ctx);
61
71
  }
62
72
 
63
- // Dispose all components attached to a DOM subtree
64
- function disposeTree(node) {
73
+ // Dispose all components and reactive effects attached to a DOM subtree
74
+ export function disposeTree(node) {
65
75
  if (!node) return;
66
76
  if (node._componentCtx) {
67
77
  disposeComponent(node._componentCtx);
68
78
  }
79
+ // Dispose reactive function child effects ({() => ...} wrappers)
80
+ if (node._dispose) {
81
+ try { node._dispose(); } catch (e) { /* already disposed */ }
82
+ }
69
83
  if (node.childNodes) {
70
84
  for (const child of node.childNodes) {
71
85
  disposeTree(child);
@@ -90,7 +104,7 @@ export function mount(vnode, container) {
90
104
 
91
105
  // --- Create DOM from VNode ---
92
106
 
93
- function createDOM(vnode, parent, isSvg) {
107
+ export function createDOM(vnode, parent, isSvg) {
94
108
  // Null/false/true → placeholder comment (preserves child indices for reconciliation)
95
109
  if (vnode == null || vnode === false || vnode === true) {
96
110
  return document.createComment('');
@@ -101,16 +115,34 @@ function createDOM(vnode, parent, isSvg) {
101
115
  return document.createTextNode(String(vnode));
102
116
  }
103
117
 
104
- // Reactive function child creates a text node that updates fine-grained
118
+ // DOM node passthrough (compiler-first components can return real nodes)
119
+ if (isDomNode(vnode)) {
120
+ return vnode;
121
+ }
122
+
123
+ // Reactive function child — creates a wrapper that updates fine-grained
124
+ // Handles both primitives ({() => count()}) and vnodes ({() => items().map(...)})
105
125
  if (typeof vnode === 'function') {
106
- const textNode = document.createTextNode('');
107
- effect(() => {
126
+ const wrapper = document.createElement('what-c');
127
+ let mounted = false;
128
+ const dispose = effect(() => {
108
129
  const val = vnode();
109
- // If the function returns a vnode, we can't upgrade a text node to an element.
110
- // For now, stringify the result. Component-level re-render handles complex cases.
111
- textNode.textContent = (val == null || val === false || val === true) ? '' : String(val);
130
+ // Normalize: null/false/true empty, primitives and vnodes array
131
+ const vnodes = (val == null || val === false || val === true)
132
+ ? []
133
+ : Array.isArray(val) ? val : [val];
134
+ if (!mounted) {
135
+ mounted = true;
136
+ for (const v of vnodes) {
137
+ const node = createDOM(v, wrapper, parent?._isSvg);
138
+ if (node) wrapper.appendChild(node);
139
+ }
140
+ } else {
141
+ reconcileChildren(wrapper, vnodes);
142
+ }
112
143
  });
113
- return textNode;
144
+ wrapper._dispose = dispose;
145
+ return wrapper;
114
146
  }
115
147
 
116
148
  // Array (fragment)
@@ -123,6 +155,11 @@ function createDOM(vnode, parent, isSvg) {
123
155
  return frag;
124
156
  }
125
157
 
158
+ // Unknown object child fallback
159
+ if (!isVNode(vnode)) {
160
+ return document.createTextNode(String(vnode));
161
+ }
162
+
126
163
  // Component
127
164
  if (typeof vnode.tag === 'function') {
128
165
  return createComponent(vnode, parent, isSvg);
@@ -137,9 +174,16 @@ function createDOM(vnode, parent, isSvg) {
137
174
  : document.createElement(vnode.tag);
138
175
 
139
176
  applyProps(el, vnode.props, {}, svgContext);
140
- for (const child of vnode.children) {
141
- const node = createDOM(child, el, svgContext && vnode.tag !== 'foreignObject');
142
- if (node) el.appendChild(node);
177
+ const hasRawHtml = vnode.props && (
178
+ Object.prototype.hasOwnProperty.call(vnode.props, 'dangerouslySetInnerHTML') ||
179
+ Object.prototype.hasOwnProperty.call(vnode.props, 'innerHTML')
180
+ );
181
+
182
+ if (!hasRawHtml) {
183
+ for (const child of vnode.children) {
184
+ const node = createDOM(child, el, svgContext && vnode.tag !== 'foreignObject');
185
+ if (node) el.appendChild(node);
186
+ }
143
187
  }
144
188
 
145
189
  // Store vnode on element for diffing
@@ -265,6 +309,7 @@ function createComponent(vnode, parent, isSvg) {
265
309
  });
266
310
 
267
311
  ctx.effects.push(dispose);
312
+ wrapper._vnode = vnode; // Store vnode for keyed reconciliation
268
313
  return wrapper;
269
314
  }
270
315
 
@@ -627,18 +672,41 @@ function patchNode(parent, domNode, vnode) {
627
672
  return placeholder;
628
673
  }
629
674
 
630
- // Reactive function child — replace whatever's there with a reactive text node
675
+ // Reactive function child — replace whatever's there with a reactive wrapper
631
676
  if (typeof vnode === 'function') {
632
- const textNode = document.createTextNode('');
633
- effect(() => {
677
+ const wrapper = document.createElement('what-c');
678
+ let mounted = false;
679
+ const dispose = effect(() => {
634
680
  const val = vnode();
635
- textNode.textContent = (val == null || val === false || val === true) ? '' : String(val);
681
+ const vnodes = (val == null || val === false || val === true)
682
+ ? []
683
+ : Array.isArray(val) ? val : [val];
684
+ if (!mounted) {
685
+ mounted = true;
686
+ for (const v of vnodes) {
687
+ const node = createDOM(v, wrapper);
688
+ if (node) wrapper.appendChild(node);
689
+ }
690
+ } else {
691
+ reconcileChildren(wrapper, vnodes);
692
+ }
636
693
  });
694
+ wrapper._dispose = dispose;
637
695
  if (domNode && domNode.parentNode) {
638
696
  disposeTree(domNode);
639
- parent.replaceChild(textNode, domNode);
697
+ parent.replaceChild(wrapper, domNode);
640
698
  }
641
- return textNode;
699
+ return wrapper;
700
+ }
701
+
702
+ // DOM node passthrough
703
+ if (isDomNode(vnode)) {
704
+ if (domNode === vnode) return domNode;
705
+ if (domNode && domNode.parentNode) {
706
+ disposeTree(domNode);
707
+ parent.replaceChild(vnode, domNode);
708
+ }
709
+ return vnode;
642
710
  }
643
711
 
644
712
  // Text
@@ -707,6 +775,19 @@ function patchNode(parent, domNode, vnode) {
707
775
  return startMarker;
708
776
  }
709
777
 
778
+ // Unknown object child fallback
779
+ if (!isVNode(vnode)) {
780
+ const text = String(vnode);
781
+ if (domNode.nodeType === 3) {
782
+ if (domNode.textContent !== text) domNode.textContent = text;
783
+ return domNode;
784
+ }
785
+ const newNode = document.createTextNode(text);
786
+ disposeTree(domNode);
787
+ parent.replaceChild(newNode, domNode);
788
+ return newNode;
789
+ }
790
+
710
791
  // Component
711
792
  if (typeof vnode.tag === 'function') {
712
793
  // Check if old node is a component wrapper for the same component
@@ -714,6 +795,7 @@ function patchNode(parent, domNode, vnode) {
714
795
  && domNode._componentCtx.Component === vnode.tag) {
715
796
  // Same component — update props reactively, let its effect re-render
716
797
  domNode._componentCtx._propsSignal.set({ ...vnode.props, children: vnode.children });
798
+ domNode._vnode = vnode; // Keep vnode current for keyed reconciliation
717
799
  return domNode;
718
800
  }
719
801
  // Different component or not a component — dispose old, create new
@@ -726,8 +808,26 @@ function patchNode(parent, domNode, vnode) {
726
808
  // Element: same tag? Patch props + children
727
809
  if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag) {
728
810
  const oldProps = domNode._vnode?.props || {};
729
- applyProps(domNode, vnode.props, oldProps);
730
- reconcileChildren(domNode, vnode.children);
811
+ const nextProps = vnode.props || {};
812
+ const hadRawHtml = Object.prototype.hasOwnProperty.call(oldProps, 'dangerouslySetInnerHTML')
813
+ || Object.prototype.hasOwnProperty.call(oldProps, 'innerHTML');
814
+ const hasRawHtml = Object.prototype.hasOwnProperty.call(nextProps, 'dangerouslySetInnerHTML')
815
+ || Object.prototype.hasOwnProperty.call(nextProps, 'innerHTML');
816
+
817
+ // If switching from normal children to raw HTML, dispose existing child effects first.
818
+ if (hasRawHtml && !hadRawHtml) {
819
+ for (const child of Array.from(domNode.childNodes)) {
820
+ disposeTree(child);
821
+ }
822
+ }
823
+
824
+ applyProps(domNode, nextProps, oldProps);
825
+
826
+ // Raw HTML props own the element's children. Skip vnode child reconciliation.
827
+ if (!hasRawHtml) {
828
+ reconcileChildren(domNode, vnode.children);
829
+ }
830
+
731
831
  domNode._vnode = vnode;
732
832
  return domNode;
733
833
  }
@@ -856,7 +956,17 @@ function setProp(el, key, value, isSvg) {
856
956
 
857
957
  // dangerouslySetInnerHTML
858
958
  if (key === 'dangerouslySetInnerHTML') {
859
- el.innerHTML = value.__html;
959
+ el.innerHTML = value?.__html ?? '';
960
+ return;
961
+ }
962
+
963
+ // innerHTML convenience alias
964
+ if (key === 'innerHTML') {
965
+ if (value && typeof value === 'object' && '__html' in value) {
966
+ el.innerHTML = value.__html ?? '';
967
+ } else {
968
+ el.innerHTML = value ?? '';
969
+ }
860
970
  return;
861
971
  }
862
972
 
@@ -912,5 +1022,10 @@ function removeProp(el, key, oldValue) {
912
1022
  return;
913
1023
  }
914
1024
 
1025
+ if (key === 'dangerouslySetInnerHTML' || key === 'innerHTML') {
1026
+ el.innerHTML = '';
1027
+ return;
1028
+ }
1029
+
915
1030
  el.removeAttribute(key);
916
1031
  }
package/src/form.js CHANGED
@@ -1,13 +1,29 @@
1
1
  // What Framework - Form Utilities
2
2
  // Controlled inputs, validation, and form state management
3
3
 
4
- import { signal, computed, batch, effect } from './reactive.js';
4
+ import { signal, computed, batch } from './reactive.js';
5
+ import { getCurrentComponent } from './dom.js';
5
6
  import { h } from './h.js';
6
7
 
7
8
  // --- useForm Hook ---
8
9
  // Complete form state management with validation
9
10
 
10
11
  export function useForm(options = {}) {
12
+ // Hook-stable behavior inside components
13
+ const ctx = getCurrentComponent?.();
14
+ if (ctx) {
15
+ const index = ctx.hookIndex++;
16
+ if (!ctx.hooks[index]) {
17
+ ctx.hooks[index] = createFormController(options);
18
+ }
19
+ return ctx.hooks[index];
20
+ }
21
+
22
+ // Standalone usage outside component scope
23
+ return createFormController(options);
24
+ }
25
+
26
+ function createFormController(options = {}) {
11
27
  const {
12
28
  defaultValues = {},
13
29
  mode = 'onSubmit', // 'onSubmit' | 'onChange' | 'onBlur'
@@ -19,6 +35,7 @@ export function useForm(options = {}) {
19
35
  const fieldSignals = {};
20
36
  const errorSignals = {};
21
37
  const touchedSignals = {};
38
+ const errorsState = signal({});
22
39
 
23
40
  function getFieldSignal(name) {
24
41
  if (!fieldSignals[name]) {
@@ -47,32 +64,55 @@ export function useForm(options = {}) {
47
64
  const isSubmitted = signal(false);
48
65
  const submitCount = signal(0);
49
66
 
50
- // Helper: get all current values as a plain object
51
- function getAllValues() {
67
+ // Helper: get all current values as a plain object.
68
+ // tracked=true subscribes to all known fields; tracked=false is snapshot-only.
69
+ function getAllValues(tracked = false) {
52
70
  const result = { ...defaultValues };
53
71
  for (const [name, sig] of Object.entries(fieldSignals)) {
54
- result[name] = sig.peek();
72
+ result[name] = tracked ? sig() : sig.peek();
55
73
  }
56
74
  return result;
57
75
  }
58
76
 
59
- // Helper: get all current errors as a plain object
60
- function getAllErrors() {
61
- const result = {};
62
- for (const [name, sig] of Object.entries(errorSignals)) {
63
- const err = sig.peek();
64
- if (err) result[name] = err;
65
- }
66
- return result;
77
+ // Helper: get all current errors as a plain object.
78
+ // tracked=true subscribes to all known field errors; tracked=false is snapshot-only.
79
+ function getAllErrors(tracked = false) {
80
+ return tracked ? errorsState() : errorsState.peek();
81
+ }
82
+
83
+ function setFieldError(name, error) {
84
+ const nextError = error ?? null;
85
+ getErrorSignal(name).set(nextError);
86
+ errorsState.set((prev) => {
87
+ const prevError = prev[name];
88
+ if (prevError === nextError) return prev;
89
+ if (nextError == null) {
90
+ if (!Object.prototype.hasOwnProperty.call(prev, name)) return prev;
91
+ const next = { ...prev };
92
+ delete next[name];
93
+ return next;
94
+ }
95
+ return { ...prev, [name]: nextError };
96
+ });
97
+ }
98
+
99
+ function replaceAllErrors(nextErrors = {}) {
100
+ const normalized = nextErrors || {};
101
+ batch(() => {
102
+ for (const [name, sig] of Object.entries(errorSignals)) {
103
+ if (!Object.prototype.hasOwnProperty.call(normalized, name)) {
104
+ sig.set(null);
105
+ }
106
+ }
107
+ for (const [name, err] of Object.entries(normalized)) {
108
+ getErrorSignal(name).set(err ?? null);
109
+ }
110
+ errorsState.set({ ...normalized });
111
+ });
67
112
  }
68
113
 
69
114
  // Computed states
70
- const isValid = computed(() => {
71
- for (const sig of Object.values(errorSignals)) {
72
- if (sig()) return false;
73
- }
74
- return true;
75
- });
115
+ const isValid = computed(() => Object.keys(getAllErrors(true)).length === 0);
76
116
 
77
117
  const dirtyFields = computed(() => {
78
118
  const dirty = {};
@@ -88,31 +128,16 @@ export function useForm(options = {}) {
88
128
  async function validate(fieldName) {
89
129
  if (!resolver) return true;
90
130
 
91
- const result = await resolver(getAllValues());
131
+ const result = await resolver(getAllValues(false));
132
+ const nextErrors = result?.errors || {};
92
133
 
93
134
  if (fieldName) {
94
- // Validate single field only update that field's error signal
95
- const errSig = getErrorSignal(fieldName);
96
- if (result.errors[fieldName]) {
97
- errSig.set(result.errors[fieldName]);
98
- return false;
99
- } else {
100
- errSig.set(null);
101
- return true;
102
- }
135
+ const nextError = nextErrors[fieldName] ?? null;
136
+ setFieldError(fieldName, nextError);
137
+ return !nextError;
103
138
  } else {
104
- // Validate all fields
105
- batch(() => {
106
- // Clear existing errors
107
- for (const sig of Object.values(errorSignals)) {
108
- sig.set(null);
109
- }
110
- // Set new errors
111
- for (const [name, err] of Object.entries(result.errors || {})) {
112
- getErrorSignal(name).set(err);
113
- }
114
- });
115
- return Object.keys(result.errors || {}).length === 0;
139
+ replaceAllErrors(nextErrors);
140
+ return Object.keys(nextErrors).length === 0;
116
141
  }
117
142
  }
118
143
 
@@ -162,21 +187,17 @@ export function useForm(options = {}) {
162
187
 
163
188
  // Set error for a field
164
189
  function setError(name, error) {
165
- getErrorSignal(name).set(error);
190
+ setFieldError(name, error);
166
191
  }
167
192
 
168
193
  // Clear error for a field
169
194
  function clearError(name) {
170
- getErrorSignal(name).set(null);
195
+ setFieldError(name, null);
171
196
  }
172
197
 
173
198
  // Clear all errors
174
199
  function clearErrors() {
175
- batch(() => {
176
- for (const sig of Object.values(errorSignals)) {
177
- sig.set(null);
178
- }
179
- });
200
+ replaceAllErrors({});
180
201
  }
181
202
 
182
203
  // Reset form
@@ -188,6 +209,7 @@ export function useForm(options = {}) {
188
209
  for (const sig of Object.values(errorSignals)) {
189
210
  sig.set(null);
190
211
  }
212
+ errorsState.set({});
191
213
  for (const sig of Object.values(touchedSignals)) {
192
214
  sig.set(false);
193
215
  }
@@ -210,7 +232,7 @@ export function useForm(options = {}) {
210
232
  if (isFormValid) {
211
233
  await onValid(getAllValues());
212
234
  } else if (onInvalid) {
213
- onInvalid(getAllErrors());
235
+ onInvalid(getAllErrors(false));
214
236
  }
215
237
 
216
238
  isSubmitting.set(false);
@@ -223,7 +245,7 @@ export function useForm(options = {}) {
223
245
  return computed(() => getFieldSignal(name)());
224
246
  }
225
247
  // Watch all: return a computed that reads all field signals
226
- return computed(() => getAllValues());
248
+ return computed(() => getAllValues(true));
227
249
  }
228
250
 
229
251
  return {
@@ -237,10 +259,11 @@ export function useForm(options = {}) {
237
259
  reset,
238
260
  watch,
239
261
  validate,
240
- // Form state — uses getters for errors/touched to enable per-field granularity
262
+ // Form state
241
263
  formState: {
242
- get values() { return getAllValues(); },
243
- get errors() { return getAllErrors(); },
264
+ get values() { return getAllValues(true); },
265
+ get errors() { return getAllErrors(true); },
266
+ error: (name) => getErrorSignal(name)(),
244
267
  get touched() {
245
268
  const result = {};
246
269
  for (const [name, sig] of Object.entries(touchedSignals)) {
@@ -497,8 +520,16 @@ export function Radio(props) {
497
520
 
498
521
  // --- Form Error Display ---
499
522
 
500
- export function ErrorMessage({ name, errors, render }) {
501
- const error = errors ? errors()[name] : null;
523
+ export function ErrorMessage({ name, formState, errors, render }) {
524
+ const error = formState && typeof formState.error === 'function'
525
+ ? formState.error(name)
526
+ : (
527
+ (
528
+ formState?.errors != null
529
+ ? formState.errors
530
+ : (typeof errors === 'function' ? errors() : errors)
531
+ )?.[name] || null
532
+ );
502
533
  if (!error) return null;
503
534
 
504
535
  if (render) {
package/src/helpers.js CHANGED
@@ -1,18 +1,7 @@
1
1
  // What Framework - Helpers & Utilities
2
2
  // Commonly needed patterns, zero overhead.
3
3
 
4
- import { signal, effect, computed, batch, __DEV__ } from './reactive.js';
5
-
6
- // --- show(condition, vnode) --- [DEPRECATED: use <Show> component instead]
7
- // Conditional rendering. More readable than ternary.
8
- let _showWarned = false;
9
- export function show(condition, vnode, fallback = null) {
10
- if (!_showWarned) {
11
- _showWarned = true;
12
- console.warn('[what] show() is deprecated. Use the <Show> component or ternary expressions instead.');
13
- }
14
- return condition ? vnode : fallback;
15
- }
4
+ import { signal, effect, __DEV__ } from './reactive.js';
16
5
 
17
6
  // --- each(list, fn) --- [DEPRECATED: use <For> component or .map() instead]
18
7
  // Keyed list rendering. Optimized for reconciliation.
package/src/hooks.js CHANGED
@@ -20,6 +20,8 @@ function getHook(ctx) {
20
20
  return { index, exists: index < ctx.hooks.length };
21
21
  }
22
22
 
23
+ let _useMemoNoDepsWarned = false;
24
+
23
25
  // --- useState ---
24
26
  // Returns [value, setter]. Setter triggers re-render of this component only.
25
27
 
@@ -97,6 +99,15 @@ export function useMemo(fn, deps) {
97
99
  const ctx = getCtx();
98
100
  const { index, exists } = getHook(ctx);
99
101
 
102
+ if (__DEV__ && deps === undefined && !_useMemoNoDepsWarned) {
103
+ _useMemoNoDepsWarned = true;
104
+ console.warn(
105
+ '[what] useMemo() called without a deps array. ' +
106
+ 'This recomputes every render. Use useComputed() for signal-derived values, ' +
107
+ 'or pass deps to useMemo().'
108
+ );
109
+ }
110
+
100
111
  if (!exists) {
101
112
  ctx.hooks[index] = { value: undefined, deps: undefined };
102
113
  }
package/src/index.js CHANGED
@@ -41,7 +41,6 @@ export { Head, clearHead } from './head.js';
41
41
 
42
42
  // Utilities
43
43
  export {
44
- show,
45
44
  each,
46
45
  cls,
47
46
  style,
@@ -87,6 +86,7 @@ export {
87
86
  // Accessibility utilities
88
87
  export {
89
88
  useFocus,
89
+ useFocusRestore,
90
90
  useFocusTrap,
91
91
  FocusTrap,
92
92
  announce,