@askrjs/askr 0.0.29 → 0.0.30

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.
Files changed (49) hide show
  1. package/dist/benchmark.js +2 -2
  2. package/dist/foundations/core.d.ts +15 -0
  3. package/dist/foundations/core.js +13 -0
  4. package/dist/foundations/index.d.ts +16 -3
  5. package/dist/foundations/index.js +14 -1
  6. package/dist/foundations/interactions/dismissable.d.ts +66 -0
  7. package/dist/foundations/interactions/dismissable.d.ts.map +1 -0
  8. package/dist/foundations/interactions/dismissable.js +26 -0
  9. package/dist/foundations/interactions/dismissable.js.map +1 -0
  10. package/dist/foundations/interactions/focusable.d.ts +22 -0
  11. package/dist/foundations/interactions/focusable.d.ts.map +1 -0
  12. package/dist/foundations/interactions/focusable.js +18 -0
  13. package/dist/foundations/interactions/focusable.js.map +1 -0
  14. package/dist/foundations/interactions/hoverable.d.ts +26 -0
  15. package/dist/foundations/interactions/hoverable.d.ts.map +1 -0
  16. package/dist/foundations/interactions/hoverable.js +15 -0
  17. package/dist/foundations/interactions/hoverable.js.map +1 -0
  18. package/dist/foundations/interactions/interaction-policy.d.ts +50 -0
  19. package/dist/foundations/interactions/interaction-policy.d.ts.map +1 -0
  20. package/dist/foundations/interactions/interaction-policy.js +33 -4
  21. package/dist/foundations/interactions/interaction-policy.js.map +1 -1
  22. package/dist/foundations/interactions/pressable.d.ts +61 -0
  23. package/dist/foundations/interactions/pressable.d.ts.map +1 -0
  24. package/dist/foundations/interactions/roving-focus.d.ts +52 -0
  25. package/dist/foundations/interactions/roving-focus.d.ts.map +1 -0
  26. package/dist/foundations/interactions/roving-focus.js +71 -0
  27. package/dist/foundations/interactions/roving-focus.js.map +1 -0
  28. package/dist/foundations/state/controllable.d.ts +34 -0
  29. package/dist/foundations/state/controllable.d.ts.map +1 -0
  30. package/dist/foundations/state/controllable.js +81 -0
  31. package/dist/foundations/state/controllable.js.map +1 -0
  32. package/dist/foundations/utilities/aria.d.ts +16 -0
  33. package/dist/foundations/utilities/aria.d.ts.map +1 -0
  34. package/dist/foundations/utilities/aria.js +7 -1
  35. package/dist/foundations/utilities/aria.js.map +1 -1
  36. package/dist/foundations/utilities/compose-handlers.d.ts +37 -0
  37. package/dist/foundations/utilities/compose-handlers.d.ts.map +1 -0
  38. package/dist/foundations/utilities/compose-ref.d.ts +1 -1
  39. package/dist/foundations/utilities/compose-ref.js +1 -1
  40. package/dist/foundations/utilities/event-types.d.ts +20 -0
  41. package/dist/foundations/utilities/event-types.d.ts.map +1 -0
  42. package/dist/foundations/utilities/merge-props.d.ts +5 -0
  43. package/dist/foundations/utilities/merge-props.d.ts.map +1 -0
  44. package/dist/foundations/utilities/use-id.d.ts +32 -0
  45. package/dist/foundations/utilities/use-id.d.ts.map +1 -0
  46. package/dist/foundations/utilities/use-id.js +29 -0
  47. package/dist/foundations/utilities/use-id.js.map +1 -0
  48. package/dist/index.d.ts +1 -1
  49. package/package.json +2 -2
package/dist/benchmark.js CHANGED
@@ -10,8 +10,8 @@ import { BenchmarkTable } from "./bench/components/benchmark-table.js";
10
10
  installRendererBridge();
11
11
  const benchmarkMetadata = {
12
12
  packageName: "@askrjs/askr",
13
- packageVersion: "0.0.29",
14
- buildLabel: "0.0.29-local"
13
+ packageVersion: "0.0.30",
14
+ buildLabel: "0.0.30-local"
15
15
  };
