@askrjs/askr 0.0.7 → 0.0.9

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.
@@ -1,4 +1,4 @@
1
- import { P as Props, J as JSXElement } from './jsx-AzPM8gMS.js';
1
+ import { P as Props, J as JSXElement } from './jsx-CSWf4VFg.js';
2
2
 
3
3
  /**
4
4
  * State primitive for Askr components
@@ -1,7 +1,7 @@
1
1
  export { L as LayoutComponent, l as layout } from '../layout-BINPv-nz.js';
2
- import '../types-uOPfcrdz.js';
3
- import { J as JSXElement } from '../jsx-AzPM8gMS.js';
4
- import { S as State } from '../component-BBGWJdqJ.js';
2
+ import '../types-DxdosFWx.js';
3
+ import { J as JSXElement } from '../jsx-CSWf4VFg.js';
4
+ import { S as State } from '../component-DHAn9JxU.js';
5
5
 
6
6
  type SlotProps = {
7
7
  asChild: true;
@@ -21,6 +21,7 @@ type SlotProps = {
21
21
  * 1. asChild Pattern
22
22
  * When asChild=true, merges props into the single child element.
23
23
  * Child must be a valid JSXElement; non-element children return null.
24
+ * **Slot props override child props** (injection pattern).
24
25
  *
25
26
  * 2. Fallback Behavior
26
27
  * When asChild=false, returns a Fragment (structural no-op).
@@ -93,6 +94,133 @@ interface Portal<T = unknown> {
93
94
  declare function definePortal<T = unknown>(): Portal<T>;
94
95
  declare const DefaultPortal: Portal<unknown>;
95
96
 
97
+ /**
98
+ * createCollection
99
+ *
100
+ * Ordered descendant registry for coordinating items without DOM queries.
101
+ *
102
+ * INVARIANTS:
103
+ * 1. Registration order determines item order (no DOM queries)
104
+ * 2. Stable ordering across renders (insertion order preserved)
105
+ * 3. Each item may have metadata (type-safe, user-defined)
106
+ * 4. No implicit global state (explicit collection instances)
107
+ * 5. No automatic cleanup (caller controls lifecycle)
108
+ *
109
+ * DESIGN:
110
+ * - Returns a registry API ({ register, items, clear })
111
+ * - Items are stored in insertion order
112
+ * - Registration returns an unregister function
113
+ * - No side effects on registration (pure data structure)
114
+ *
115
+ * USAGE:
116
+ * const collection = createCollection<HTMLElement, { disabled: boolean }>();
117
+ * const unregister = collection.register(element, { disabled: false });
118
+ * const allItems = collection.items();
119
+ * unregister();
120
+ */
121
+ type CollectionItem<TNode, TMetadata = unknown> = {
122
+ node: TNode;
123
+ metadata: TMetadata;
124
+ };
125
+ interface Collection<TNode, TMetadata = unknown> {
126
+ /**
127
+ * Register a node with optional metadata.
128
+ * Returns an unregister function.
129
+ */
130
+ register(node: TNode, metadata: TMetadata): () => void;
131
+ /**
132
+ * Get all registered items in insertion order.
133
+ */
134
+ items(): ReadonlyArray<CollectionItem<TNode, TMetadata>>;
135
+ /**
136
+ * Clear all registered items.
137
+ */
138
+ clear(): void;
139
+ /**
140
+ * Get the count of registered items.
141
+ */
142
+ size(): number;
143
+ }
144
+ declare function createCollection<TNode, TMetadata = unknown>(): Collection<TNode, TMetadata>;
145
+
146
+ /**
147
+ * createLayer
148
+ *
149
+ * Manages stacking order and coordination for overlays (modals, popovers, etc).
150
+ *
151
+ * INVARIANTS:
152
+ * 1. Layers are ordered by registration time (FIFO)
153
+ * 2. Only the top layer handles Escape key
154
+ * 3. Only the top layer handles outside pointer events
155
+ * 4. Nested layers are supported
156
+ * 5. Does not implement portals (orthogonal concern)
157
+ * 6. No automatic DOM insertion (caller controls mounting)
158
+ *
159
+ * DESIGN:
160
+ * - Returns a layer manager with register/unregister API
161
+ * - Each layer has a unique ID and can query if it's the top layer
162
+ * - Escape and outside pointer coordination via callbacks
163
+ * - No z-index management (CSS concern)
164
+ *
165
+ * USAGE:
166
+ * const manager = createLayer();
167
+ *
168
+ * const layer = manager.register({
169
+ * onEscape: () => { ... },
170
+ * onOutsidePointer: () => { ... }
171
+ * });
172
+ *
173
+ * layer.isTop(); // true if this is the topmost layer
174
+ * layer.unregister();
175
+ */
176
+ interface LayerOptions {
177
+ /**
178
+ * Called when Escape is pressed and this is the top layer
179
+ */
180
+ onEscape?: () => void;
181
+ /**
182
+ * Called when pointer event occurs outside and this is the top layer
183
+ */
184
+ onOutsidePointer?: (e: PointerEvent) => void;
185
+ /**
186
+ * Optional node reference for outside pointer detection
187
+ */
188
+ node?: Node | null;
189
+ }
190
+ interface Layer {
191
+ /**
192
+ * Unique layer ID
193
+ */
194
+ id: number;
195
+ /**
196
+ * Check if this layer is the topmost
197
+ */
198
+ isTop(): boolean;
199
+ /**
200
+ * Remove this layer from the stack
201
+ */
202
+ unregister(): void;
203
+ }
204
+ interface LayerManager {
205
+ /**
206
+ * Register a new layer
207
+ */
208
+ register(options: LayerOptions): Layer;
209
+ /**
210
+ * Get all active layers in order
211
+ */
212
+ layers(): ReadonlyArray<Layer>;
213
+ /**
214
+ * Manually trigger escape handling on the top layer
215
+ */
216
+ handleEscape(): void;
217
+ /**
218
+ * Manually trigger outside pointer handling on the top layer
219
+ */
220
+ handleOutsidePointer(e: PointerEvent): void;
221
+ }
222
+ declare function createLayer(): LayerManager;
223
+
96
224
  /**
97
225
  * composeHandlers
98
226
  *
@@ -318,22 +446,59 @@ declare function pressable({ disabled, onPress, isNativeButton, }: PressableOpti
318
446
  /**
319
447
  * dismissable
320
448
  *
321
- * Provides props and helpers to support dismissal behaviour. This helper is
322
- * runtime-agnostic:
323
- * - It returns `onKeyDown` prop which will call onDismiss when Escape is
324
- * pressed.
325
- * - It also provides `outsideListener` factory which given an `isInside`
326
- * predicate returns a handler suitable to attach at the document level that
327
- * will call onDismiss when the pointerdown target is outside the component.
449
+ * THE dismissal primitive. Handles Escape key and outside interactions.
450
+ *
451
+ * INVARIANTS:
452
+ * 1. Returns props that compose via mergeProps (no factories)
453
+ * 2. Disabled state respected exactly once, here
454
+ * 3. No side effects - pure props generation
455
+ * 4. Outside detection requires explicit node reference
456
+ * 5. This is the ONLY dismissal primitive - do not create alternatives
457
+ *
458
+ * DESIGN:
459
+ * - Returns standard event handler props (onKeyDown, onPointerDownCapture)
460
+ * - Composable via mergeProps with other foundations
461
+ * - Caller provides node reference for outside detection
462
+ * - Single onDismiss callback for all dismiss triggers
463
+ *
464
+ * PIT OF SUCCESS:
465
+ * ✓ Can't accidentally bypass (only way to get dismiss behavior)
466
+ * ✓ Can't duplicate (disabled checked once)
467
+ * ✓ Composes via mergeProps (standard props)
468
+ * ✓ Wrong usage is hard (no factories to misuse)
469
+ *
470
+ * USAGE:
471
+ * const props = dismissable({
472
+ * node: elementRef,
473
+ * disabled: false,
474
+ * onDismiss: () => close()
475
+ * });
476
+ *
477
+ * <div ref={elementRef} {...props}>Content</div>
478
+ *
479
+ * MISUSE EXAMPLE (PREVENTED):
480
+ * ❌ Can't forget to check disabled - checked inside dismissable
481
+ * ❌ Can't create custom escape handler - this is the only one
482
+ * ❌ Can't bypass via direct event listeners - mergeProps composes correctly
328
483
  */
