what-core 0.12.3 → 0.12.4

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/index.d.ts CHANGED
@@ -28,7 +28,13 @@ export function signal<T>(initial: T, debugName?: string): Signal<T>;
28
28
  export function computed<T>(fn: () => T): Computed<T>;
29
29
  export function effect(fn: () => void | (() => void), opts?: { stable?: boolean }): () => void;
30
30
  export function signalMemo<T>(fn: () => T): Computed<T>;
31
- export function batch<T>(fn: () => T): T;
31
+ /**
32
+ * Group signal writes so effects run once at the end. The callback's return
33
+ * value is DISCARDED: batch() returns undefined. It was declared as `<T>(fn: ()
34
+ * => T) => T`, so `const rows = batch(() => compute())` typechecked and handed
35
+ * back undefined at runtime.
36
+ */
37
+ export function batch(fn: () => unknown): void;
32
38
  export function untrack<T>(fn: () => T): T;
33
39
  export function flushSync(): void;
34
40
  export function createRoot<T>(fn: (dispose: () => void) => T): T;
@@ -106,11 +112,33 @@ export function classList(el: Element, classes: Record<string, boolean | (() =>
106
112
 
107
113
  // --- Hooks ---
108
114
 
109
- export function useState<T>(initial: T | (() => T)): [T, (value: Updater<T>) => void];
115
+ /**
116
+ * Returns [signal, setter]. The first element is the SIGNAL ITSELF, not a
117
+ * snapshot value: components run once, so there is no re-render to hand a new
118
+ * `T` to. Read it by calling it (`count()`), or pass it straight into JSX where
119
+ * insert() binds it reactively.
120
+ *
121
+ * It was declared as `[T, setter]`, which made the correct code (`count()`) a
122
+ * type error and the wrong code (`count + 1`) type-check.
123
+ */
124
+ export function useState<T>(initial: T | (() => T)): [Signal<T>, (value: Updater<T>) => void];
110
125
  export function useSignal<T>(initial: T | (() => T)): Signal<T>;
111
126
  export function useComputed<T>(fn: () => T): Computed<T>;
112
127
  export function useEffect(fn: () => void | (() => void), deps?: unknown[]): void;
113
- export function useMemo<T>(fn: () => T, deps?: unknown[]): T;
128
+ /**
129
+ * Returns a COMPUTED ACCESSOR, not the value. Same reason as useState: the
130
+ * component body runs once, so nothing would ever hand back a refreshed `T`.
131
+ * Read it by calling it (`total()`), or pass the accessor straight into JSX
132
+ * where insert() tracks it.
133
+ *
134
+ * `deps` is accepted for React familiarity and ignored at runtime: computed()
135
+ * tracks whatever signals the callback actually reads.
136
+ *
137
+ * It was declared as `T`, which inverted both halves: `useMemo(...) * 2`
138
+ * type-checked and produced NaN, while the correct `useMemo(...)()` was the
139
+ * type error.
140
+ */
141
+ export function useMemo<T>(fn: () => T, deps?: unknown[]): Computed<T>;
114
142
  export function useCallback<T extends (...args: any[]) => any>(fn: T, deps?: unknown[]): T;
115
143
  export function useRef<T>(initial: T): { current: T };
116
144
 
@@ -121,11 +149,19 @@ export interface Context<T> {
121
149
 
122
150
  export function createContext<T>(defaultValue: T): Context<T>;
123
151
  export function useContext<T>(context: Context<T>): T;
152
+ /**
153
+ * Returns [signal, dispatch]. The first element is the SIGNAL ITSELF, exactly
154
+ * as useState returns it and for the same run-once reason. Read it with
155
+ * `state()`.
156
+ *
157
+ * It was declared as `[S, dispatch]`, so `state.items` type-checked (and read
158
+ * `undefined` off a function object) while `state()` did not.
159
+ */
124
160
  export function useReducer<S, A>(
125
161
  reducer: (state: S, action: A) => S,
126
162
  initialState: S,
127
163
  init?: (initial: S) => S,
128
- ): [S, (action: A) => void];
164
+ ): [Signal<S>, (action: A) => void];
129
165
  export function onMount(fn: () => void): void;
130
166
  export function onCleanup(fn: () => void): void;
131
167
 
@@ -190,11 +226,22 @@ export function Island(props: IslandProps): VNode;
190
226
 
191
227
  // --- State ---
192
228
 
193
- export type DerivedFn<T> = ((state: any) => T) & { _isDerived: true };
229
+ /** `_storeComputed` is the marker createStore() actually reads to tell a derived value from an action. */
230
+ export type DerivedFn<T> = ((state: any) => T) & { _storeComputed: true };
194
231
  export function derived<T>(fn: (state: any) => T): DerivedFn<T>;
232
+ /** @deprecated Use derived(). Warns once at runtime. */
195
233
  export function storeComputed<T>(fn: (state: any) => T): DerivedFn<T>;
196
234
  export type StoreDefinition = Record<string, any>;
197
- export type Store<T extends StoreDefinition> = T;
235
+ /**
236
+ * The store hook resolves every derived key to its VALUE. State and derived
237
+ * keys are both getters on the returned object, only actions stay callable.
238
+ * `Store<T> = T` left a derived key typed as the DerivedFn you wrote in the
239
+ * definition, so `store.total` looked callable and `store.total * 2` was the
240
+ * type error, which is backwards from the runtime.
241
+ */
242
+ export type Store<T extends StoreDefinition> = {
243
+ [K in keyof T]: T[K] extends DerivedFn<infer U> ? U : T[K];
244
+ };
198
245
  export function createStore<T extends StoreDefinition>(definition: T): () => Store<T>;
199
246
  export function atom<T>(initial: T): Signal<T>;
200
247
 
@@ -280,31 +327,153 @@ export function smoothScrollTo(
280
327
  ): Promise<void>;
281
328
 
282
329
  // --- Animation ---
330
+ //
331
+ // Every accessor below is a plain getter function, NOT a Signal: the objects
332
+ // these factories return expose reads through closures and offer writes as
333
+ // named methods (set/snap/setValue), so nothing here carries .set/.peek. The
334
+ // declarations used to promise Signal<number> and a single shared SpringValue
335
+ // shape for both spring() and tween(), which are two different objects.
336
+
337
+ export interface SpringConfig {
338
+ stiffness?: number;
339
+ damping?: number;
340
+ mass?: number;
341
+ precision?: number;
342
+ }
283
343
 
344
+ /** Physics-based value. `set()` retargets and animates; `snap()` jumps without animating. */
284
345
  export interface SpringValue {
285
346
  current(): number;
286
- set(value: number): void;
347
+ target(): number;
348
+ velocity(): number;
349
+ isAnimating(): boolean;
350
+ set(target: number): void;
287
351
  stop(): void;
288
- reset(): void;
352
+ snap(value: number): void;
353
+ subscribe(fn: (value: number) => void): () => void;
354
+ }
355
+
356
+ export interface TweenConfig {
357
+ duration?: number;
358
+ easing?: (t: number) => number;
359
+ onUpdate?: (value: number, progress: number) => void;
360
+ onComplete?: () => void;
361
+ }
362
+
363
+ /**
364
+ * Easing-based interpolation from `from` to `to`. Starts immediately on
365
+ * creation and runs to completion, so it exposes `cancel()` rather than the
366
+ * spring's retargeting `set()`/`stop()`.
367
+ */
368
+ export interface TweenValue {
369
+ progress(): number;
370
+ value(): number;
371
+ isAnimating(): boolean;
372
+ cancel(): void;
373
+ subscribe(fn: (value: number) => void): () => void;
289
374
  }
290
375
 
291
- export function spring(initialValue?: number, config?: Record<string, any>): SpringValue;
292
- export function tween(initialValue?: number, config?: Record<string, any>): SpringValue;
376
+ export function spring(initialValue: number, config?: SpringConfig): SpringValue;
377
+ /**
378
+ * Both endpoints are required and positional. This was declared as
379
+ * `tween(initialValue?, config?)` returning a SpringValue, so the documented
380
+ * call `tween(0, 100, { duration: 300 })` was a type error, while
381
+ * `tween(0).current()` type-checked and threw (`current` does not exist, and a
382
+ * missing `to` interpolates toward undefined).
383
+ */
384
+ export function tween(from: number, to: number, config?: TweenConfig): TweenValue;
293
385
  export const easings: Record<string, (t: number) => number>;
294
- export function useTransition(options?: Record<string, any>): {
295
- mounted: Signal<boolean>;
296
- styles: Computed<Record<string, any>>;
297
- show: () => void;
298
- hide: () => void;
386
+ /**
387
+ * Drives a 0..1 progress value over `duration`. This is NOT React's
388
+ * useTransition, and it never had the mounted/styles/show/hide members that
389
+ * were declared here, and all four were undefined at runtime.
390
+ */
391
+ export function useTransition(options?: { duration?: number; easing?: (t: number) => number }): {
392
+ isTransitioning: () => boolean;
393
+ progress: () => number;
394
+ start: (callback?: () => void) => Promise<void>;
299
395
  };
300
- export function useGesture(ref: { current?: Element | null } | Element, handlers?: Record<string, (payload: any) => void>): void;
301
- export function useAnimatedValue(initialValue?: number): {
302
- value: Signal<number>;
303
- animateTo: (target: number, config?: Record<string, any>) => Promise<void>;
396
+
397
+ /** Live gesture state. `startX`/`startY` are plain numbers rewritten in place at gesture start. */
398
+ export interface GestureState {
399
+ isDragging: Signal<boolean>;
400
+ startX: number;
401
+ startY: number;
402
+ currentX: Signal<number>;
403
+ currentY: Signal<number>;
404
+ deltaX: Signal<number>;
405
+ deltaY: Signal<number>;
406
+ velocity: Signal<GestureVelocity>;
407
+ }
408
+
409
+ export interface GestureVelocity {
410
+ x: number;
411
+ y: number;
412
+ }
413
+
414
+ /**
415
+ * `preventDefault` is why this cannot be a `Record<string, (payload) => void>`:
416
+ * it sits in the same object as the callbacks and is a boolean.
417
+ */
418
+ export interface GestureHandlers {
419
+ onDragStart?: (payload: { x: number; y: number }) => void;
420
+ onDrag?: (payload: {
421
+ x: number; y: number; deltaX: number; deltaY: number; velocity: GestureVelocity;
422
+ }) => void;
423
+ onDragEnd?: (payload: { deltaX: number; deltaY: number; velocity: GestureVelocity }) => void;
424
+ onPinch?: (payload: { scale: number; centerX: number; centerY: number }) => void;
425
+ onSwipe?: (payload: { direction: 'up' | 'down' | 'left' | 'right'; velocity: GestureVelocity }) => void;
426
+ onTap?: (payload: { x: number; y: number }) => void;
427
+ onLongPress?: (payload: { x: number; y: number }) => void;
428
+ /** Opt in to e.preventDefault() in the touch handlers (listeners become non-passive). */
429
+ preventDefault?: boolean;
430
+ }
431
+
432
+ /** Returns the gesture state. It was declared `void`, so the state was unreachable from TypeScript. */
433
+ export function useGesture(
434
+ ref: { current?: Element | null } | Element | (() => Element | null),
435
+ handlers?: GestureHandlers,
436
+ ): GestureState;
437
+
438
+ /** Handle for one running animation started by useAnimatedValue. */
439
+ export interface AnimationHandle {
304
440
  stop: () => void;
441
+ }
442
+
443
+ /**
444
+ * `animateTo` and `stop` were declared here and have never existed. Animations
445
+ * are started with spring()/timing(), each returning its own handle to stop.
446
+ */
447
+ export function useAnimatedValue(initialValue: number): {
448
+ value: () => number;
449
+ setValue: (value: number) => void;
450
+ spring: (toValue: number, config?: SpringConfig) => AnimationHandle;
451
+ timing: (toValue: number, config?: TweenConfig) => AnimationHandle;
452
+ interpolate: (inputRange: number[], outputRange: number[]) => () => number;
453
+ subscribe: (fn: (value: number) => void) => () => void;
305
454
  };
306
- export function createTransitionClasses(name: string): string;
307
- export function cssTransition(config: Record<string, any>): Record<string, any>;
455
+
456
+ export interface TransitionClasses {
457
+ enter: string;
458
+ enterActive: string;
459
+ enterDone: string;
460
+ exit: string;
461
+ exitActive: string;
462
+ exitDone: string;
463
+ }
464
+
465
+ /** Returns the six class NAMES for `name`, not a single string. */
466
+ export function createTransitionClasses(name: string): TransitionClasses;
467
+ /**
468
+ * Applies the enter/exit class sequence to an element and resolves when the
469
+ * transition is done. It was declared as a synchronous `(config) => object`.
470
+ */
471
+ export function cssTransition(
472
+ element: Element,
473
+ name: string,
474
+ type?: 'enter' | 'exit',
475
+ duration?: number,
476
+ ): Promise<void>;
308
477
 
309
478
  // --- Accessibility ---
310
479
 
@@ -353,11 +522,51 @@ export function useAriaChecked(initialChecked?: boolean): {
353
522
  checkboxProps: () => Record<string, any>;
354
523
  };
355
524
 
356
- export function useRovingTabIndex(itemCountOrSignal: number | (() => number)): {
525
+ export interface RovingTabIndexOptions {
526
+ /**
527
+ * Container role, emitted by containerProps(). The hook emits NO role by
528
+ * default: roving tabindex is the shared keyboard mechanic of toolbars,
529
+ * menus, trees, grids, tablists, radiogroups and listboxes, and a default
530
+ * spread last would silently overwrite the role the caller wrote beside it.
531
+ */
532
+ role?: string;
533
+ }
534
+
535
+ /**
536
+ * Props for one roving item. `ref` is the item's REGISTRATION (the hook holds
537
+ * the node through it and cannot move focus without it), so a `ref` passed via
538
+ * `overrides` is chained rather than replaced. `tabIndex` is an accessor, not a
539
+ * number: exactly one item is tabbable at a time and which one changes as focus
540
+ * roves, so a resolved value could not survive the spread.
541
+ */
542
+ export interface RovingItemProps {
543
+ ref: { current: Element | null };
544
+ tabIndex: () => number;
545
+ onKeyDown: (e: KeyboardEvent) => void;
546
+ onFocus: (e: Event) => void;
547
+ [prop: string]: any;
548
+ }
549
+
550
+ export function useRovingTabIndex(
551
+ itemCountOrSignal: number | (() => number),
552
+ options?: RovingTabIndexOptions,
553
+ ): {
554
+ /** Active index, clamped to the current count so the group is never untabbable. */
357
555
  focusIndex: () => number;
556
+ /**
557
+ * Set the active index. Out-of-range indexes are REFUSED, not clamped. Moves
558
+ * real focus only when the group already owns it, so syncing from application
559
+ * state cannot steal focus from elsewhere on the page.
560
+ */
358
561
  setFocusIndex: (index: number) => void;
359
- getItemProps: (index: number) => Record<string, any>;
360
- containerProps: () => Record<string, any>;
562
+ /**
563
+ * Explicitly move focus to an item (a menu focusing its first item on open).
564
+ * Returns the element it focused, or null when the index is out of range or
565
+ * the item is not in the DOM.
566
+ */
567
+ focusItem: (index: number) => Element | null;
568
+ getItemProps: (index: number, overrides?: Record<string, any>) => RovingItemProps;
569
+ containerProps: (overrides?: Record<string, any>) => Record<string, any>;
361
570
  };
362
571
 
363
572
  export function VisuallyHidden(props: { children?: VNodeChild; as?: string }): VNode;
@@ -413,7 +622,8 @@ export function Spinner(props?: Record<string, any>): VNode;
413
622
  // --- Data Fetching ---
414
623
 
415
624
  export function useFetch<T = any>(url: string, options?: Record<string, any>): {
416
- data: () => T;
625
+ /** null until the first response lands. It was declared as bare `T`, so strict-mode callers skipped the check they need. */
626
+ data: () => T | null;
417
627
  error: () => any;
418
628
  isLoading: () => boolean;
419
629
  refetch: () => Promise<void>;
@@ -429,36 +639,88 @@ export function useSWR<T = any>(key: string | null | false, fetcher: (key: strin
429
639
  revalidate: () => Promise<T | void>;
430
640
  };
431
641
 
642
+ /**
643
+ * 'idle' is a real state, not a placeholder: a query with `enabled: false` and
644
+ * nothing cached is idle, and reporting it as loading is what renders a spinner
645
+ * that never comes down.
646
+ */
647
+ export type QueryStatus = 'idle' | 'loading' | 'success' | 'error';
648
+ /** Whether a request is on the wire, independent of whether data exists. */
649
+ export type FetchStatus = 'idle' | 'fetching';
650
+
651
+ /**
652
+ * Any array key is joined into a single normalized string, so an array and its
653
+ * normalization address the same cache entry everywhere a key is accepted.
654
+ */
655
+ export type QueryKey = string | readonly unknown[];
656
+
432
657
  export function useQuery<T = any>(options: Record<string, any>): {
433
658
  data: () => T | null;
434
659
  error: () => any;
435
- status: () => string;
660
+ status: () => QueryStatus;
661
+ fetchStatus: () => FetchStatus;
436
662
  isLoading: () => boolean;
437
663
  isFetching: () => boolean;
438
664
  isError: () => boolean;
439
665
  isSuccess: () => boolean;
666
+ isIdle: () => boolean;
667
+ /** Whether the `enabled` gate is currently open. */
668
+ isEnabled: () => boolean;
440
669
  refetch: () => Promise<T | void>;
441
670
  };
442
671
 
672
+ /**
673
+ * Pages, plus the param each was fetched with. `pageParams[i]` names `pages[i]`.
674
+ */
675
+ export interface InfiniteData<T> {
676
+ pages: T[];
677
+ pageParams: unknown[];
678
+ }
679
+
680
+ /**
681
+ * `data()` hands back the page CONTAINER, not a flat array. It was declared as
682
+ * `T[]`, so `data().map(...)` type-checked and threw. The rows are in
683
+ * `data().pages`. A `select` option replaces the container wholesale, and that
684
+ * transformation is not expressible here.
685
+ */
443
686
  export function useInfiniteQuery<T = any>(options: Record<string, any>): {
444
- data: () => T[];
687
+ data: () => InfiniteData<T>;
445
688
  error: () => any;
446
- status: () => string;
689
+ status: () => QueryStatus;
447
690
  isLoading: () => boolean;
448
- isFetchingNextPage: () => boolean;
691
+ isError: () => boolean;
692
+ isSuccess: () => boolean;
693
+ isIdle: () => boolean;
694
+ isFetching: () => boolean;
695
+ isEnabled: () => boolean;
449
696
  hasNextPage: () => boolean;
697
+ hasPreviousPage: () => boolean;
698
+ isFetchingNextPage: () => boolean;
699
+ isFetchingPreviousPage: () => boolean;
450
700
  fetchNextPage: () => Promise<void>;
701
+ fetchPreviousPage: () => Promise<void>;
451
702
  refetch: () => Promise<void>;
452
703
  };
453
704
 
705
+ /**
706
+ * Synchronous. It was declared as returning Promise<void>, so `await
707
+ * invalidateQueries(...)` awaited undefined and read as "the refetches are
708
+ * done" when they had only been kicked off. The subscribers it wakes fetch on
709
+ * their own.
710
+ *
711
+ * An array key is a PREFIX unless `exact` is set: invalidateQueries(['todos'])
712
+ * also invalidates ['todos', 1]. `hard` clears the cached value immediately
713
+ * (loading state); the default keeps stale data on screen while it refetches.
714
+ */
454
715
  export function invalidateQueries(
455
- keyOrPredicate: string | ((key: string) => boolean),
456
- options?: { exact?: boolean },
457
- ): Promise<void>;
458
-
459
- export function prefetchQuery<T = any>(key: string, fetcher: (key: string) => Promise<T>): Promise<T>;
460
- export function setQueryData<T = any>(key: string, updater: T | ((prev: T | null) => T)): void;
461
- export function getQueryData<T = any>(key: string): T | null;
716
+ keyOrPredicate: QueryKey | ((key: string) => boolean),
717
+ options?: { exact?: boolean; hard?: boolean },
718
+ ): void;
719
+
720
+ export function prefetchQuery<T = any>(key: QueryKey, fetcher: (key: string) => Promise<T>): Promise<T>;
721
+ export function setQueryData<T = any>(key: QueryKey, updater: T | ((prev: T | null) => T)): void;
722
+ /** `undefined` for a key the cache has never held; `null` for one that was emptied. */
723
+ export function getQueryData<T = any>(key: QueryKey): T | null | undefined;
462
724
  export function clearCache(): void;
463
725
 
464
726
  // --- Forms ---
@@ -469,14 +731,39 @@ export interface FieldError {
469
731
  [key: string]: any;
470
732
  }
471
733
 
472
- export interface RegisterProps {
734
+ /**
735
+ * What register() actually hands back, which varies by control type. The event
736
+ * handlers are LOWERCASE (`oninput`, `onchange`), because that is the key the
737
+ * DOM binding and the merge in <Input>/<Radio> look for; `onInput` was declared
738
+ * here and register() has never defined it, so `register('email').onInput(e)`
739
+ * type-checked and threw at the first keystroke.
740
+ *
741
+ * `value` and `checked` are reactive rather than snapshots: `value` and the
742
+ * checkbox `checked` are getters, and the radio `checked` is a thunk, so that a
743
+ * spread (`{...register('email')}`) binds instead of freezing the value at
744
+ * mount.
745
+ *
746
+ * A closed type alias rather than an interface: the key set really is closed
747
+ * (this is the whole object register() builds), and closing it is what makes
748
+ * the phantom `onInput` an error again instead of an `any` off an index
749
+ * signature. It must stay a `type`, not an `interface`, because only a type
750
+ * alias gets the implicit index signature that `h('input', register(name))`
751
+ * needs to be assignable to Record<string, any>.
752
+ */
753
+ export type RegisterProps = {
473
754
  name: string;
474
- value: any;
475
- onInput: (e: any) => void;
476
755
  onBlur: () => void;
477
756
  onFocus: () => void;
478
757
  ref?: any;
479
- }
758
+ /** text/select/textarea, and the option's own value on a radio. */
759
+ value?: any;
760
+ /** checkbox/radio only. */
761
+ checked?: boolean | (() => boolean);
762
+ /** text/select/textarea only. */
763
+ oninput?: (e: any) => void;
764
+ /** checkbox/radio only. */
765
+ onchange?: (e: any) => void;
766
+ };
480
767
 
481
768
  export interface FormState {
482
769
  readonly values: Record<string, any>;
@@ -485,6 +772,8 @@ export interface FormState {
485
772
  readonly touched: Record<string, boolean>;
486
773
  isDirty: () => boolean;
487
774
  isValid: Computed<boolean>;
775
+ /** True while the resolver is running. Present since resolvers went async; never declared. */
776
+ isValidating: () => boolean;
488
777
  isSubmitting: () => boolean;
489
778
  isSubmitted: () => boolean;
490
779
  submitCount: () => number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-core",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
4
4
  "description": "What Framework - Signal-based UI framework built for AI agents",
5
5
  "type": "module",
6
6
  "main": "src/index.js",