16
16
  function getBenchmarkMetadata() {
17
17
  return { ...benchmarkMetadata };
@@ -0,0 +1,15 @@
1
+ import { ComposeHandlersOptions, composeHandlers } from "./utilities/compose-handlers.js";
2
+ import { mergeProps } from "./utilities/merge-props.js";
3
+ import { ariaDisabled, ariaExpanded, ariaSelected } from "./utilities/aria.js";
4
+ import { Ref, composeRefs, setRef } from "./utilities/compose-ref.js";
5
+ import { FormatIdOptions, formatId } from "./utilities/use-id.js";
6
+ import { DefaultPreventable, FocusLikeEvent, KeyboardLikeEvent, PointerLikeEvent, PropagationStoppable } from "./utilities/event-types.js";
7
+ import { PressableOptions, PressableResult, pressable } from "./interactions/pressable.js";
8
+ import { DismissableOptions, dismissable } from "./interactions/dismissable.js";
9
+ import { FocusableOptions, FocusableResult, focusable } from "./interactions/focusable.js";
10
+ import { HoverableOptions, HoverableResult, hoverable } from "./interactions/hoverable.js";
11
+ import { Orientation, RovingFocusOptions, RovingFocusResult, rovingFocus } from "./interactions/roving-focus.js";
12
+ import { InteractionPolicyInput, applyInteractionPolicy, mergeInteractionProps } from "./interactions/interaction-policy.js";
13
+ import { ControllableState, controllableState, isControlled, makeControllable, resolveControllable } from "./state/controllable.js";
14
+ import { IconOwnProps, IconProps, IconSizeToken, IconStyleObject } from "./icon/icon.types.js";
15
+ import { IconBase, getIconContractProps, isIconSizeToken, joinIconStyle, normalizeIconSizeValue, resolveIconSizeVariable, resolveIconStrokeWidthVariable, serializeIconStyle } from "./icon/icon.js";
@@ -0,0 +1,13 @@
1
+ import "./utilities/aria.js";
2
+ import "./interactions/pressable.js";
3
+ import "./utilities/compose-handlers.js";
4
+ import "./utilities/compose-ref.js";
5
+ import "./utilities/merge-props.js";
6
+ import "./interactions/interaction-policy.js";
7
+ import "./utilities/use-id.js";
8
+ import "./interactions/dismissable.js";
9
+ import "./interactions/focusable.js";
10
+ import "./interactions/hoverable.js";
11
+ import "./interactions/roving-focus.js";
12
+ import "./state/controllable.js";
13
+ import "./icon/icon.js";
@@ -1,10 +1,23 @@
1
1
  import { JSXElement } from "../common/jsx.js";
2
+ import { ComposeHandlersOptions, composeHandlers } from "./utilities/compose-handlers.js";
3
+ import { mergeProps } from "./utilities/merge-props.js";
4
+ import { ariaDisabled, ariaExpanded, ariaSelected } from "./utilities/aria.js";
5
+ import { Ref, composeRefs, setRef } from "./utilities/compose-ref.js";
6
+ import { FormatIdOptions, formatId } from "./utilities/use-id.js";
7
+ import { DefaultPreventable, FocusLikeEvent, KeyboardLikeEvent, PointerLikeEvent, PropagationStoppable } from "./utilities/event-types.js";
8
+ import { PressableOptions, PressableResult, pressable } from "./interactions/pressable.js";
9
+ import { DismissableOptions, dismissable } from "./interactions/dismissable.js";
10
+ import { FocusableOptions, FocusableResult, focusable } from "./interactions/focusable.js";
11
+ import { HoverableOptions, HoverableResult, hoverable } from "./interactions/hoverable.js";
12
+ import { Orientation, RovingFocusOptions, RovingFocusResult, rovingFocus } from "./interactions/roving-focus.js";
13
+ import { InteractionPolicyInput, applyInteractionPolicy, mergeInteractionProps } from "./interactions/interaction-policy.js";
14
+ import { ControllableState, controllableState, isControlled, makeControllable, resolveControllable } from "./state/controllable.js";
15
+ import { IconOwnProps, IconProps, IconSizeToken, IconStyleObject } from "./icon/icon.types.js";
16
+ import { IconBase, getIconContractProps, isIconSizeToken, joinIconStyle, normalizeIconSizeValue, resolveIconSizeVariable, resolveIconStrokeWidthVariable, serializeIconStyle } from "./icon/icon.js";
2
17
  import { LayoutComponent, layout } from "./structures/layout.js";
3
18
  import { Slot, SlotProps } from "./structures/slot.js";
4
19
  import { Presence, PresenceProps } from "./structures/presence.js";
5
20
  import { DefaultPortal, Portal, PortalProps, definePortal } from "./structures/portal.js";
6
21
  import { Collection, CollectionItem, createCollection } from "./structures/collection.js";
7
22
  import { Layer, LayerManager, LayerOptions, createLayer } from "./structures/layer.js";
8
- import { IconOwnProps, IconProps, IconSizeToken, IconStyleObject } from "./icon/icon.types.js";
9
- import { IconBase, getIconContractProps, isIconSizeToken, joinIconStyle, normalizeIconSizeValue, resolveIconSizeVariable, resolveIconStrokeWidthVariable, serializeIconStyle } from "./icon/icon.js";
10
- export { Collection, CollectionItem, DefaultPortal, IconBase, type IconOwnProps, type IconProps, type IconSizeToken, type IconStyleObject, JSXElement, Layer, LayerManager, LayerOptions, LayoutComponent, Portal, PortalProps, Presence, PresenceProps, Slot, SlotProps, createCollection, createLayer, definePortal, getIconContractProps, isIconSizeToken, joinIconStyle, layout, normalizeIconSizeValue, resolveIconSizeVariable, resolveIconStrokeWidthVariable, serializeIconStyle };
23
+ export { Collection, CollectionItem, ComposeHandlersOptions, ControllableState, DefaultPortal, DefaultPreventable, DismissableOptions, FocusLikeEvent, FocusableOptions, FocusableResult, FormatIdOptions, HoverableOptions, HoverableResult, IconBase, type IconOwnProps, type IconProps, type IconSizeToken, type IconStyleObject, InteractionPolicyInput, JSXElement, KeyboardLikeEvent, Layer, LayerManager, LayerOptions, LayoutComponent, Orientation, PointerLikeEvent, Portal, PortalProps, Presence, PresenceProps, PressableOptions, PressableResult, PropagationStoppable, Ref, RovingFocusOptions, RovingFocusResult, Slot, SlotProps, applyInteractionPolicy, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, createCollection, createLayer, definePortal, dismissable, focusable, formatId, getIconContractProps, hoverable, isControlled, isIconSizeToken, joinIconStyle, layout, makeControllable, mergeInteractionProps, mergeProps, normalizeIconSizeValue, pressable, resolveControllable, resolveIconSizeVariable, resolveIconStrokeWidthVariable, rovingFocus, serializeIconStyle, setRef };
@@ -1,9 +1,22 @@
1
1
  import { DefaultPortal, Portal, definePortal } from "./structures/portal.js";
2
+ import { ariaDisabled, ariaExpanded, ariaSelected } from "./utilities/aria.js";
3
+ import { pressable } from "./interactions/pressable.js";
4
+ import { composeHandlers } from "./utilities/compose-handlers.js";
5
+ import { composeRefs, setRef } from "./utilities/compose-ref.js";
6
+ import { mergeProps } from "./utilities/merge-props.js";
7
+ import { applyInteractionPolicy, mergeInteractionProps } from "./interactions/interaction-policy.js";
2
8
  import { layout } from "./structures/layout.js";
3
9
  import { Slot } from "./structures/slot.js";
4
10
  import { Presence } from "./structures/presence.js";
5
11
  import { createCollection } from "./structures/collection.js";
6
12
  import { createLayer } from "./structures/layer.js";
7
13
  import "./structures.js";
14
+ import { formatId } from "./utilities/use-id.js";
15
+ import { dismissable } from "./interactions/dismissable.js";
16
+ import { focusable } from "./interactions/focusable.js";
17
+ import { hoverable } from "./interactions/hoverable.js";
18
+ import { rovingFocus } from "./interactions/roving-focus.js";
19
+ import { controllableState, isControlled, makeControllable, resolveControllable } from "./state/controllable.js";
8
20
  import { IconBase, getIconContractProps, isIconSizeToken, joinIconStyle, normalizeIconSizeValue, resolveIconSizeVariable, resolveIconStrokeWidthVariable, serializeIconStyle } from "./icon/icon.js";
9
- export { DefaultPortal, IconBase, Portal, Presence, Slot, createCollection, createLayer, definePortal, getIconContractProps, isIconSizeToken, joinIconStyle, layout, normalizeIconSizeValue, resolveIconSizeVariable, resolveIconStrokeWidthVariable, serializeIconStyle };
21
+ import "./core.js";
22
+ export { DefaultPortal, IconBase, Portal, Presence, Slot, applyInteractionPolicy, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, createCollection, createLayer, definePortal, dismissable, focusable, formatId, getIconContractProps, hoverable, isControlled, isIconSizeToken, joinIconStyle, layout, makeControllable, mergeInteractionProps, mergeProps, normalizeIconSizeValue, pressable, resolveControllable, resolveIconSizeVariable, resolveIconStrokeWidthVariable, rovingFocus, serializeIconStyle, setRef };
@@ -0,0 +1,66 @@
1
+ import { KeyboardLikeEvent, PointerLikeEvent } from "../utilities/event-types.js";
2
+
3
+ //#region src/foundations/interactions/dismissable.d.ts
4
+ /**
5
+ * dismissable
6
+ *
7
+ * THE dismissal primitive. Handles Escape key and outside interactions.
8
+ *
9
+ * INVARIANTS:
10
+ * 1. Returns props that compose via mergeProps (no factories)
11
+ * 2. Disabled state respected exactly once, here
12
+ * 3. No side effects - pure props generation
13
+ * 4. Outside detection requires explicit node reference
14
+ * 5. This is the ONLY dismissal primitive - do not create alternatives
15
+ *
16
+ * DESIGN:
17
+ * - Returns standard event handler props (onKeyDown, onPointerDownCapture)
18
+ * - Composable via mergeProps with other foundations
19
+ * - Caller provides node reference for outside detection
20
+ * - Single onDismiss callback for all dismiss triggers
21
+ *
22
+ * PIT OF SUCCESS:
23
+ * ✓ Can't accidentally bypass (only way to get dismiss behavior)
24
+ * ✓ Can't duplicate (disabled checked once)
25
+ * ✓ Composes via mergeProps (standard props)
26
+ * ✓ Wrong usage is hard (no factories to misuse)
27
+ *
28
+ * USAGE:
29
+ * const props = dismissable({
30
+ * node: elementRef,
31
+ * disabled: false,
32
+ * onDismiss: () => close()
33
+ * });
34
+ *
35
+ * <div ref={elementRef} {...props}>Content</div>
36
+ *
37
+ * MISUSE EXAMPLE (PREVENTED):
38
+ * ❌ Can't forget to check disabled - checked inside dismissable
39
+ * ❌ Can't create custom escape handler - this is the only one
40
+ * ❌ Can't bypass via direct event listeners - mergeProps composes correctly
41
+ */
42
+ interface DismissableOptions {
43
+ /**
44
+ * Reference to the element for outside click detection
45
+ */
46
+ node?: Node | null;
47
+ /**
48
+ * Whether dismiss is disabled
49
+ */
50
+ disabled?: boolean;
51
+ /**
52
+ * Called when dismiss is triggered (Escape or outside click)
53
+ */
54
+ onDismiss?: (trigger: 'escape' | 'outside') => void;
55
+ }
56
+ declare function dismissable({
57
+ node,
58
+ disabled,
59
+ onDismiss
60
+ }: DismissableOptions): {
61
+ onKeyDown: (e: KeyboardLikeEvent) => void;
62
+ onPointerDownCapture: (e: PointerLikeEvent) => void;
63
+ };
64
+ //#endregion
65
+ export { DismissableOptions, dismissable };
66
+ //# sourceMappingURL=dismissable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dismissable.d.ts","names":[],"sources":["../../../src/foundations/interactions/dismissable.ts"],"mappings":";;;;;;AAuCA;;;;;;;;;;;AAsBA;;;;;;;;;;;;;;;;;;;;;;;;UAtBiB,kBAAA;EAgCsC;;;EA5BrD,IAAA,GAAO,IAAA;;;;EAKP,QAAA;;;;EAKA,SAAA,IAAa,OAAA;AAAA;AAAA,iBAQC,WAAA,CAAA;EAAc,IAAA;EAAM,QAAA;EAAU;AAAA,GAAa,kBAAA;iBAC/B,iBAAA;4BASW,gBAAA;AAAA"}
@@ -0,0 +1,26 @@
1
+ //#region src/foundations/interactions/dismissable.ts
2
+ function dismissable({ node, disabled, onDismiss }) {
3
+ function handleKeyDown(e) {
4
+ if (disabled) return;
5
+ if (e.key === "Escape") {
6
+ e.preventDefault?.();
7
+ e.stopPropagation?.();
8
+ onDismiss?.("escape");
9
+ }
10
+ }
11
+ function handlePointerDownCapture(e) {
12
+ if (disabled) return;
13
+ const target = e.target;
14
+ if (!(target instanceof Node)) return;
15
+ if (!node) return;
16
+ if (!node.contains(target)) onDismiss?.("outside");
17
+ }
18
+ return {
19
+ onKeyDown: handleKeyDown,
20
+ onPointerDownCapture: handlePointerDownCapture
21
+ };
22
+ }
23
+ //#endregion
24
+ export { dismissable };
25
+
26
+ //# sourceMappingURL=dismissable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dismissable.js","names":[],"sources":["../../../src/foundations/interactions/dismissable.ts"],"sourcesContent":["/**\n * dismissable\n *\n * THE dismissal primitive. Handles Escape key and outside interactions.\n *\n * INVARIANTS:\n * 1. Returns props that compose via mergeProps (no factories)\n * 2. Disabled state respected exactly once, here\n * 3. No side effects - pure props generation\n * 4. Outside detection requires explicit node reference\n * 5. This is the ONLY dismissal primitive - do not create alternatives\n *\n * DESIGN:\n * - Returns standard event handler props (onKeyDown, onPointerDownCapture)\n * - Composable via mergeProps with other foundations\n * - Caller provides node reference for outside detection\n * - Single onDismiss callback for all dismiss triggers\n *\n * PIT OF SUCCESS:\n * ✓ Can't accidentally bypass (only way to get dismiss behavior)\n * ✓ Can't duplicate (disabled checked once)\n * ✓ Composes via mergeProps (standard props)\n * ✓ Wrong usage is hard (no factories to misuse)\n *\n * USAGE:\n * const props = dismissable({\n * node: elementRef,\n * disabled: false,\n * onDismiss: () => close()\n * });\n *\n * <div ref={elementRef} {...props}>Content</div>\n *\n * MISUSE EXAMPLE (PREVENTED):\n * ❌ Can't forget to check disabled - checked inside dismissable\n * ❌ Can't create custom escape handler - this is the only one\n * ❌ Can't bypass via direct event listeners - mergeProps composes correctly\n */\n\nexport interface DismissableOptions {\n /**\n * Reference to the element for outside click detection\n */\n node?: Node | null;\n\n /**\n * Whether dismiss is disabled\n */\n disabled?: boolean;\n\n /**\n * Called when dismiss is triggered (Escape or outside click)\n */\n onDismiss?: (trigger: 'escape' | 'outside') => void;\n}\n\nimport type {\n KeyboardLikeEvent,\n PointerLikeEvent,\n} from '../utilities/event-types';\n\nexport function dismissable({ node, disabled, onDismiss }: DismissableOptions) {\n function handleKeyDown(e: KeyboardLikeEvent) {\n if (disabled) return;\n if (e.key === 'Escape') {\n e.preventDefault?.();\n e.stopPropagation?.();\n onDismiss?.('escape');\n }\n }\n\n function handlePointerDownCapture(e: PointerLikeEvent) {\n if (disabled) return;\n\n const target = e.target;\n if (!(target instanceof Node)) return;\n\n // If no node provided, can't detect outside clicks\n if (!node) return;\n\n // Check if click is outside\n if (!node.contains(target)) {\n onDismiss?.('outside');\n }\n }\n\n return {\n onKeyDown: handleKeyDown,\n // Use capture phase to catch events before they bubble\n onPointerDownCapture: handlePointerDownCapture,\n };\n}\n"],"mappings":";AA6DA,SAAgB,YAAY,EAAE,MAAM,UAAU,aAAiC;CAC7E,SAAS,cAAc,GAAsB;AAC3C,MAAI,SAAU;AACd,MAAI,EAAE,QAAQ,UAAU;AACtB,KAAE,kBAAkB;AACpB,KAAE,mBAAmB;AACrB,eAAY,SAAS;;;CAIzB,SAAS,yBAAyB,GAAqB;AACrD,MAAI,SAAU;EAEd,MAAM,SAAS,EAAE;AACjB,MAAI,EAAE,kBAAkB,MAAO;AAG/B,MAAI,CAAC,KAAM;AAGX,MAAI,CAAC,KAAK,SAAS,OAAO,CACxB,aAAY,UAAU;;AAI1B,QAAO;EACL,WAAW;EAEX,sBAAsB;EACvB"}
@@ -0,0 +1,22 @@
1
+ //#region src/foundations/interactions/focusable.d.ts
2
+ /**
3
+ * focusable
4
+ *
5
+ * Normalize focus-related props for hosts.
6
+ * - No DOM manipulation here; returns props that the runtime may attach.
7
+ */
8
+ interface FocusableOptions {
9
+ disabled?: boolean;
10
+ tabIndex?: number | undefined;
11
+ }
12
+ interface FocusableResult {
13
+ tabIndex?: number;
14
+ 'aria-disabled'?: 'true';
15
+ }
16
+ declare function focusable({
17
+ disabled,
18
+ tabIndex
19
+ }: FocusableOptions): FocusableResult;
20
+ //#endregion
21
+ export { FocusableOptions, FocusableResult, focusable };
22
+ //# sourceMappingURL=focusable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focusable.d.ts","names":[],"sources":["../../../src/foundations/interactions/focusable.ts"],"mappings":";;AASA;;;;;UAAiB,gBAAA;EACf,QAAA;EACA,QAAA;AAAA;AAAA,UAGe,eAAA;EACf,QAAA;EACA,eAAA;AAAA;AAAA,iBAGc,SAAA,CAAA;EACd,QAAA;EACA;AAAA,GACC,gBAAA,GAAmB,eAAA"}
@@ -0,0 +1,18 @@
1
+ import { ariaDisabled } from "../utilities/aria.js";
2
+ //#region src/foundations/interactions/focusable.ts
3
+ /**
4
+ * focusable
5
+ *
6
+ * Normalize focus-related props for hosts.
7
+ * - No DOM manipulation here; returns props that the runtime may attach.
8
+ */
9
+ function focusable({ disabled, tabIndex }) {
10
+ return {
11
+ tabIndex: disabled ? -1 : tabIndex === void 0 ? 0 : tabIndex,
12
+ ...ariaDisabled(disabled)
13
+ };
14
+ }
15
+ //#endregion
16
+ export { focusable };
17
+
18
+ //# sourceMappingURL=focusable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focusable.js","names":[],"sources":["../../../src/foundations/interactions/focusable.ts"],"sourcesContent":["/**\n * focusable\n *\n * Normalize focus-related props for hosts.\n * - No DOM manipulation here; returns props that the runtime may attach.\n */\n\nimport { ariaDisabled } from '../utilities/aria';\n\nexport interface FocusableOptions {\n disabled?: boolean;\n tabIndex?: number | undefined;\n}\n\nexport interface FocusableResult {\n tabIndex?: number;\n 'aria-disabled'?: 'true';\n}\n\nexport function focusable({\n disabled,\n tabIndex,\n}: FocusableOptions): FocusableResult {\n return {\n tabIndex: disabled ? -1 : tabIndex === undefined ? 0 : tabIndex,\n ...ariaDisabled(disabled),\n };\n}\n"],"mappings":";;;;;;;;AAmBA,SAAgB,UAAU,EACxB,UACA,YACoC;AACpC,QAAO;EACL,UAAU,WAAW,KAAK,aAAa,SAAY,IAAI;EACvD,GAAG,aAAa,SAAS;EAC1B"}
@@ -0,0 +1,26 @@
1
+ import { DefaultPreventable, PropagationStoppable } from "../utilities/event-types.js";
2
+
3
+ //#region src/foundations/interactions/hoverable.d.ts
4
+ /**
5
+ * hoverable
6
+ *
7
+ * Produces props for pointer enter/leave handling. Pure and deterministic.
8
+ */
9
+ interface HoverableOptions {
10
+ disabled?: boolean;
11
+ onEnter?: (e: HoverEvent) => void;
12
+ onLeave?: (e: HoverEvent) => void;
13
+ }
14
+ type HoverEvent = DefaultPreventable & PropagationStoppable;
15
+ interface HoverableResult {
16
+ onPointerEnter?: (e: HoverEvent) => void;
17
+ onPointerLeave?: (e: HoverEvent) => void;
18
+ }
19
+ declare function hoverable({
20
+ disabled,
21
+ onEnter,
22
+ onLeave
23
+ }: HoverableOptions): HoverableResult;
24
+ //#endregion
25
+ export { HoverableOptions, HoverableResult, hoverable };
26
+ //# sourceMappingURL=hoverable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hoverable.d.ts","names":[],"sources":["../../../src/foundations/interactions/hoverable.ts"],"mappings":";;;;;;AAMA;;UAAiB,gBAAA;EACf,QAAA;EACA,OAAA,IAAW,CAAA,EAAG,UAAA;EACd,OAAA,IAAW,CAAA,EAAG,UAAA;AAAA;AAAA,KAQX,UAAA,GAAa,kBAAA,GAAqB,oBAAA;AAAA,UAEtB,eAAA;EACf,cAAA,IAAkB,CAAA,EAAG,UAAA;EACrB,cAAA,IAAkB,CAAA,EAAG,UAAA;AAAA;AAAA,iBAGP,SAAA,CAAA;EACd,QAAA;EACA,OAAA;EACA;AAAA,GACC,gBAAA,GAAmB,eAAA"}
@@ -0,0 +1,15 @@
1
+ //#region src/foundations/interactions/hoverable.ts
2
+ function hoverable({ disabled, onEnter, onLeave }) {
3
+ return {
4
+ onPointerEnter: disabled ? void 0 : (e) => {
5
+ onEnter?.(e);
6
+ },
7
+ onPointerLeave: disabled ? void 0 : (e) => {
8
+ onLeave?.(e);
9
+ }
10
+ };
11
+ }
12
+ //#endregion
13
+ export { hoverable };
14
+
15
+ //# sourceMappingURL=hoverable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hoverable.js","names":[],"sources":["../../../src/foundations/interactions/hoverable.ts"],"sourcesContent":["/**\n * hoverable\n *\n * Produces props for pointer enter/leave handling. Pure and deterministic.\n */\n\nexport interface HoverableOptions {\n disabled?: boolean;\n onEnter?: (e: HoverEvent) => void;\n onLeave?: (e: HoverEvent) => void;\n}\n\nimport type {\n DefaultPreventable,\n PropagationStoppable,\n} from '../utilities/event-types';\n\ntype HoverEvent = DefaultPreventable & PropagationStoppable;\n\nexport interface HoverableResult {\n onPointerEnter?: (e: HoverEvent) => void;\n onPointerLeave?: (e: HoverEvent) => void;\n}\n\nexport function hoverable({\n disabled,\n onEnter,\n onLeave,\n}: HoverableOptions): HoverableResult {\n return {\n onPointerEnter: disabled\n ? undefined\n : (e) => {\n onEnter?.(e);\n },\n onPointerLeave: disabled\n ? undefined\n : (e) => {\n onLeave?.(e);\n },\n };\n}\n"],"mappings":";AAwBA,SAAgB,UAAU,EACxB,UACA,SACA,WACoC;AACpC,QAAO;EACL,gBAAgB,WACZ,UACC,MAAM;AACL,aAAU,EAAE;;EAElB,gBAAgB,WACZ,UACC,MAAM;AACL,aAAU,EAAE;;EAEnB"}
@@ -0,0 +1,50 @@
1
+ import { Ref } from "../utilities/compose-ref.js";
2
+ import { DefaultPreventable, KeyboardLikeEvent, PropagationStoppable } from "../utilities/event-types.js";
3
+ //#region src/foundations/interactions/interaction-policy.d.ts
4
+ interface InteractionPolicyInput {
5
+ /** Whether the host element is a native interactive element (button, a, etc) */
6
+ isNative: boolean;
7
+ /** Disabled state - checked ONLY here, never in components */
8
+ disabled: boolean;
9
+ /** User-provided press handler - semantic action, not DOM event */
10
+ onPress?: (e: Event) => void;
11
+ /** Optional ref to compose */
12
+ ref?: Ref<unknown>;
13
+ }
14
+ /**
15
+ * THE interaction policy. Components MUST use this, NEVER implement
16
+ * interaction logic directly.
17
+ */
18
+ declare function applyInteractionPolicy({
19
+ isNative,
20
+ disabled,
21
+ onPress,
22
+ ref
23
+ }: InteractionPolicyInput): {
24
+ disabled: true | undefined;
25
+ onClick: (e: Event) => void;
26
+ ref: Ref<unknown>;
27
+ } | {
28
+ 'aria-disabled': true | undefined;
29
+ tabIndex: number;
30
+ ref: Ref<unknown>;
31
+ onClick: (e: DefaultPreventable & PropagationStoppable) => void;
32
+ disabled?: true;
33
+ role?: "button";
34
+ onKeyDown?: (e: KeyboardLikeEvent) => void;
35
+ onKeyUp?: (e: KeyboardLikeEvent) => void;
36
+ };
37
+ /**
38
+ * Merge rule for Slot / asChild
39
+ *
40
+ * Precedence:
41
+ * policy → user → child
42
+ *
43
+ * Event handlers are composed (policy first).
44
+ * Refs are always composed.
45
+ * Policy props MUST take precedence to enforce invariants.
46
+ */
47
+ declare function mergeInteractionProps(childProps: Record<string, unknown>, policyProps: Record<string, unknown>, userProps?: Record<string, unknown>): Record<string, unknown>;
48
+ //#endregion
49
+ export { InteractionPolicyInput, applyInteractionPolicy, mergeInteractionProps };
50
+ //# sourceMappingURL=interaction-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interaction-policy.d.ts","names":[],"sources":["../../../src/foundations/interactions/interaction-policy.ts"],"mappings":";;;UA6DiB,sBAAA;;EAEf,QAAA;;EAEA,QAAA;;EAEA,OAAA,IAAW,CAAA,EAAG,KAAA;;EAEd,GAAA,GAAM,GAAA;AAAA;;;AAwDR;;iBAjDgB,sBAAA,CAAA;EACd,QAAA;EACA,QAAA;EACA,OAAA;EACA;AAAA,GACC,sBAAA;;eAcgB,KAAA;;;;;;;;;;;;;;;;;;;;;;iBA8BH,qBAAA,CACd,UAAA,EAAY,MAAA,mBACZ,WAAA,EAAa,MAAA,mBACb,SAAA,GAAY,MAAA,oBAAuB,MAAA"}
@@ -1,7 +1,7 @@
1
1
  import { pressable } from "./pressable.js";
2
- import "../utilities/compose-handlers.js";
3
- import "../utilities/compose-ref.js";
4
- import "../utilities/merge-props.js";
2
+ import { composeHandlers } from "../utilities/compose-handlers.js";
3
+ import { composeRefs } from "../utilities/compose-ref.js";
4
+ import { mergeProps } from "../utilities/merge-props.js";
5
5
  //#region src/foundations/interactions/interaction-policy.ts
6
6
  /**
7
7
  * INTERACTION POLICY (FRAMEWORK LAW)
@@ -87,7 +87,36 @@ function applyInteractionPolicy({ isNative, disabled, onPress, ref }) {
87
87
  ref
88
88
  };
89
89
  }
90
+ /**
91
+ * Merge rule for Slot / asChild
92
+ *
93
+ * Precedence:
94
+ * policy → user → child
95
+ *
96
+ * Event handlers are composed (policy first).
97
+ * Refs are always composed.
98
+ * Policy props MUST take precedence to enforce invariants.
99
+ */
100
+ function mergeInteractionProps(childProps, policyProps, userProps) {
101
+ let out = mergeProps(childProps, policyProps);
102
+ if (userProps) out = mergeProps(out, userProps);
103
+ for (const k in out) {
104
+ if (!k.startsWith("on")) continue;
105
+ const policyHandler = policyProps?.[k];
106
+ const userHandler = userProps?.[k];
107
+ const childHandler = childProps?.[k];
108
+ if (policyHandler || userHandler || childHandler) {
109
+ const toHandler = (h) => typeof h === "function" ? h : void 0;
110
+ out[k] = composeHandlers(toHandler(policyHandler), composeHandlers(toHandler(userHandler), toHandler(childHandler)));
111
+ }
112
+ }
113
+ const childRef = childProps?.ref;
114
+ const userRef = userProps?.ref;
115
+ const policyRef = policyProps?.ref;
116
+ out.ref = composeRefs(childRef, userRef, policyRef);
117
+ return out;
118
+ }
90
119
  //#endregion
91
- export { applyInteractionPolicy };
120
+ export { applyInteractionPolicy, mergeInteractionProps };
92
121
 
93
122
  //# sourceMappingURL=interaction-policy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"interaction-policy.js","names":["mergePropsBase"],"sources":["../../../src/foundations/interactions/interaction-policy.ts"],"sourcesContent":["/**\n * INTERACTION POLICY (FRAMEWORK LAW)\n *\n * This is THE ONLY way to create interactive elements. Components MUST NOT\n * implement interaction logic directly.\n *\n * INVARIANTS (ENFORCED):\n * 1. \"Press\" is the semantic. Click/touch/Enter/Space are implementation details.\n * 2. Disabled is enforced exactly once, here. Components may not check disabled.\n * 3. Keyboard handling is automatic. Components may not add onKeyDown for activation.\n * 4. Native elements opt out of polyfills, not semantics.\n * 5. asChild may replace the host element, not interaction behavior.\n * 6. This policy is the SINGLE SOURCE OF TRUTH for interactive behavior.\n *\n * DESIGN:\n * - Single public function: applyInteractionPolicy\n * - Returns props that compose via mergeProps\n * - Delegates to pressable for mechanics\n * - Enforces disabled once and only once\n * - No configuration beyond disabled and native element type\n *\n * PIT OF SUCCESS:\n * ✓ Can't bypass policy (only way to get interaction behavior)\n * ✓ Can't duplicate disabled checks (enforced once, here)\n * ✓ Can't write custom keyboard handlers for buttons (policy owns it)\n * ✓ Composes via mergeProps (standard props)\n * ✓ Wrong usage is impossible (no escape hatch)\n *\n * USAGE:\n * function Button({ onPress, disabled }) {\n * const interaction = applyInteractionPolicy({\n * isNative: true,\n * disabled,\n * onPress\n * });\n *\n * return <button {...interaction}>Click me</button>;\n * }\n *\n * MISUSE EXAMPLE (PREVENTED):\n * ❌ Button checking disabled again:\n * function Button({ disabled, onPress }) {\n * if (disabled) return; // NO! Policy handles this\n * const interaction = applyInteractionPolicy(...);\n * }\n *\n * ❌ Custom keyboard handler:\n * function Button({ onPress }) {\n * const interaction = applyInteractionPolicy(...);\n * return <button {...interaction} onKeyDown={...}>; // NO! Policy owns this\n * }\n *\n * ❌ Direct event handler:\n * <button onClick={onPress}>; // NO! Use applyInteractionPolicy\n */\n\nimport { pressable } from './pressable';\nimport { composeHandlers } from '../utilities/compose-handlers';\nimport { composeRefs, Ref } from '../utilities/compose-ref';\nimport { mergeProps as mergePropsBase } from '../utilities/merge-props';\n\nexport interface InteractionPolicyInput {\n /** Whether the host element is a native interactive element (button, a, etc) */\n isNative: boolean;\n /** Disabled state - checked ONLY here, never in components */\n disabled: boolean;\n /** User-provided press handler - semantic action, not DOM event */\n onPress?: (e: Event) => void;\n /** Optional ref to compose */\n ref?: Ref<unknown>;\n}\n\n/**\n * THE interaction policy. Components MUST use this, NEVER implement\n * interaction logic directly.\n */\nexport function applyInteractionPolicy({\n isNative,\n disabled,\n onPress,\n ref,\n}: InteractionPolicyInput) {\n // Disabled is enforced exactly once, here.\n // Components downstream may NOT check disabled.\n function invokePress(e: Event) {\n if (disabled) {\n e.preventDefault?.();\n return;\n }\n onPress?.(e);\n }\n\n if (isNative) {\n return {\n disabled: disabled || undefined,\n onClick: (e: Event) => invokePress(e),\n ref,\n };\n }\n\n // Non-native elements get full button semantics\n const interaction = pressable({\n disabled,\n isNativeButton: false,\n onPress: (e) => invokePress(e as Event),\n });\n\n return {\n ...interaction,\n 'aria-disabled': disabled || undefined,\n tabIndex: disabled ? -1 : (interaction.tabIndex ?? 0),\n ref,\n };\n}\n\n/**\n * Merge rule for Slot / asChild\n *\n * Precedence:\n * policy → user → child\n *\n * Event handlers are composed (policy first).\n * Refs are always composed.\n * Policy props MUST take precedence to enforce invariants.\n */\nexport function mergeInteractionProps(\n childProps: Record<string, unknown>,\n policyProps: Record<string, unknown>,\n userProps?: Record<string, unknown>\n) {\n let out = mergePropsBase(childProps, policyProps);\n if (userProps) out = mergePropsBase(out, userProps);\n\n // Ensure policy handlers always run first\n for (const k in out) {\n if (!k.startsWith('on')) continue;\n\n const policyHandler = policyProps?.[k];\n const userHandler = userProps?.[k];\n const childHandler = childProps?.[k];\n\n if (policyHandler || userHandler || childHandler) {\n const toHandler = (h: unknown) =>\n typeof h === 'function'\n ? (h as (...args: readonly unknown[]) => void)\n : undefined;\n\n out[k] = composeHandlers(\n toHandler(policyHandler),\n composeHandlers(toHandler(userHandler), toHandler(childHandler))\n );\n }\n }\n\n const childRef = childProps?.ref as Ref<unknown> | undefined;\n const userRef = userProps?.ref as Ref<unknown> | undefined;\n const policyRef = policyProps?.ref as Ref<unknown> | undefined;\n\n out.ref = composeRefs(childRef, userRef, policyRef);\n\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAgB,uBAAuB,EACrC,UACA,UACA,SACA,OACyB;CAGzB,SAAS,YAAY,GAAU;AAC7B,MAAI,UAAU;AACZ,KAAE,kBAAkB;AACpB;;AAEF,YAAU,EAAE;;AAGd,KAAI,SACF,QAAO;EACL,UAAU,YAAY;EACtB,UAAU,MAAa,YAAY,EAAE;EACrC;EACD;CAIH,MAAM,cAAc,UAAU;EAC5B;EACA,gBAAgB;EAChB,UAAU,MAAM,YAAY,EAAW;EACxC,CAAC;AAEF,QAAO;EACL,GAAG;EACH,iBAAiB,YAAY;EAC7B,UAAU,WAAW,KAAM,YAAY,YAAY;EACnD;EACD"}
1
+ {"version":3,"file":"interaction-policy.js","names":["mergePropsBase"],"sources":["../../../src/foundations/interactions/interaction-policy.ts"],"sourcesContent":["/**\n * INTERACTION POLICY (FRAMEWORK LAW)\n *\n * This is THE ONLY way to create interactive elements. Components MUST NOT\n * implement interaction logic directly.\n *\n * INVARIANTS (ENFORCED):\n * 1. \"Press\" is the semantic. Click/touch/Enter/Space are implementation details.\n * 2. Disabled is enforced exactly once, here. Components may not check disabled.\n * 3. Keyboard handling is automatic. Components may not add onKeyDown for activation.\n * 4. Native elements opt out of polyfills, not semantics.\n * 5. asChild may replace the host element, not interaction behavior.\n * 6. This policy is the SINGLE SOURCE OF TRUTH for interactive behavior.\n *\n * DESIGN:\n * - Single public function: applyInteractionPolicy\n * - Returns props that compose via mergeProps\n * - Delegates to pressable for mechanics\n * - Enforces disabled once and only once\n * - No configuration beyond disabled and native element type\n *\n * PIT OF SUCCESS:\n * ✓ Can't bypass policy (only way to get interaction behavior)\n * ✓ Can't duplicate disabled checks (enforced once, here)\n * ✓ Can't write custom keyboard handlers for buttons (policy owns it)\n * ✓ Composes via mergeProps (standard props)\n * ✓ Wrong usage is impossible (no escape hatch)\n *\n * USAGE:\n * function Button({ onPress, disabled }) {\n * const interaction = applyInteractionPolicy({\n * isNative: true,\n * disabled,\n * onPress\n * });\n *\n * return <button {...interaction}>Click me</button>;\n * }\n *\n * MISUSE EXAMPLE (PREVENTED):\n * ❌ Button checking disabled again:\n * function Button({ disabled, onPress }) {\n * if (disabled) return; // NO! Policy handles this\n * const interaction = applyInteractionPolicy(...);\n * }\n *\n * ❌ Custom keyboard handler:\n * function Button({ onPress }) {\n * const interaction = applyInteractionPolicy(...);\n * return <button {...interaction} onKeyDown={...}>; // NO! Policy owns this\n * }\n *\n * ❌ Direct event handler:\n * <button onClick={onPress}>; // NO! Use applyInteractionPolicy\n */\n\nimport { pressable } from './pressable';\nimport { composeHandlers } from '../utilities/compose-handlers';\nimport { composeRefs, Ref } from '../utilities/compose-ref';\nimport { mergeProps as mergePropsBase } from '../utilities/merge-props';\n\nexport interface InteractionPolicyInput {\n /** Whether the host element is a native interactive element (button, a, etc) */\n isNative: boolean;\n /** Disabled state - checked ONLY here, never in components */\n disabled: boolean;\n /** User-provided press handler - semantic action, not DOM event */\n onPress?: (e: Event) => void;\n /** Optional ref to compose */\n ref?: Ref<unknown>;\n}\n\n/**\n * THE interaction policy. Components MUST use this, NEVER implement\n * interaction logic directly.\n */\nexport function applyInteractionPolicy({\n isNative,\n disabled,\n onPress,\n ref,\n}: InteractionPolicyInput) {\n // Disabled is enforced exactly once, here.\n // Components downstream may NOT check disabled.\n function invokePress(e: Event) {\n if (disabled) {\n e.preventDefault?.();\n return;\n }\n onPress?.(e);\n }\n\n if (isNative) {\n return {\n disabled: disabled || undefined,\n onClick: (e: Event) => invokePress(e),\n ref,\n };\n }\n\n // Non-native elements get full button semantics\n const interaction = pressable({\n disabled,\n isNativeButton: false,\n onPress: (e) => invokePress(e as Event),\n });\n\n return {\n ...interaction,\n 'aria-disabled': disabled || undefined,\n tabIndex: disabled ? -1 : (interaction.tabIndex ?? 0),\n ref,\n };\n}\n\n/**\n * Merge rule for Slot / asChild\n *\n * Precedence:\n * policy → user → child\n *\n * Event handlers are composed (policy first).\n * Refs are always composed.\n * Policy props MUST take precedence to enforce invariants.\n */\nexport function mergeInteractionProps(\n childProps: Record<string, unknown>,\n policyProps: Record<string, unknown>,\n userProps?: Record<string, unknown>\n) {\n let out = mergePropsBase(childProps, policyProps);\n if (userProps) out = mergePropsBase(out, userProps);\n\n // Ensure policy handlers always run first\n for (const k in out) {\n if (!k.startsWith('on')) continue;\n\n const policyHandler = policyProps?.[k];\n const userHandler = userProps?.[k];\n const childHandler = childProps?.[k];\n\n if (policyHandler || userHandler || childHandler) {\n const toHandler = (h: unknown) =>\n typeof h === 'function'\n ? (h as (...args: readonly unknown[]) => void)\n : undefined;\n\n out[k] = composeHandlers(\n toHandler(policyHandler),\n composeHandlers(toHandler(userHandler), toHandler(childHandler))\n );\n }\n }\n\n const childRef = childProps?.ref as Ref<unknown> | undefined;\n const userRef = userProps?.ref as Ref<unknown> | undefined;\n const policyRef = policyProps?.ref as Ref<unknown> | undefined;\n\n out.ref = composeRefs(childRef, userRef, policyRef);\n\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAgB,uBAAuB,EACrC,UACA,UACA,SACA,OACyB;CAGzB,SAAS,YAAY,GAAU;AAC7B,MAAI,UAAU;AACZ,KAAE,kBAAkB;AACpB;;AAEF,YAAU,EAAE;;AAGd,KAAI,SACF,QAAO;EACL,UAAU,YAAY;EACtB,UAAU,MAAa,YAAY,EAAE;EACrC;EACD;CAIH,MAAM,cAAc,UAAU;EAC5B;EACA,gBAAgB;EAChB,UAAU,MAAM,YAAY,EAAW;EACxC,CAAC;AAEF,QAAO;EACL,GAAG;EACH,iBAAiB,YAAY;EAC7B,UAAU,WAAW,KAAM,YAAY,YAAY;EACnD;EACD;;;;;;;;;;;;AAaH,SAAgB,sBACd,YACA,aACA,WACA;CACA,IAAI,MAAMA,WAAe,YAAY,YAAY;AACjD,KAAI,UAAW,OAAMA,WAAe,KAAK,UAAU;AAGnD,MAAK,MAAM,KAAK,KAAK;AACnB,MAAI,CAAC,EAAE,WAAW,KAAK,CAAE;EAEzB,MAAM,gBAAgB,cAAc;EACpC,MAAM,cAAc,YAAY;EAChC,MAAM,eAAe,aAAa;AAElC,MAAI,iBAAiB,eAAe,cAAc;GAChD,MAAM,aAAa,MACjB,OAAO,MAAM,aACR,IACD;AAEN,OAAI,KAAK,gBACP,UAAU,cAAc,EACxB,gBAAgB,UAAU,YAAY,EAAE,UAAU,aAAa,CAAC,CACjE;;;CAIL,MAAM,WAAW,YAAY;CAC7B,MAAM,UAAU,WAAW;CAC3B,MAAM,YAAY,aAAa;AAE/B,KAAI,MAAM,YAAY,UAAU,SAAS,UAAU;AAEnD,QAAO"}
@@ -0,0 +1,61 @@
1
+ import { DefaultPreventable, KeyboardLikeEvent, PropagationStoppable } from "../utilities/event-types.js";
2
+
3
+ //#region src/foundations/interactions/pressable.d.ts
4
+ /**
5
+ * pressable
6
+ *
7
+ * Interaction helper that produces VNode props for 'press' semantics.
8
+ * - Pure and deterministic: no DOM construction or mutation here
9
+ * - The runtime owns event attachment and scheduling
10
+ * - This helper returns plain props (handlers) intended to be attached by the runtime
11
+ *
12
+ * Behaviour:
13
+ * - For native buttons: only an `onClick` prop is provided (no ARIA or keyboard shims)
14
+ * - For non-button elements: add `role="button"` and `tabIndex` and keyboard handlers
15
+ * - Activation: `Enter` activates on keydown, `Space` activates on keyup (matches native button)
16
+ * - Disabled: handlers short-circuit and `aria-disabled` is set for all hosts
17
+ *
18
+ * POLICY DECISIONS (LOCKED):
19
+ *
20
+ * 1. Activation Timing (Platform Parity)
21
+ * - Enter fires on keydown (immediate response)
22
+ * - Space fires on keyup (allows cancel by moving focus, matches native)
23
+ * - Space keydown prevents scroll (matches native button behavior)
24
+ *
25
+ * 2. Disabled Enforcement Strategy
26
+ * - Native buttons: Use HTML `disabled` attribute (platform-enforced non-interactivity)
27
+ * AND `aria-disabled` (consistent a11y signaling)
28
+ * - Non-native: Use `tabIndex=-1` (removes from tab order)
29
+ * AND `aria-disabled` (signals disabled state to AT)
30
+ * - Click handler short-circuits as defense-in-depth (prevents leaked focus issues)
31
+ *
32
+ * 3. Key Repeat Behavior
33
+ * - Held Enter/Space will fire onPress repeatedly (matches native button)
34
+ * - No debouncing or repeat prevention (platform parity)
35
+ */
36
+ interface PressableOptions {
37
+ disabled?: boolean;
38
+ onPress?: (e: PressEvent) => void;
39
+ /**
40
+ * Whether the host is a native button. Defaults to false.
41
+ */
42
+ isNativeButton?: boolean;
43
+ }
44
+ type PressEvent = DefaultPreventable & PropagationStoppable;
45
+ interface PressableResult {
46
+ onClick: (e: PressEvent) => void;
47
+ disabled?: true;
48
+ role?: 'button';
49
+ tabIndex?: number;
50
+ onKeyDown?: (e: KeyboardLikeEvent) => void;
51
+ onKeyUp?: (e: KeyboardLikeEvent) => void;
52
+ 'aria-disabled'?: 'true';
53
+ }
54
+ declare function pressable({
55
+ disabled,
56
+ onPress,
57
+ isNativeButton
58
+ }: PressableOptions): PressableResult;
59
+ //#endregion
60
+ export { PressableOptions, PressableResult, pressable };
61
+ //# sourceMappingURL=pressable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pressable.d.ts","names":[],"sources":["../../../src/foundations/interactions/pressable.ts"],"mappings":";;;;;;AAiCA;;;;;;;;;;;AAakC;;;;;AAKlC;;;;;;;;;;;;;UAlBiB,gBAAA;EACf,QAAA;EACA,OAAA,IAAW,CAAA,EAAG,UAAA;EAqBD;;;EAjBb,cAAA;AAAA;AAAA,KAUG,UAAA,GAAa,kBAAA,GAAqB,oBAAA;AAAA,UAEtB,eAAA;EACf,OAAA,GAAU,CAAA,EAAG,UAAA;EACb,QAAA;EACA,IAAA;EACA,QAAA;EACA,SAAA,IAAa,CAAA,EAAG,iBAAA;EAChB,OAAA,IAAW,CAAA,EAAG,iBAAA;EACd,eAAA;AAAA;AAAA,iBAGc,SAAA,CAAA;EACd,QAAA;EACA,OAAA;EACA;AAAA,GACC,gBAAA,GAAmB,eAAA"}
@@ -0,0 +1,52 @@
1
+ import { KeyboardLikeEvent } from "../utilities/event-types.js";
2
+
3
+ //#region src/foundations/interactions/roving-focus.d.ts
4
+ type Orientation = 'horizontal' | 'vertical' | 'both';
5
+ interface RovingFocusOptions {
6
+ /**
7
+ * Current focused index
8
+ */
9
+ currentIndex: number;
10
+ /**
11
+ * Total number of items
12
+ */
13
+ itemCount: number;
14
+ /**
15
+ * Navigation orientation
16
+ * - horizontal: ArrowLeft/ArrowRight
17
+ * - vertical: ArrowUp/ArrowDown
18
+ * - both: all arrow keys
19
+ */
20
+ orientation?: Orientation;
21
+ /**
22
+ * Whether to loop when reaching the end
23
+ */
24
+ loop?: boolean;
25
+ /**
26
+ * Callback when navigation occurs
27
+ */
28
+ onNavigate?: (index: number) => void;
29
+ /**
30
+ * Optional disabled state check per index
31
+ */
32
+ isDisabled?: (index: number) => boolean;
33
+ }
34
+ interface RovingFocusResult {
35
+ /**
36
+ * Props for the container element (composes via mergeProps)
37
+ */
38
+ container: {
39
+ onKeyDown: (e: KeyboardLikeEvent) => void;
40
+ };
41
+ /**
42
+ * Generate props for an item at the given index (composes via mergeProps)
43
+ */
44
+ item: (index: number) => {
45
+ tabIndex: number;
46
+ 'data-roving-index': number;
47
+ };
48
+ }
49
+ declare function rovingFocus(options: RovingFocusOptions): RovingFocusResult;
50
+ //#endregion
51
+ export { Orientation, RovingFocusOptions, RovingFocusResult, rovingFocus };
52
+ //# sourceMappingURL=roving-focus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roving-focus.d.ts","names":[],"sources":["../../../src/foundations/interactions/roving-focus.ts"],"mappings":";;;KA8CY,WAAA;AAAA,UAEK,kBAAA;;;;EAIf,YAAA;;;;EAKA,SAAA;;;;;;;EAQA,WAAA,GAAc,WAAA;;;;EAKd,IAAA;;;;EAKA,UAAA,IAAc,KAAA;;;;EAKd,UAAA,IAAc,KAAA;AAAA;AAAA,UAGC,iBAAA;;;;EAIf,SAAA;IACE,SAAA,GAAY,CAAA,EAAG,iBAAA;EAAA;;;;EAMjB,IAAA,GAAO,KAAA;IACL,QAAA;IACA,mBAAA;EAAA;AAAA;AAAA,iBAIY,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,iBAAA"}
@@ -0,0 +1,71 @@
1
+ //#region src/foundations/interactions/roving-focus.ts
2
+ function rovingFocus(options) {
3
+ const { currentIndex, itemCount, orientation = "horizontal", loop = false, onNavigate, isDisabled } = options;
4
+ function findNextIndex(from, direction) {
5
+ let next = from + direction;
6
+ if (loop) {
7
+ if (next < 0) next = itemCount - 1;
8
+ if (next >= itemCount) next = 0;
9
+ } else if (next < 0 || next >= itemCount) return void 0;
10
+ if (isDisabled?.(next)) {
11
+ if (next === from) return void 0;
12
+ return findNextIndex(next, direction);
13
+ }
14
+ return next;
15
+ }
16
+ function handleKeyDown(e) {
17
+ const { key } = e;
18
+ let direction;
19
+ if (orientation === "horizontal" || orientation === "both") {
20
+ if (key === "ArrowRight") direction = 1;
21
+ if (key === "ArrowLeft") direction = -1;
22
+ }
23
+ if (orientation === "vertical" || orientation === "both") {
24
+ if (key === "ArrowDown") direction = 1;
25
+ if (key === "ArrowUp") direction = -1;
26
+ }
27
+ if (direction === void 0) return;
28
+ const nextIndex = findNextIndex(currentIndex, direction);
29
+ if (nextIndex === void 0) return;
30
+ e.preventDefault?.();
31
+ e.stopPropagation?.();
32
+ onNavigate?.(nextIndex);
33
+ }
34
+ return {
35
+ container: { onKeyDown: handleKeyDown },
36
+ item: (index) => ({
37
+ tabIndex: index === currentIndex ? 0 : -1,
38
+ "data-roving-index": index
39
+ })
40
+ };
41
+ }
42
+ /**
43
+ * USAGE EXAMPLE:
44
+ *
45
+ * function Menu() {
46
+ * const [focusIndex, setFocusIndex] = state(0);
47
+ * const items = ['File', 'Edit', 'View'];
48
+ *
49
+ * const navigation = rovingFocus({
50
+ * currentIndex: focusIndex(),
51
+ * itemCount: items.length,
52
+ * orientation: 'horizontal',
53
+ * loop: true,
54
+ * onNavigate: setFocusIndex,
55
+ * });
56
+ *
57
+ * return (
58
+ * <div {...navigation.container}>
59
+ * {items.map((label, index) => (
60
+ * <button {...navigation.item(index)}>
61
+ * {label}
62
+ * </button>
63
+ * ))}
64
+ * </div>
65
+ * );
66
+ * }
67
+ */
68
+ //#endregion
69
+ export { rovingFocus };
70
+
71
+ //# sourceMappingURL=roving-focus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roving-focus.js","names":[],"sources":["../../../src/foundations/interactions/roving-focus.ts"],"sourcesContent":["/**\n * rovingFocus\n *\n * Single tab stop navigation with arrow-key control.\n *\n * INVARIANTS:\n * 1. Only one item in the group is reachable via Tab (single tab stop)\n * 2. Arrow keys move focus within the group\n * 3. Orientation determines which arrow keys are active\n * 4. Looping is opt-in\n * 5. Disabled items are skipped\n * 6. Returns props objects, never factories (composes via mergeProps)\n *\n * DESIGN:\n * - Container gets onKeyDown for arrow navigation\n * - Each item gets tabIndex based on current selection\n * - Navigation logic is pure - caller controls focus application\n * - Disabled check happens per-item via predicate\n *\n * PIT OF SUCCESS:\n * ✓ Can't accidentally break tab order (tabIndex assigned correctly)\n * ✓ Can't duplicate navigation logic (single source)\n * ✓ Composes via mergeProps (all standard props)\n * ✓ Type-safe - invalid indices caught at call site\n *\n * USAGE:\n * const nav = rovingFocus({\n * currentIndex: 0,\n * itemCount: 3,\n * orientation: 'horizontal',\n * onNavigate: setIndex\n * });\n *\n * <div {...nav.container}>\n * <button {...nav.item(0)}>First</button>\n * <button {...nav.item(1)}>Second</button>\n * </div>\n *\n * MISUSE EXAMPLE (PREVENTED):\n * ❌ Can't forget to set tabIndex - returned in item props\n * ❌ Can't create conflicting arrow handlers - mergeProps composes\n * ❌ Can't skip disabled items incorrectly - logic is internal\n */\n\nimport type { KeyboardLikeEvent } from '../utilities/event-types';\n\nexport type Orientation = 'horizontal' | 'vertical' | 'both';\n\nexport interface RovingFocusOptions {\n /**\n * Current focused index\n */\n currentIndex: number;\n\n /**\n * Total number of items\n */\n itemCount: number;\n\n /**\n * Navigation orientation\n * - horizontal: ArrowLeft/ArrowRight\n * - vertical: ArrowUp/ArrowDown\n * - both: all arrow keys\n */\n orientation?: Orientation;\n\n /**\n * Whether to loop when reaching the end\n */\n loop?: boolean;\n\n /**\n * Callback when navigation occurs\n */\n onNavigate?: (index: number) => void;\n\n /**\n * Optional disabled state check per index\n */\n isDisabled?: (index: number) => boolean;\n}\n\nexport interface RovingFocusResult {\n /**\n * Props for the container element (composes via mergeProps)\n */\n container: {\n onKeyDown: (e: KeyboardLikeEvent) => void;\n };\n\n /**\n * Generate props for an item at the given index (composes via mergeProps)\n */\n item: (index: number) => {\n tabIndex: number;\n 'data-roving-index': number;\n };\n}\n\nexport function rovingFocus(options: RovingFocusOptions): RovingFocusResult {\n const {\n currentIndex,\n itemCount,\n orientation = 'horizontal',\n loop = false,\n onNavigate,\n isDisabled,\n } = options;\n\n function findNextIndex(from: number, direction: 1 | -1): number | undefined {\n let next = from + direction;\n\n // Handle looping\n if (loop) {\n if (next < 0) next = itemCount - 1;\n if (next >= itemCount) next = 0;\n } else {\n if (next < 0 || next >= itemCount) return undefined;\n }\n\n // Skip disabled items\n if (isDisabled?.(next)) {\n // Recursively find the next non-disabled item\n if (next === from) return undefined; // Prevent infinite loop\n return findNextIndex(next, direction);\n }\n\n return next;\n }\n\n function handleKeyDown(e: KeyboardLikeEvent) {\n const { key } = e;\n\n let direction: 1 | -1 | undefined;\n\n if (orientation === 'horizontal' || orientation === 'both') {\n if (key === 'ArrowRight') direction = 1;\n if (key === 'ArrowLeft') direction = -1;\n }\n\n if (orientation === 'vertical' || orientation === 'both') {\n if (key === 'ArrowDown') direction = 1;\n if (key === 'ArrowUp') direction = -1;\n }\n\n if (direction === undefined) return;\n\n const nextIndex = findNextIndex(currentIndex, direction);\n if (nextIndex === undefined) return;\n\n e.preventDefault?.();\n e.stopPropagation?.();\n\n onNavigate?.(nextIndex);\n }\n\n return {\n container: {\n onKeyDown: handleKeyDown,\n },\n item: (index: number) => ({\n tabIndex: index === currentIndex ? 0 : -1,\n 'data-roving-index': index,\n }),\n };\n}\n\n/**\n * USAGE EXAMPLE:\n *\n * function Menu() {\n * const [focusIndex, setFocusIndex] = state(0);\n * const items = ['File', 'Edit', 'View'];\n *\n * const navigation = rovingFocus({\n * currentIndex: focusIndex(),\n * itemCount: items.length,\n * orientation: 'horizontal',\n * loop: true,\n * onNavigate: setFocusIndex,\n * });\n *\n * return (\n * <div {...navigation.container}>\n * {items.map((label, index) => (\n * <button {...navigation.item(index)}>\n * {label}\n * </button>\n * ))}\n * </div>\n * );\n * }\n */\n"],"mappings":";AAoGA,SAAgB,YAAY,SAAgD;CAC1E,MAAM,EACJ,cACA,WACA,cAAc,cACd,OAAO,OACP,YACA,eACE;CAEJ,SAAS,cAAc,MAAc,WAAuC;EAC1E,IAAI,OAAO,OAAO;AAGlB,MAAI,MAAM;AACR,OAAI,OAAO,EAAG,QAAO,YAAY;AACjC,OAAI,QAAQ,UAAW,QAAO;aAE1B,OAAO,KAAK,QAAQ,UAAW,QAAO;AAI5C,MAAI,aAAa,KAAK,EAAE;AAEtB,OAAI,SAAS,KAAM,QAAO;AAC1B,UAAO,cAAc,MAAM,UAAU;;AAGvC,SAAO;;CAGT,SAAS,cAAc,GAAsB;EAC3C,MAAM,EAAE,QAAQ;EAEhB,IAAI;AAEJ,MAAI,gBAAgB,gBAAgB,gBAAgB,QAAQ;AAC1D,OAAI,QAAQ,aAAc,aAAY;AACtC,OAAI,QAAQ,YAAa,aAAY;;AAGvC,MAAI,gBAAgB,cAAc,gBAAgB,QAAQ;AACxD,OAAI,QAAQ,YAAa,aAAY;AACrC,OAAI,QAAQ,UAAW,aAAY;;AAGrC,MAAI,cAAc,OAAW;EAE7B,MAAM,YAAY,cAAc,cAAc,UAAU;AACxD,MAAI,cAAc,OAAW;AAE7B,IAAE,kBAAkB;AACpB,IAAE,mBAAmB;AAErB,eAAa,UAAU;;AAGzB,QAAO;EACL,WAAW,EACT,WAAW,eACZ;EACD,OAAO,WAAmB;GACxB,UAAU,UAAU,eAAe,IAAI;GACvC,qBAAqB;GACtB;EACF"}
@@ -0,0 +1,34 @@
1
+ import { State } from "../../runtime/state.js";
2
+
3
+ //#region src/foundations/state/controllable.d.ts
4
+ declare function isControlled<T>(value: T | undefined): value is T;
5
+ declare function resolveControllable<T>(value: T | undefined, defaultValue: T): {
6
+ value: T;
7
+ isControlled: boolean;
8
+ };
9
+ declare function makeControllable<T>(options: {
10
+ value: T | undefined;
11
+ defaultValue: T;
12
+ onChange?: (next: T) => void;
13
+ setInternal?: (next: T) => void;
14
+ }): {
15
+ set: (next: T) => void;
16
+ isControlled: boolean;
17
+ };
18
+ type ControllableState<T> = State<T> & {
19
+ isControlled: boolean;
20
+ };
21
+ /**
22
+ * controllableState
23
+ *
24
+ * Hook-like primitive that mirrors `state()` semantics while supporting
25
+ * controlled/uncontrolled behavior.
26
+ */
27
+ declare function controllableState<T>(options: {
28
+ value: T | undefined;
29
+ defaultValue: T;
30
+ onChange?: (next: T) => void;
31
+ }): ControllableState<T>;
32
+ //#endregion
33
+ export { ControllableState, controllableState, isControlled, makeControllable, resolveControllable };
34
+ //# sourceMappingURL=controllable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controllable.d.ts","names":[],"sources":["../../../src/foundations/state/controllable.ts"],"mappings":";;;iBA0BgB,YAAA,GAAA,CAAgB,KAAA,EAAO,CAAA,eAAgB,KAAA,IAAS,CAAA;AAAA,iBAIhD,mBAAA,GAAA,CACd,KAAA,EAAO,CAAA,cACP,YAAA,EAAc,CAAA;EACX,KAAA,EAAO,CAAA;EAAG,YAAA;AAAA;AAAA,iBAQC,gBAAA,GAAA,CAAoB,OAAA;EAClC,KAAA,EAAO,CAAA;EACP,YAAA,EAAc,CAAA;EACd,QAAA,IAAY,IAAA,EAAM,CAAA;EAClB,WAAA,IAAe,IAAA,EAAM,CAAA;AAAA;cAKF,CAAA;;;KAYT,iBAAA,MAAuB,KAAA,CAAM,CAAA;EAAO,YAAA;AAAA;;;;;;;iBAQhC,iBAAA,GAAA,CAAqB,OAAA;EACnC,KAAA,EAAO,CAAA;EACP,YAAA,EAAc,CAAA;EACd,QAAA,IAAY,IAAA,EAAM,CAAA;AAAA,IAChB,iBAAA,CAAkB,CAAA"}
@@ -0,0 +1,81 @@
1
+ import { state } from "../../runtime/state.js";
2
+ //#region src/foundations/state/controllable.ts
3
+ /**
4
+ * controllable
5
+ *
6
+ * Small utilities for controlled vs uncontrolled components. These helpers are
7
+ * intentionally minimal and do not manage state themselves; they help component
8
+ * implementations make correct decisions about when to call `onChange` vs
9
+ * update internal state.
10
+ *
11
+ * POLICY DECISIONS (LOCKED):
12
+ *
13
+ * 1. Controlled Detection
14
+ * A value is "controlled" if it is not `undefined`.
15
+ * This matches React conventions and is SSR-safe.
16
+ *
17
+ * 2. onChange Timing
18
+ * - Controlled mode: onChange called immediately, no internal update
19
+ * - Uncontrolled mode: internal state updated first, then onChange called
20
+ * This ensures onChange sees the new value in both modes.
21
+ *
22
+ * 3. Value Equality
23
+ * controllableState uses Object.is() to prevent unnecessary onChange calls.
24
+ * This is intentional — strict equality, no deep comparison.
25
+ */
26
+ function isControlled(value) {
27
+ return value !== void 0;
28
+ }
29
+ function resolveControllable(value, defaultValue) {
30
+ const controlled = isControlled(value);
31
+ return {
32
+ value: controlled ? value : defaultValue,
33
+ isControlled: controlled
34
+ };
35
+ }
36
+ function makeControllable(options) {
37
+ const { value, defaultValue, onChange, setInternal } = options;
38
+ const { isControlled } = resolveControllable(value, defaultValue);
39
+ function set(next) {
40
+ if (isControlled) onChange?.(next);
41
+ else {
42
+ setInternal?.(next);
43
+ onChange?.(next);
44
+ }
45
+ }
46
+ return {
47
+ set,
48
+ isControlled
49
+ };
50
+ }
51
+ /**
52
+ * controllableState
53
+ *
54
+ * Hook-like primitive that mirrors `state()` semantics while supporting
55
+ * controlled/uncontrolled behavior.
56
+ */
57
+ function controllableState(options) {
58
+ const isControlled = options.value !== void 0;
59
+ const internal = isControlled ? null : state(options.defaultValue);
60
+ function read() {
61
+ if (isControlled) return options.value;
62
+ return internal();
63
+ }
64
+ read.set = (nextOrUpdater) => {
65
+ const prev = read();
66
+ const next = typeof nextOrUpdater === "function" ? nextOrUpdater(prev) : nextOrUpdater;
67
+ if (Object.is(prev, next)) return;
68
+ if (isControlled) {
69
+ options.onChange?.(next);
70
+ return;
71
+ }
72
+ internal.set(nextOrUpdater);
73
+ options.onChange?.(next);
74
+ };
75
+ read.isControlled = isControlled;
76
+ return read;
77
+ }
78
+ //#endregion
79
+ export { controllableState, isControlled, makeControllable, resolveControllable };
80
+
81
+ //# sourceMappingURL=controllable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controllable.js","names":[],"sources":["../../../src/foundations/state/controllable.ts"],"sourcesContent":["/**\n * controllable\n *\n * Small utilities for controlled vs uncontrolled components. These helpers are\n * intentionally minimal and do not manage state themselves; they help component\n * implementations make correct decisions about when to call `onChange` vs\n * update internal state.\n *\n * POLICY DECISIONS (LOCKED):\n *\n * 1. Controlled Detection\n * A value is \"controlled\" if it is not `undefined`.\n * This matches React conventions and is SSR-safe.\n *\n * 2. onChange Timing\n * - Controlled mode: onChange called immediately, no internal update\n * - Uncontrolled mode: internal state updated first, then onChange called\n * This ensures onChange sees the new value in both modes.\n *\n * 3. Value Equality\n * controllableState uses Object.is() to prevent unnecessary onChange calls.\n * This is intentional — strict equality, no deep comparison.\n */\n\nimport { state, type State } from '../../runtime/state';\n\nexport function isControlled<T>(value: T | undefined): value is T {\n return value !== undefined;\n}\n\nexport function resolveControllable<T>(\n value: T | undefined,\n defaultValue: T\n): { value: T; isControlled: boolean } {\n const controlled = isControlled(value);\n return {\n value: controlled ? (value as T) : defaultValue,\n isControlled: controlled,\n };\n}\n\nexport function makeControllable<T>(options: {\n value: T | undefined;\n defaultValue: T;\n onChange?: (next: T) => void;\n setInternal?: (next: T) => void;\n}) {\n const { value, defaultValue, onChange, setInternal } = options;\n const { isControlled } = resolveControllable(value, defaultValue);\n\n function set(next: T) {\n if (isControlled) {\n onChange?.(next);\n } else {\n setInternal?.(next);\n onChange?.(next);\n }\n }\n\n return { set, isControlled };\n}\n\nexport type ControllableState<T> = State<T> & { isControlled: boolean };\n\n/**\n * controllableState\n *\n * Hook-like primitive that mirrors `state()` semantics while supporting\n * controlled/uncontrolled behavior.\n */\nexport function controllableState<T>(options: {\n value: T | undefined;\n defaultValue: T;\n onChange?: (next: T) => void;\n}): ControllableState<T> {\n const isControlled = options.value !== undefined;\n const internal = isControlled ? null : state<T>(options.defaultValue);\n\n function read(): T {\n if (isControlled) {\n return options.value as T;\n }\n\n return internal!();\n }\n\n read.set = (nextOrUpdater: T | ((prev: T) => T)) => {\n const prev = read();\n const next =\n typeof nextOrUpdater === 'function'\n ? (nextOrUpdater as (p: T) => T)(prev)\n : (nextOrUpdater as T);\n\n if (Object.is(prev, next)) return;\n\n if (isControlled) {\n options.onChange?.(next);\n return;\n }\n\n internal!.set(nextOrUpdater as never);\n options.onChange?.(next);\n };\n\n (read as ControllableState<T>).isControlled = isControlled;\n return read as ControllableState<T>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,aAAgB,OAAkC;AAChE,QAAO,UAAU;;AAGnB,SAAgB,oBACd,OACA,cACqC;CACrC,MAAM,aAAa,aAAa,MAAM;AACtC,QAAO;EACL,OAAO,aAAc,QAAc;EACnC,cAAc;EACf;;AAGH,SAAgB,iBAAoB,SAKjC;CACD,MAAM,EAAE,OAAO,cAAc,UAAU,gBAAgB;CACvD,MAAM,EAAE,iBAAiB,oBAAoB,OAAO,aAAa;CAEjE,SAAS,IAAI,MAAS;AACpB,MAAI,aACF,YAAW,KAAK;OACX;AACL,iBAAc,KAAK;AACnB,cAAW,KAAK;;;AAIpB,QAAO;EAAE;EAAK;EAAc;;;;;;;;AAW9B,SAAgB,kBAAqB,SAIZ;CACvB,MAAM,eAAe,QAAQ,UAAU;CACvC,MAAM,WAAW,eAAe,OAAO,MAAS,QAAQ,aAAa;CAErE,SAAS,OAAU;AACjB,MAAI,aACF,QAAO,QAAQ;AAGjB,SAAO,UAAW;;AAGpB,MAAK,OAAO,kBAAwC;EAClD,MAAM,OAAO,MAAM;EACnB,MAAM,OACJ,OAAO,kBAAkB,aACpB,cAA8B,KAAK,GACnC;AAEP,MAAI,OAAO,GAAG,MAAM,KAAK,CAAE;AAE3B,MAAI,cAAc;AAChB,WAAQ,WAAW,KAAK;AACxB;;AAGF,WAAU,IAAI,cAAuB;AACrC,UAAQ,WAAW,KAAK;;AAGzB,MAA8B,eAAe;AAC9C,QAAO"}
@@ -0,0 +1,16 @@
1
+ //#region src/foundations/utilities/aria.d.ts
2
+ /**
3
+ * Tiny aria helpers
4
+ */
5
+ declare function ariaDisabled(disabled?: boolean): {
6
+ 'aria-disabled'?: 'true';
7
+ };
8
+ declare function ariaExpanded(expanded?: boolean): {
9
+ 'aria-expanded'?: 'true' | 'false';
10
+ };
11
+ declare function ariaSelected(selected?: boolean): {
12
+ 'aria-selected'?: 'true' | 'false';
13
+ };
14
+ //#endregion
15
+ export { ariaDisabled, ariaExpanded, ariaSelected };
16
+ //# sourceMappingURL=aria.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aria.d.ts","names":[],"sources":["../../../src/foundations/utilities/aria.ts"],"mappings":";;AAIA;;iBAAgB,YAAA,CAAa,QAAA;EAAuB,eAAA;AAAA;AAAA,iBAIpC,YAAA,CAAa,QAAA;EAC3B,eAAA;AAAA;AAAA,iBAOc,YAAA,CAAa,QAAA;EAC3B,eAAA;AAAA"}
@@ -5,7 +5,13 @@
5
5
  function ariaDisabled(disabled) {
6
6
  return disabled ? { "aria-disabled": "true" } : {};
7
7
  }
8
+ function ariaExpanded(expanded) {
9
+ return expanded === void 0 ? {} : { "aria-expanded": String(expanded) };
10
+ }
11
+ function ariaSelected(selected) {
12
+ return selected === void 0 ? {} : { "aria-selected": String(selected) };
13
+ }
8
14
  //#endregion
9
- export { ariaDisabled };
15
+ export { ariaDisabled, ariaExpanded, ariaSelected };
10
16
 
11
17
  //# sourceMappingURL=aria.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"aria.js","names":[],"sources":["../../../src/foundations/utilities/aria.ts"],"sourcesContent":["/**\n * Tiny aria helpers\n */\n\nexport function ariaDisabled(disabled?: boolean): { 'aria-disabled'?: 'true' } {\n return disabled ? { 'aria-disabled': 'true' } : {};\n}\n\nexport function ariaExpanded(expanded?: boolean): {\n 'aria-expanded'?: 'true' | 'false';\n} {\n return expanded === undefined\n ? {}\n : { 'aria-expanded': String(expanded) as 'true' | 'false' };\n}\n\nexport function ariaSelected(selected?: boolean): {\n 'aria-selected'?: 'true' | 'false';\n} {\n return selected === undefined\n ? {}\n : { 'aria-selected': String(selected) as 'true' | 'false' };\n}\n"],"mappings":";;;;AAIA,SAAgB,aAAa,UAAkD;AAC7E,QAAO,WAAW,EAAE,iBAAiB,QAAQ,GAAG,EAAE"}
1
+ {"version":3,"file":"aria.js","names":[],"sources":["../../../src/foundations/utilities/aria.ts"],"sourcesContent":["/**\n * Tiny aria helpers\n */\n\nexport function ariaDisabled(disabled?: boolean): { 'aria-disabled'?: 'true' } {\n return disabled ? { 'aria-disabled': 'true' } : {};\n}\n\nexport function ariaExpanded(expanded?: boolean): {\n 'aria-expanded'?: 'true' | 'false';\n} {\n return expanded === undefined\n ? {}\n : { 'aria-expanded': String(expanded) as 'true' | 'false' };\n}\n\nexport function ariaSelected(selected?: boolean): {\n 'aria-selected'?: 'true' | 'false';\n} {\n return selected === undefined\n ? {}\n : { 'aria-selected': String(selected) as 'true' | 'false' };\n}\n"],"mappings":";;;;AAIA,SAAgB,aAAa,UAAkD;AAC7E,QAAO,WAAW,EAAE,iBAAiB,QAAQ,GAAG,EAAE;;AAGpD,SAAgB,aAAa,UAE3B;AACA,QAAO,aAAa,SAChB,EAAE,GACF,EAAE,iBAAiB,OAAO,SAAS,EAAsB;;AAG/D,SAAgB,aAAa,UAE3B;AACA,QAAO,aAAa,SAChB,EAAE,GACF,EAAE,iBAAiB,OAAO,SAAS,EAAsB"}
@@ -0,0 +1,37 @@
1
+ //#region src/foundations/utilities/compose-handlers.d.ts
2
+ /**
3
+ * composeHandlers
4
+ *
5
+ * Compose two event handlers into one. The first handler runs, and unless it
6
+ * calls `event.preventDefault()` (or sets `defaultPrevented`), the second
7
+ * handler runs. This prevents accidental clobbering of child handlers when
8
+ * injecting props.
9
+ *
10
+ * POLICY DECISIONS (LOCKED):
11
+ *
12
+ * 1. Execution Order
13
+ * First handler runs before second (injected before base).
14
+ * This allows injected handlers to prevent default behavior.
15
+ *
16
+ * 2. Default Prevention Check
17
+ * By default, checks `defaultPrevented` on first argument.
18
+ * Can be disabled via options.checkDefaultPrevented = false.
19
+ *
20
+ * 3. Undefined Handler Support
21
+ * Undefined handlers are skipped (no-op). This simplifies usage
22
+ * where handlers are optional.
23
+ *
24
+ * 4. Type Safety
25
+ * Args are readonly to prevent mutation. Return type matches input.
26
+ */
27
+ interface ComposeHandlersOptions {
28
+ /**
29
+ * When true (default), do not run the second handler if the first prevented default.
30
+ * When false, always run both handlers.
31
+ */
32
+ checkDefaultPrevented?: boolean;
33
+ }
34
+ declare function composeHandlers<A extends readonly unknown[]>(first?: (...args: A) => void, second?: (...args: A) => void, options?: ComposeHandlersOptions): (...args: A) => void;
35
+ //#endregion
36
+ export { ComposeHandlersOptions, composeHandlers };
37
+ //# sourceMappingURL=compose-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose-handlers.d.ts","names":[],"sources":["../../../src/foundations/utilities/compose-handlers.ts"],"mappings":";;AA0BA;;;;;AAmBA;;;;;;;;;;;;;;;;;;;UAnBiB,sBAAA;EAuBH;;;;EAlBZ,qBAAA;AAAA;AAAA,iBAcc,eAAA,8BAAA,CACd,KAAA,OAAY,IAAA,EAAM,CAAA,WAClB,MAAA,OAAa,IAAA,EAAM,CAAA,WACnB,OAAA,GAAU,sBAAA,OACL,IAAA,EAAM,CAAA"}
@@ -23,5 +23,5 @@ type Ref<T> = ((value: T | null) => void) | {
23
23
  declare function setRef<T>(ref: Ref<T>, value: T | null): void;
24
24
  declare function composeRefs<T>(...refs: Array<Ref<T>>): (value: T | null) => void;
25
25
  //#endregion
26
- export { Ref };
26
+ export { Ref, composeRefs, setRef };
27
27
  //# sourceMappingURL=compose-ref.d.ts.map
@@ -13,6 +13,6 @@ function composeRefs(...refs) {
13
13
  };
14
14
  }
15
15
  //#endregion
16
- export { composeRefs };
16
+ export { composeRefs, setRef };
17
17
 
18
18
  //# sourceMappingURL=compose-ref.js.map
@@ -0,0 +1,20 @@
1
+ //#region src/foundations/utilities/event-types.d.ts
2
+ interface DefaultPreventable {
3
+ defaultPrevented?: boolean;
4
+ preventDefault?: () => void;
5
+ }
6
+ interface PropagationStoppable {
7
+ stopPropagation?: () => void;
8
+ }
9
+ interface KeyboardLikeEvent extends DefaultPreventable, PropagationStoppable {
10
+ key: string;
11
+ }
12
+ interface PointerLikeEvent extends DefaultPreventable, PropagationStoppable {
13
+ target?: unknown;
14
+ }
15
+ interface FocusLikeEvent extends DefaultPreventable, PropagationStoppable {
16
+ relatedTarget?: unknown;
17
+ }
18
+ //#endregion
19
+ export { DefaultPreventable, FocusLikeEvent, KeyboardLikeEvent, PointerLikeEvent, PropagationStoppable };
20
+ //# sourceMappingURL=event-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-types.d.ts","names":[],"sources":["../../../src/foundations/utilities/event-types.ts"],"mappings":";UAAiB,kBAAA;EACf,gBAAA;EACA,cAAA;AAAA;AAAA,UAGe,oBAAA;EACf,eAAA;AAAA;AAAA,UAGe,iBAAA,SACP,kBAAA,EAAoB,oBAAA;EAC5B,GAAA;AAAA;AAAA,UAGe,gBAAA,SACP,kBAAA,EAAoB,oBAAA;EAC5B,MAAA;AAAA;AAAA,UAGe,cAAA,SACP,kBAAA,EAAoB,oBAAA;EAC5B,aAAA;AAAA"}
@@ -0,0 +1,5 @@
1
+ //#region src/foundations/utilities/merge-props.d.ts
2
+ declare function mergeProps<TBase extends object, TInjected extends object>(base: TBase, injected: TInjected): TInjected & TBase;
3
+ //#endregion
4
+ export { mergeProps };
5
+ //# sourceMappingURL=merge-props.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-props.d.ts","names":[],"sources":["../../../src/foundations/utilities/merge-props.ts"],"mappings":";iBAiCgB,UAAA,gDAAA,CACd,IAAA,EAAM,KAAA,EACN,QAAA,EAAU,SAAA,GACT,SAAA,GAAY,KAAA"}
@@ -0,0 +1,32 @@
1
+ //#region src/foundations/utilities/use-id.d.ts
2
+ interface FormatIdOptions {
3
+ /** Defaults to 'askr' */
4
+ prefix?: string;
5
+ /** Stable, caller-provided identity */
6
+ id: string | number;
7
+ }
8
+ /**
9
+ * formatId
10
+ *
11
+ * Formats a stable ID from a caller-provided identity.
12
+ * - Pure and deterministic (no time/randomness/global counters)
13
+ * - SSR-safe
14
+ *
15
+ * POLICY DECISIONS (LOCKED):
16
+ *
17
+ * 1. No Auto-Generation
18
+ * Caller must provide the `id`. No random/sequential generation.
19
+ * This ensures determinism and SSR safety.
20
+ *
21
+ * 2. Format Convention
22
+ * IDs are formatted as `{prefix}-{id}`.
23
+ * Default prefix is "askr".
24
+ *
25
+ * 3. Type Coercion
26
+ * Numbers are coerced to strings via String().
27
+ * This is deterministic and consistent.
28
+ */
29
+ declare function formatId(options: FormatIdOptions): string;
30
+ //#endregion
31
+ export { FormatIdOptions, formatId };
32
+ //# sourceMappingURL=use-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-id.d.ts","names":[],"sources":["../../../src/foundations/utilities/use-id.ts"],"mappings":";UAAiB,eAAA;EAAA;EAEf,MAAA;;EAEA,EAAA;AAAA;AAwBF;;;;;;;;;;;;;;;;;;;;;AAAA,iBAAgB,QAAA,CAAS,OAAA,EAAS,eAAA"}
@@ -0,0 +1,29 @@
1
+ //#region src/foundations/utilities/use-id.ts
2
+ /**
3
+ * formatId
4
+ *
5
+ * Formats a stable ID from a caller-provided identity.
6
+ * - Pure and deterministic (no time/randomness/global counters)
7
+ * - SSR-safe
8
+ *
9
+ * POLICY DECISIONS (LOCKED):
10
+ *
11
+ * 1. No Auto-Generation
12
+ * Caller must provide the `id`. No random/sequential generation.
13
+ * This ensures determinism and SSR safety.
14
+ *
15
+ * 2. Format Convention
16
+ * IDs are formatted as `{prefix}-{id}`.
17
+ * Default prefix is "askr".
18
+ *
19
+ * 3. Type Coercion
20
+ * Numbers are coerced to strings via String().
21
+ * This is deterministic and consistent.
22
+ */
23
+ function formatId(options) {
24
+ return `${options.prefix ?? "askr"}-${String(options.id)}`;
25
+ }
26
+ //#endregion
27
+ export { formatId };
28
+
29
+ //# sourceMappingURL=use-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-id.js","names":[],"sources":["../../../src/foundations/utilities/use-id.ts"],"sourcesContent":["export interface FormatIdOptions {\n /** Defaults to 'askr' */\n prefix?: string;\n /** Stable, caller-provided identity */\n id: string | number;\n}\n\n/**\n * formatId\n *\n * Formats a stable ID from a caller-provided identity.\n * - Pure and deterministic (no time/randomness/global counters)\n * - SSR-safe\n *\n * POLICY DECISIONS (LOCKED):\n *\n * 1. No Auto-Generation\n * Caller must provide the `id`. No random/sequential generation.\n * This ensures determinism and SSR safety.\n *\n * 2. Format Convention\n * IDs are formatted as `{prefix}-{id}`.\n * Default prefix is \"askr\".\n *\n * 3. Type Coercion\n * Numbers are coerced to strings via String().\n * This is deterministic and consistent.\n */\nexport function formatId(options: FormatIdOptions): string {\n const prefix = options.prefix ?? 'askr';\n return `${prefix}-${String(options.id)}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,SAAS,SAAkC;AAEzD,QAAO,GADQ,QAAQ,UAAU,OAChB,GAAG,OAAO,QAAQ,GAAG"}
package/dist/index.d.ts CHANGED
@@ -9,9 +9,9 @@ import { HistoryScrollBehavior, NavigateOptions, NavigationScrollBehavior, Scrol
9
9
  import { HydrateSPAConfig, IslandConfig, IslandsConfig, SPAConfig, cleanupApp, createIsland, createIslands, createSPA, hasApp, hydrateSPA } from "./boot/index.js";
10
10
  import { Show, ShowProps } from "./control/show.js";
11
11
  import { Case, CaseProps, Match, MatchProps } from "./control/case.js";
12
+ import { jsx, jsxs } from "./jsx-runtime.js";
12
13
  import { Slot, SlotProps } from "./foundations/structures/slot.js";
13
14
  import { DefaultPortal, Portal, PortalProps, definePortal } from "./foundations/structures/portal.js";
14
- import { jsx, jsxs } from "./jsx-runtime.js";
15
15
  import { Derived, derive } from "./runtime/derive.js";
16
16
  import { Selector, selector } from "./runtime/selector.js";
17
17
  import { ResourceResult, resource } from "./runtime/operations.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/askr",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "Actor-backed deterministic UI framework",
5
5
  "keywords": [
6
6
  "askr",
@@ -100,7 +100,7 @@
100
100
  "@axe-core/playwright": "^4.11.2",
101
101
  "@playwright/test": "^1.59.1",
102
102
  "@types/node": "^25.6.0",
103
- "jsdom": "^29.1.0",
103
+ "jsdom": "^29.1.1",
104
104
  "playwright": "^1.59.1",
105
105
  "tsd": "^0.33.0",
106
106
  "typescript": "^6.0.3",