329
484
  interface DismissableOptions {
330
- onDismiss?: () => void;
485
+ /**
486
+ * Reference to the element for outside click detection
487
+ */
488
+ node?: Node | null;
489
+ /**
490
+ * Whether dismiss is disabled
491
+ */
331
492
  disabled?: boolean;
493
+ /**
494
+ * Called when dismiss is triggered (Escape or outside click)
495
+ */
496
+ onDismiss?: (trigger: 'escape' | 'outside') => void;
332
497
  }
333
498
 
334
- declare function dismissable({ onDismiss, disabled }: DismissableOptions): {
335
- onKeyDown: ((e: KeyboardLikeEvent) => void) | undefined;
336
- outsideListener: ((isInside: (target: unknown) => boolean) => (e: PointerLikeEvent) => void) | undefined;
499
+ declare function dismissable({ node, disabled, onDismiss, }: DismissableOptions): {
500
+ onKeyDown: (e: KeyboardLikeEvent) => void;
501
+ onPointerDownCapture: (e: PointerLikeEvent) => void;
337
502
  };
338
503
 
339
504
  /**
@@ -370,4 +535,190 @@ interface HoverableResult {
370
535
  }
371
536
  declare function hoverable({ disabled, onEnter, onLeave, }: HoverableOptions): HoverableResult;
372
537
 
373
- export { type ComposeHandlersOptions, type ControllableState, DefaultPortal, type DismissableOptions, type FocusableOptions, type FocusableResult, type HoverableOptions, type HoverableResult, JSXElement, type Portal, Presence, type PresenceProps, type PressableOptions, type PressableResult, type Ref, Slot, type SlotProps, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, definePortal, dismissable, focusable, hoverable, isControlled, makeControllable, mergeProps, pressable, resolveControllable, setRef, useId };
538
+ /**
539
+ * rovingFocus
540
+ *
541
+ * Single tab stop navigation with arrow-key control.
542
+ *
543
+ * INVARIANTS:
544
+ * 1. Only one item in the group is reachable via Tab (single tab stop)
545
+ * 2. Arrow keys move focus within the group
546
+ * 3. Orientation determines which arrow keys are active
547
+ * 4. Looping is opt-in
548
+ * 5. Disabled items are skipped
549
+ * 6. Returns props objects, never factories (composes via mergeProps)
550
+ *
551
+ * DESIGN:
552
+ * - Container gets onKeyDown for arrow navigation
553
+ * - Each item gets tabIndex based on current selection
554
+ * - Navigation logic is pure - caller controls focus application
555
+ * - Disabled check happens per-item via predicate
556
+ *
557
+ * PIT OF SUCCESS:
558
+ * ✓ Can't accidentally break tab order (tabIndex assigned correctly)
559
+ * ✓ Can't duplicate navigation logic (single source)
560
+ * ✓ Composes via mergeProps (all standard props)
561
+ * ✓ Type-safe - invalid indices caught at call site
562
+ *
563
+ * USAGE:
564
+ * const nav = rovingFocus({
565
+ * currentIndex: 0,
566
+ * itemCount: 3,
567
+ * orientation: 'horizontal',
568
+ * onNavigate: setIndex
569
+ * });
570
+ *
571
+ * <div {...nav.container}>
572
+ * <button {...nav.item(0)}>First</button>
573
+ * <button {...nav.item(1)}>Second</button>
574
+ * </div>
575
+ *
576
+ * MISUSE EXAMPLE (PREVENTED):
577
+ * ❌ Can't forget to set tabIndex - returned in item props
578
+ * ❌ Can't create conflicting arrow handlers - mergeProps composes
579
+ * ❌ Can't skip disabled items incorrectly - logic is internal
580
+ */
581
+
582
+ type Orientation = 'horizontal' | 'vertical' | 'both';
583
+ interface RovingFocusOptions {
584
+ /**
585
+ * Current focused index
586
+ */
587
+ currentIndex: number;
588
+ /**
589
+ * Total number of items
590
+ */
591
+ itemCount: number;
592
+ /**
593
+ * Navigation orientation
594
+ * - horizontal: ArrowLeft/ArrowRight
595
+ * - vertical: ArrowUp/ArrowDown
596
+ * - both: all arrow keys
597
+ */
598
+ orientation?: Orientation;
599
+ /**
600
+ * Whether to loop when reaching the end
601
+ */
602
+ loop?: boolean;
603
+ /**
604
+ * Callback when navigation occurs
605
+ */
606
+ onNavigate?: (index: number) => void;
607
+ /**
608
+ * Optional disabled state check per index
609
+ */
610
+ isDisabled?: (index: number) => boolean;
611
+ }
612
+ interface RovingFocusResult {
613
+ /**
614
+ * Props for the container element (composes via mergeProps)
615
+ */
616
+ container: {
617
+ onKeyDown: (e: KeyboardLikeEvent) => void;
618
+ };
619
+ /**
620
+ * Generate props for an item at the given index (composes via mergeProps)
621
+ */
622
+ item: (index: number) => {
623
+ tabIndex: number;
624
+ 'data-roving-index': number;
625
+ };
626
+ }
627
+ declare function rovingFocus(options: RovingFocusOptions): RovingFocusResult;
628
+
629
+ /**
630
+ * INTERACTION POLICY (FRAMEWORK LAW)
631
+ *
632
+ * This is THE ONLY way to create interactive elements. Components MUST NOT
633
+ * implement interaction logic directly.
634
+ *
635
+ * INVARIANTS (ENFORCED):
636
+ * 1. "Press" is the semantic. Click/touch/Enter/Space are implementation details.
637
+ * 2. Disabled is enforced exactly once, here. Components may not check disabled.
638
+ * 3. Keyboard handling is automatic. Components may not add onKeyDown for activation.
639
+ * 4. Native elements opt out of polyfills, not semantics.
640
+ * 5. asChild may replace the host element, not interaction behavior.
641
+ * 6. This policy is the SINGLE SOURCE OF TRUTH for interactive behavior.
642
+ *
643
+ * DESIGN:
644
+ * - Single public function: applyInteractionPolicy
645
+ * - Returns props that compose via mergeProps
646
+ * - Delegates to pressable for mechanics
647
+ * - Enforces disabled once and only once
648
+ * - No configuration beyond disabled and native element type
649
+ *
650
+ * PIT OF SUCCESS:
651
+ * ✓ Can't bypass policy (only way to get interaction behavior)
652
+ * ✓ Can't duplicate disabled checks (enforced once, here)
653
+ * ✓ Can't write custom keyboard handlers for buttons (policy owns it)
654
+ * ✓ Composes via mergeProps (standard props)
655
+ * ✓ Wrong usage is impossible (no escape hatch)
656
+ *
657
+ * USAGE:
658
+ * function Button({ onPress, disabled }) {
659
+ * const interaction = applyInteractionPolicy({
660
+ * isNative: true,
661
+ * disabled,
662
+ * onPress
663
+ * });
664
+ *
665
+ * return <button {...interaction}>Click me</button>;
666
+ * }
667
+ *
668
+ * MISUSE EXAMPLE (PREVENTED):
669
+ * ❌ Button checking disabled again:
670
+ * function Button({ disabled, onPress }) {
671
+ * if (disabled) return; // NO! Policy handles this
672
+ * const interaction = applyInteractionPolicy(...);
673
+ * }
674
+ *
675
+ * ❌ Custom keyboard handler:
676
+ * function Button({ onPress }) {
677
+ * const interaction = applyInteractionPolicy(...);
678
+ * return <button {...interaction} onKeyDown={...}>; // NO! Policy owns this
679
+ * }
680
+ *
681
+ * ❌ Direct event handler:
682
+ * <button onClick={onPress}>; // NO! Use applyInteractionPolicy
683
+ */
684
+ interface InteractionPolicyInput {
685
+ /** Whether the host element is a native interactive element (button, a, etc) */
686
+ isNative: boolean;
687
+ /** Disabled state - checked ONLY here, never in components */
688
+ disabled: boolean;
689
+ /** User-provided press handler - semantic action, not DOM event */
690
+ onPress?: (e: Event) => void;
691
+ /** Optional ref to compose */
692
+ ref?: any;
693
+ }
694
+ /**
695
+ * THE interaction policy. Components MUST use this, NEVER implement
696
+ * interaction logic directly.
697
+ */
698
+ declare function applyInteractionPolicy({ isNative, disabled, onPress, ref, }: InteractionPolicyInput): {
699
+ disabled: true | undefined;
700
+ onClick: (e: Event) => void;
701
+ ref: any;
702
+ } | {
703
+ 'aria-disabled': true | undefined;
704
+ tabIndex: number;
705
+ ref: any;
706
+ onClick: (e: DefaultPreventable & PropagationStoppable) => void;
707
+ disabled?: true;
708
+ role?: "button";
709
+ onKeyDown?: (e: KeyboardLikeEvent) => void;
710
+ onKeyUp?: (e: KeyboardLikeEvent) => void;
711
+ };
712
+ /**
713
+ * Merge rule for Slot / asChild
714
+ *
715
+ * Precedence:
716
+ * policy → user → child
717
+ *
718
+ * Event handlers are composed (policy first).
719
+ * Refs are always composed.
720
+ * Policy props MUST take precedence to enforce invariants.
721
+ */
722
+ declare function mergeInteractionProps(childProps: Record<string, any>, policyProps: Record<string, any>, userProps?: Record<string, any>): Record<string, any>;
723
+
724
+ export { type Collection, type CollectionItem, type ComposeHandlersOptions, type ControllableState, DefaultPortal, type DismissableOptions, type FocusableOptions, type FocusableResult, type HoverableOptions, type HoverableResult, type InteractionPolicyInput, JSXElement, type Layer, type LayerManager, type LayerOptions, type Orientation, type Portal, Presence, type PresenceProps, type PressableOptions, type PressableResult, type Ref, type RovingFocusOptions, type RovingFocusResult, Slot, type SlotProps, applyInteractionPolicy, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, createCollection, createLayer, definePortal, dismissable, focusable, hoverable, isControlled, makeControllable, mergeInteractionProps, mergeProps, pressable, resolveControllable, rovingFocus, setRef, useId };