@askrjs/askr 0.0.29 → 0.0.31
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/README.md +21 -3
- package/dist/benchmark.js +2 -2
- package/dist/common/vnode.d.ts +1 -0
- package/dist/common/vnode.d.ts.map +1 -1
- package/dist/common/vnode.js +2 -1
- package/dist/common/vnode.js.map +1 -1
- package/dist/components/error-boundary.d.ts +25 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/error-boundary.js +114 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/foundations/core.d.ts +15 -0
- package/dist/foundations/core.js +13 -0
- package/dist/foundations/index.d.ts +16 -3
- package/dist/foundations/index.js +14 -1
- package/dist/foundations/interactions/dismissable.d.ts +66 -0
- package/dist/foundations/interactions/dismissable.d.ts.map +1 -0
- package/dist/foundations/interactions/dismissable.js +26 -0
- package/dist/foundations/interactions/dismissable.js.map +1 -0
- package/dist/foundations/interactions/focusable.d.ts +22 -0
- package/dist/foundations/interactions/focusable.d.ts.map +1 -0
- package/dist/foundations/interactions/focusable.js +18 -0
- package/dist/foundations/interactions/focusable.js.map +1 -0
- package/dist/foundations/interactions/hoverable.d.ts +26 -0
- package/dist/foundations/interactions/hoverable.d.ts.map +1 -0
- package/dist/foundations/interactions/hoverable.js +15 -0
- package/dist/foundations/interactions/hoverable.js.map +1 -0
- package/dist/foundations/interactions/interaction-policy.d.ts +50 -0
- package/dist/foundations/interactions/interaction-policy.d.ts.map +1 -0
- package/dist/foundations/interactions/interaction-policy.js +33 -4
- package/dist/foundations/interactions/interaction-policy.js.map +1 -1
- package/dist/foundations/interactions/pressable.d.ts +61 -0
- package/dist/foundations/interactions/pressable.d.ts.map +1 -0
- package/dist/foundations/interactions/roving-focus.d.ts +52 -0
- package/dist/foundations/interactions/roving-focus.d.ts.map +1 -0
- package/dist/foundations/interactions/roving-focus.js +71 -0
- package/dist/foundations/interactions/roving-focus.js.map +1 -0
- package/dist/foundations/state/controllable.d.ts +34 -0
- package/dist/foundations/state/controllable.d.ts.map +1 -0
- package/dist/foundations/state/controllable.js +81 -0
- package/dist/foundations/state/controllable.js.map +1 -0
- package/dist/foundations/utilities/aria.d.ts +16 -0
- package/dist/foundations/utilities/aria.d.ts.map +1 -0
- package/dist/foundations/utilities/aria.js +7 -1
- package/dist/foundations/utilities/aria.js.map +1 -1
- package/dist/foundations/utilities/compose-handlers.d.ts +37 -0
- package/dist/foundations/utilities/compose-handlers.d.ts.map +1 -0
- package/dist/foundations/utilities/compose-ref.d.ts +1 -1
- package/dist/foundations/utilities/compose-ref.js +1 -1
- package/dist/foundations/utilities/event-types.d.ts +20 -0
- package/dist/foundations/utilities/event-types.d.ts.map +1 -0
- package/dist/foundations/utilities/merge-props.d.ts +5 -0
- package/dist/foundations/utilities/merge-props.d.ts.map +1 -0
- package/dist/foundations/utilities/use-id.d.ts +32 -0
- package/dist/foundations/utilities/use-id.d.ts.map +1 -0
- package/dist/foundations/utilities/use-id.js +29 -0
- package/dist/foundations/utilities/use-id.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/renderer/dom.js +23 -1
- package/dist/renderer/dom.js.map +1 -1
- package/dist/runtime/component.d.ts +5 -0
- package/dist/runtime/component.d.ts.map +1 -1
- package/dist/runtime/component.js.map +1 -1
- package/dist/ssr/index.d.ts.map +1 -1
- package/dist/ssr/index.js +166 -0
- package/dist/ssr/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,8 +23,8 @@ createIsland({ root: document.body, component: Counter });
|
|
|
23
23
|
### Runtime
|
|
24
24
|
|
|
25
25
|
`@askrjs/askr` exports the core runtime primitives: `state()`, `derive()`,
|
|
26
|
-
`selector()`, `resource()`, routing helpers, JSX control-flow
|
|
27
|
-
entrypoints.
|
|
26
|
+
`selector()`, `resource()`, `ErrorBoundary`, routing helpers, JSX control-flow
|
|
27
|
+
helpers, and SSR/SSG entrypoints.
|
|
28
28
|
|
|
29
29
|
### Explicit reactivity
|
|
30
30
|
|
|
@@ -69,7 +69,25 @@ function Data({ id }: { id: string }) {
|
|
|
69
69
|
|
|
70
70
|
if (data.pending) return <div>Loading...</div>;
|
|
71
71
|
if (data.error) return <div>Failed to load</div>;
|
|
72
|
-
|
|
72
|
+
return <div>{data.value.name}</div>;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Developer error boundaries
|
|
77
|
+
|
|
78
|
+
`ErrorBoundary` is the opt-in boundary primitive for render-time failures. It
|
|
79
|
+
renders a visible fallback in development, still logs the underlying error, and
|
|
80
|
+
can reset when your app state changes.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { ErrorBoundary } from '@askrjs/askr';
|
|
84
|
+
|
|
85
|
+
function App() {
|
|
86
|
+
return (
|
|
87
|
+
<ErrorBoundary fallback={<div>Something went wrong</div>}>
|
|
88
|
+
<FlakyView />
|
|
89
|
+
</ErrorBoundary>
|
|
90
|
+
);
|
|
73
91
|
}
|
|
74
92
|
```
|
|
75
93
|
|
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.
|
|
14
|
-
buildLabel: "0.0.
|
|
13
|
+
packageVersion: "0.0.31",
|
|
14
|
+
buildLabel: "0.0.31-local"
|
|
15
15
|
};
|
|
16
16
|
function getBenchmarkMetadata() {
|
|
17
17
|
return { ...benchmarkMetadata };
|
package/dist/common/vnode.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Props } from "./props.js";
|
|
|
2
2
|
import { ForState } from "../runtime/for.js";
|
|
3
3
|
import { ControlBoundaryState } from "../runtime/control.js";
|
|
4
4
|
//#region src/common/vnode.d.ts
|
|
5
|
+
declare const __ERROR_BOUNDARY__: unique symbol;
|
|
5
6
|
interface DOMElement {
|
|
6
7
|
type: string | ((props: Props) => any) | symbol;
|
|
7
8
|
props?: Props;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vnode.d.ts","names":[],"sources":["../../src/common/vnode.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"vnode.d.ts","names":[],"sources":["../../src/common/vnode.ts"],"mappings":";;;;cAUa,kBAAA;AAAA,UAEI,UAAA;EAKf,IAAA,aAAiB,KAAA,EAAO,KAAA;EACxB,KAAA,GAAQ,KAAA;EACR,QAAA,GAAW,KAAA;EACX,GAAA;EAAA,CACC,MAAA,CAAO,QAAA;EACR,aAAA,GAAgB,oBAAA;EAChB,SAAA,GAAY,QAAA;AAAA;AAAA,KAIF,KAAA,GAAQ,UAAA;AAAA,cAIP,gBAAA;AAAA,iBAEG,aAAA,CAAc,IAAA,YAAgB,IAAA,IAAQ,UAAA"}
|
package/dist/common/vnode.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { __CONTROL_BOUNDARY__ } from "./control.js";
|
|
2
2
|
//#region src/common/vnode.ts
|
|
3
|
+
const __ERROR_BOUNDARY__ = Symbol.for("askr.error-boundary");
|
|
3
4
|
const __FOR_BOUNDARY__ = __CONTROL_BOUNDARY__;
|
|
4
5
|
function _isDOMElement(node) {
|
|
5
6
|
return typeof node === "object" && node !== null && "type" in node;
|
|
6
7
|
}
|
|
7
8
|
//#endregion
|
|
8
|
-
export { __FOR_BOUNDARY__, _isDOMElement };
|
|
9
|
+
export { __ERROR_BOUNDARY__, __FOR_BOUNDARY__, _isDOMElement };
|
|
9
10
|
|
|
10
11
|
//# sourceMappingURL=vnode.js.map
|
package/dist/common/vnode.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vnode.js","names":[],"sources":["../../src/common/vnode.ts"],"sourcesContent":["/**\n * Common call contracts: VNode / virtual DOM shapes\n */\n\nimport type { Props } from './props';\nimport type { ControlBoundaryState } from '../runtime/control';\nimport type { ForState } from '../runtime/for';\nexport { __CONTROL_BOUNDARY__ } from './control';\nimport { __CONTROL_BOUNDARY__ } from './control';\n\nexport interface DOMElement {\n // Element `type` can be an intrinsic tag name, a component function, or\n // a special symbol (e.g. `Fragment`). Include `symbol` in the type union\n // so runtime comparisons against `Fragment` are type-safe.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n type: string | ((props: Props) => any) | symbol;\n props?: Props;\n children?: VNode[];\n key?: string | number | null;\n [Symbol.iterator]?: never;\n _controlState?: ControlBoundaryState; // Internal: control boundary state\n _forState?: ForState<unknown>; // Deprecated internal alias during migration\n}\n\n// Type for virtual DOM nodes\nexport type VNode = DOMElement | string | number | boolean | null | undefined;\n\n// Backward-compatible internal alias while renderer/runtime migrates away from\n// the old For-only boundary naming.\nexport const __FOR_BOUNDARY__ = __CONTROL_BOUNDARY__;\n\nexport function _isDOMElement(node: unknown): node is DOMElement {\n return typeof node === 'object' && node !== null && 'type' in node;\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"vnode.js","names":[],"sources":["../../src/common/vnode.ts"],"sourcesContent":["/**\n * Common call contracts: VNode / virtual DOM shapes\n */\n\nimport type { Props } from './props';\nimport type { ControlBoundaryState } from '../runtime/control';\nimport type { ForState } from '../runtime/for';\nexport { __CONTROL_BOUNDARY__ } from './control';\nimport { __CONTROL_BOUNDARY__ } from './control';\n\nexport const __ERROR_BOUNDARY__ = Symbol.for('askr.error-boundary');\n\nexport interface DOMElement {\n // Element `type` can be an intrinsic tag name, a component function, or\n // a special symbol (e.g. `Fragment`). Include `symbol` in the type union\n // so runtime comparisons against `Fragment` are type-safe.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n type: string | ((props: Props) => any) | symbol;\n props?: Props;\n children?: VNode[];\n key?: string | number | null;\n [Symbol.iterator]?: never;\n _controlState?: ControlBoundaryState; // Internal: control boundary state\n _forState?: ForState<unknown>; // Deprecated internal alias during migration\n}\n\n// Type for virtual DOM nodes\nexport type VNode = DOMElement | string | number | boolean | null | undefined;\n\n// Backward-compatible internal alias while renderer/runtime migrates away from\n// the old For-only boundary naming.\nexport const __FOR_BOUNDARY__ = __CONTROL_BOUNDARY__;\n\nexport function _isDOMElement(node: unknown): node is DOMElement {\n return typeof node === 'object' && node !== null && 'type' in node;\n}\n"],"mappings":";;AAUA,MAAa,qBAAqB,OAAO,IAAI,sBAAsB;AAqBnE,MAAa,mBAAmB;AAEhC,SAAgB,cAAc,MAAmC;AAC/D,QAAO,OAAO,SAAS,YAAY,SAAS,QAAQ,UAAU"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Props } from "../common/props.js";
|
|
2
|
+
import { JSXElement } from "../common/jsx.js";
|
|
3
|
+
import { ComponentInstance } from "../runtime/component.js";
|
|
4
|
+
|
|
5
|
+
//#region src/components/error-boundary.d.ts
|
|
6
|
+
type ErrorBoundaryFallbackRender = (error: unknown, reset: () => void) => unknown;
|
|
7
|
+
interface ErrorBoundaryProps extends Props {
|
|
8
|
+
children?: unknown;
|
|
9
|
+
fallback?: unknown | ErrorBoundaryFallbackRender;
|
|
10
|
+
onError?: (error: unknown) => void;
|
|
11
|
+
resetKey?: unknown;
|
|
12
|
+
}
|
|
13
|
+
type ErrorBoundaryState = NonNullable<ComponentInstance['errorBoundaryState']>;
|
|
14
|
+
type ErrorBoundaryVNode = JSXElement & {
|
|
15
|
+
__instance?: ComponentInstance;
|
|
16
|
+
};
|
|
17
|
+
declare function ErrorBoundary(props: ErrorBoundaryProps): JSXElement;
|
|
18
|
+
declare function isErrorBoundaryVNode(value: unknown): value is ErrorBoundaryVNode;
|
|
19
|
+
declare function resolveErrorBoundaryState(vnode: ErrorBoundaryVNode): ErrorBoundaryState | null;
|
|
20
|
+
declare function resolveErrorBoundaryFallback(fallback: ErrorBoundaryProps['fallback'], error: unknown, reset: () => void): unknown;
|
|
21
|
+
declare function createBoundaryReset(instance: ComponentInstance): () => void;
|
|
22
|
+
declare function reportBoundaryError(instance: ComponentInstance, error: unknown, onError?: (error: unknown) => void): void;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ErrorBoundary, ErrorBoundaryFallbackRender, ErrorBoundaryProps };
|
|
25
|
+
//# sourceMappingURL=error-boundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-boundary.d.ts","names":[],"sources":["../../src/components/error-boundary.tsx"],"mappings":";;;;;KAUY,2BAAA,IACV,KAAA,WACA,KAAA;AAAA,UAGe,kBAAA,SAA2B,KAAA;EAC1C,QAAA;EACA,QAAA,aAAqB,2BAAA;EACrB,OAAA,IAAW,KAAA;EACX,QAAA;AAAA;AAAA,KAGG,kBAAA,GAAqB,WAAA,CAAY,iBAAA;AAAA,KAEjC,kBAAA,GAAqB,UAAA;EACxB,UAAA,GAAa,iBAAA;AAAA;AAAA,iBA+DC,aAAA,CAAc,KAAA,EAAO,kBAAA,GAAqB,UAAA;AAAA,iBAY1C,oBAAA,CACd,KAAA,YACC,KAAA,IAAS,kBAAA;AAAA,iBAQI,yBAAA,CACd,KAAA,EAAO,kBAAA,GACN,kBAAA;AAAA,iBAIa,4BAAA,CACd,QAAA,EAAU,kBAAA,cACV,KAAA,WACA,KAAA;AAAA,iBAoDc,mBAAA,CAAoB,QAAA,EAAU,iBAAA;AAAA,iBAc9B,mBAAA,CACd,QAAA,EAAU,iBAAA,EACV,KAAA,WACA,OAAA,IAAW,KAAA"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { __ERROR_BOUNDARY__ } from "../common/vnode.js";
|
|
2
|
+
import { isDevelopmentEnvironment } from "../common/env.js";
|
|
3
|
+
import { logger } from "../dev/logger.js";
|
|
4
|
+
import { ELEMENT_TYPE } from "../common/jsx.js";
|
|
5
|
+
import { getCurrentComponentInstance } from "../runtime/component.js";
|
|
6
|
+
//#region src/components/error-boundary.tsx
|
|
7
|
+
function getBoundaryMessage(error) {
|
|
8
|
+
if (error instanceof Error) return error.message || error.name || "Unknown error";
|
|
9
|
+
if (typeof error === "string") return error;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.stringify(error);
|
|
12
|
+
} catch {
|
|
13
|
+
return String(error);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function ensureBoundaryState(instance, resetKey) {
|
|
17
|
+
const boundaryState = instance.errorBoundaryState ?? (instance.errorBoundaryState = {
|
|
18
|
+
error: null,
|
|
19
|
+
resetKey,
|
|
20
|
+
notified: false
|
|
21
|
+
});
|
|
22
|
+
if (!Object.is(boundaryState.resetKey, resetKey)) {
|
|
23
|
+
boundaryState.error = null;
|
|
24
|
+
boundaryState.resetKey = resetKey;
|
|
25
|
+
boundaryState.notified = false;
|
|
26
|
+
}
|
|
27
|
+
return boundaryState;
|
|
28
|
+
}
|
|
29
|
+
function createBoundaryVNode(instance, props) {
|
|
30
|
+
const key = typeof props.key === "symbol" ? null : props.key ?? null;
|
|
31
|
+
return {
|
|
32
|
+
$$typeof: ELEMENT_TYPE,
|
|
33
|
+
type: __ERROR_BOUNDARY__,
|
|
34
|
+
props: {
|
|
35
|
+
children: props.children,
|
|
36
|
+
fallback: props.fallback,
|
|
37
|
+
onError: props.onError,
|
|
38
|
+
resetKey: props.resetKey
|
|
39
|
+
},
|
|
40
|
+
key,
|
|
41
|
+
__instance: instance
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function ErrorBoundary(props) {
|
|
45
|
+
const instance = getCurrentComponentInstance();
|
|
46
|
+
if (!instance) throw new Error("[Askr] ErrorBoundary() can only be used during component render execution.");
|
|
47
|
+
ensureBoundaryState(instance, props.resetKey);
|
|
48
|
+
return createBoundaryVNode(instance, props);
|
|
49
|
+
}
|
|
50
|
+
function resolveErrorBoundaryFallback(fallback, error, reset) {
|
|
51
|
+
if (typeof fallback === "function") return fallback(error, reset);
|
|
52
|
+
if (fallback !== void 0) return fallback;
|
|
53
|
+
const message = getBoundaryMessage(error);
|
|
54
|
+
const wrapper = document.createElement("div");
|
|
55
|
+
wrapper.setAttribute("role", "alert");
|
|
56
|
+
wrapper.setAttribute("data-askr-error-boundary", "true");
|
|
57
|
+
wrapper.style.boxSizing = "border-box";
|
|
58
|
+
wrapper.style.padding = "1rem";
|
|
59
|
+
wrapper.style.border = "1px solid currentColor";
|
|
60
|
+
wrapper.style.borderRadius = "0.75rem";
|
|
61
|
+
wrapper.style.display = "grid";
|
|
62
|
+
wrapper.style.gap = "0.75rem";
|
|
63
|
+
wrapper.style.maxWidth = "100%";
|
|
64
|
+
const title = document.createElement("strong");
|
|
65
|
+
title.textContent = "Something went wrong while rendering this view.";
|
|
66
|
+
const summary = document.createElement("p");
|
|
67
|
+
summary.textContent = "The app recovered into a visible fallback so the error is not hidden in the console.";
|
|
68
|
+
summary.style.margin = "0";
|
|
69
|
+
const details = document.createElement("details");
|
|
70
|
+
details.open = isDevelopmentEnvironment();
|
|
71
|
+
const detailsSummary = document.createElement("summary");
|
|
72
|
+
detailsSummary.textContent = "Error details";
|
|
73
|
+
const pre = document.createElement("pre");
|
|
74
|
+
pre.textContent = message;
|
|
75
|
+
pre.style.margin = "0";
|
|
76
|
+
pre.style.whiteSpace = "pre-wrap";
|
|
77
|
+
pre.style.wordBreak = "break-word";
|
|
78
|
+
const button = document.createElement("button");
|
|
79
|
+
button.type = "button";
|
|
80
|
+
button.textContent = "Try again";
|
|
81
|
+
button.addEventListener("click", () => reset());
|
|
82
|
+
details.append(detailsSummary, pre);
|
|
83
|
+
wrapper.append(title, summary, details, button);
|
|
84
|
+
return wrapper;
|
|
85
|
+
}
|
|
86
|
+
function createBoundaryReset(instance) {
|
|
87
|
+
return () => {
|
|
88
|
+
const boundaryState = instance.errorBoundaryState;
|
|
89
|
+
if (!boundaryState) return;
|
|
90
|
+
boundaryState.error = null;
|
|
91
|
+
boundaryState.notified = false;
|
|
92
|
+
queueMicrotask(() => {
|
|
93
|
+
instance._enqueueRun?.();
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function reportBoundaryError(instance, error, onError) {
|
|
98
|
+
const boundaryState = instance.errorBoundaryState;
|
|
99
|
+
if (boundaryState && Object.is(boundaryState.error, error) && boundaryState.notified) return;
|
|
100
|
+
if (boundaryState) {
|
|
101
|
+
boundaryState.error = error;
|
|
102
|
+
boundaryState.notified = true;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
onError?.(error);
|
|
106
|
+
} catch (hookError) {
|
|
107
|
+
logger.error("[Askr] ErrorBoundary onError handler threw:", hookError);
|
|
108
|
+
}
|
|
109
|
+
logger.error("[Askr] ErrorBoundary caught render error:", error);
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { ErrorBoundary, createBoundaryReset, reportBoundaryError, resolveErrorBoundaryFallback };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=error-boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-boundary.js","names":[],"sources":["../../src/components/error-boundary.tsx"],"sourcesContent":["import type { Props } from '../common/props';\nimport { ELEMENT_TYPE, type JSXElement } from '../common/jsx';\nimport { __ERROR_BOUNDARY__ } from '../common/vnode';\nimport {\n getCurrentComponentInstance,\n type ComponentInstance,\n} from '../runtime/component';\nimport { logger } from '../dev/logger';\nimport { isDevelopmentEnvironment } from '../common/env';\n\nexport type ErrorBoundaryFallbackRender = (\n error: unknown,\n reset: () => void\n) => unknown;\n\nexport interface ErrorBoundaryProps extends Props {\n children?: unknown;\n fallback?: unknown | ErrorBoundaryFallbackRender;\n onError?: (error: unknown) => void;\n resetKey?: unknown;\n}\n\ntype ErrorBoundaryState = NonNullable<ComponentInstance['errorBoundaryState']>;\n\ntype ErrorBoundaryVNode = JSXElement & {\n __instance?: ComponentInstance;\n};\n\nfunction getBoundaryMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message || error.name || 'Unknown error';\n }\n\n if (typeof error === 'string') {\n return error;\n }\n\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n}\n\nfunction ensureBoundaryState(\n instance: ComponentInstance,\n resetKey: unknown\n): ErrorBoundaryState {\n const boundaryState =\n instance.errorBoundaryState ??\n (instance.errorBoundaryState = {\n error: null,\n resetKey,\n notified: false,\n });\n\n if (!Object.is(boundaryState.resetKey, resetKey)) {\n boundaryState.error = null;\n boundaryState.resetKey = resetKey;\n boundaryState.notified = false;\n }\n\n return boundaryState;\n}\n\nfunction createBoundaryVNode(\n instance: ComponentInstance,\n props: ErrorBoundaryProps\n): ErrorBoundaryVNode {\n const key =\n typeof props.key === 'symbol'\n ? null\n : ((props.key ?? null) as string | number | null);\n\n return {\n $$typeof: ELEMENT_TYPE,\n type: __ERROR_BOUNDARY__,\n props: {\n children: props.children,\n fallback: props.fallback,\n onError: props.onError,\n resetKey: props.resetKey,\n },\n key,\n __instance: instance,\n };\n}\n\nexport function ErrorBoundary(props: ErrorBoundaryProps): JSXElement {\n const instance = getCurrentComponentInstance();\n if (!instance) {\n throw new Error(\n '[Askr] ErrorBoundary() can only be used during component render execution.'\n );\n }\n\n ensureBoundaryState(instance, props.resetKey);\n return createBoundaryVNode(instance, props);\n}\n\nexport function isErrorBoundaryVNode(\n value: unknown\n): value is ErrorBoundaryVNode {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as ErrorBoundaryVNode).type === __ERROR_BOUNDARY__\n );\n}\n\nexport function resolveErrorBoundaryState(\n vnode: ErrorBoundaryVNode\n): ErrorBoundaryState | null {\n return vnode.__instance?.errorBoundaryState ?? null;\n}\n\nexport function resolveErrorBoundaryFallback(\n fallback: ErrorBoundaryProps['fallback'],\n error: unknown,\n reset: () => void\n): unknown {\n if (typeof fallback === 'function') {\n return fallback(error, reset);\n }\n\n if (fallback !== undefined) {\n return fallback;\n }\n\n const message = getBoundaryMessage(error);\n const wrapper = document.createElement('div');\n wrapper.setAttribute('role', 'alert');\n wrapper.setAttribute('data-askr-error-boundary', 'true');\n wrapper.style.boxSizing = 'border-box';\n wrapper.style.padding = '1rem';\n wrapper.style.border = '1px solid currentColor';\n wrapper.style.borderRadius = '0.75rem';\n wrapper.style.display = 'grid';\n wrapper.style.gap = '0.75rem';\n wrapper.style.maxWidth = '100%';\n\n const title = document.createElement('strong');\n title.textContent = 'Something went wrong while rendering this view.';\n\n const summary = document.createElement('p');\n summary.textContent =\n 'The app recovered into a visible fallback so the error is not hidden in the console.';\n summary.style.margin = '0';\n\n const details = document.createElement('details');\n details.open = isDevelopmentEnvironment();\n\n const detailsSummary = document.createElement('summary');\n detailsSummary.textContent = 'Error details';\n\n const pre = document.createElement('pre');\n pre.textContent = message;\n pre.style.margin = '0';\n pre.style.whiteSpace = 'pre-wrap';\n pre.style.wordBreak = 'break-word';\n\n const button = document.createElement('button');\n button.type = 'button';\n button.textContent = 'Try again';\n button.addEventListener('click', () => reset());\n\n details.append(detailsSummary, pre);\n wrapper.append(title, summary, details, button);\n return wrapper;\n}\n\nexport function createBoundaryReset(instance: ComponentInstance): () => void {\n return () => {\n const boundaryState = instance.errorBoundaryState;\n if (!boundaryState) {\n return;\n }\n boundaryState.error = null;\n boundaryState.notified = false;\n queueMicrotask(() => {\n instance._enqueueRun?.();\n });\n };\n}\n\nexport function reportBoundaryError(\n instance: ComponentInstance,\n error: unknown,\n onError?: (error: unknown) => void\n): void {\n const boundaryState = instance.errorBoundaryState;\n if (\n boundaryState &&\n Object.is(boundaryState.error, error) &&\n boundaryState.notified\n ) {\n return;\n }\n\n if (boundaryState) {\n boundaryState.error = error;\n boundaryState.notified = true;\n }\n\n try {\n onError?.(error);\n } catch (hookError) {\n logger.error('[Askr] ErrorBoundary onError handler threw:', hookError);\n }\n\n logger.error('[Askr] ErrorBoundary caught render error:', error);\n}\n"],"mappings":";;;;;;AA4BA,SAAS,mBAAmB,OAAwB;AAClD,KAAI,iBAAiB,MACnB,QAAO,MAAM,WAAW,MAAM,QAAQ;AAGxC,KAAI,OAAO,UAAU,SACnB,QAAO;AAGT,KAAI;AACF,SAAO,KAAK,UAAU,MAAM;SACtB;AACN,SAAO,OAAO,MAAM;;;AAIxB,SAAS,oBACP,UACA,UACoB;CACpB,MAAM,gBACJ,SAAS,uBACR,SAAS,qBAAqB;EAC7B,OAAO;EACP;EACA,UAAU;EACX;AAEH,KAAI,CAAC,OAAO,GAAG,cAAc,UAAU,SAAS,EAAE;AAChD,gBAAc,QAAQ;AACtB,gBAAc,WAAW;AACzB,gBAAc,WAAW;;AAG3B,QAAO;;AAGT,SAAS,oBACP,UACA,OACoB;CACpB,MAAM,MACJ,OAAO,MAAM,QAAQ,WACjB,OACE,MAAM,OAAO;AAErB,QAAO;EACL,UAAU;EACV,MAAM;EACN,OAAO;GACL,UAAU,MAAM;GAChB,UAAU,MAAM;GAChB,SAAS,MAAM;GACf,UAAU,MAAM;GACjB;EACD;EACA,YAAY;EACb;;AAGH,SAAgB,cAAc,OAAuC;CACnE,MAAM,WAAW,6BAA6B;AAC9C,KAAI,CAAC,SACH,OAAM,IAAI,MACR,6EACD;AAGH,qBAAoB,UAAU,MAAM,SAAS;AAC7C,QAAO,oBAAoB,UAAU,MAAM;;AAmB7C,SAAgB,6BACd,UACA,OACA,OACS;AACT,KAAI,OAAO,aAAa,WACtB,QAAO,SAAS,OAAO,MAAM;AAG/B,KAAI,aAAa,OACf,QAAO;CAGT,MAAM,UAAU,mBAAmB,MAAM;CACzC,MAAM,UAAU,SAAS,cAAc,MAAM;AAC7C,SAAQ,aAAa,QAAQ,QAAQ;AACrC,SAAQ,aAAa,4BAA4B,OAAO;AACxD,SAAQ,MAAM,YAAY;AAC1B,SAAQ,MAAM,UAAU;AACxB,SAAQ,MAAM,SAAS;AACvB,SAAQ,MAAM,eAAe;AAC7B,SAAQ,MAAM,UAAU;AACxB,SAAQ,MAAM,MAAM;AACpB,SAAQ,MAAM,WAAW;CAEzB,MAAM,QAAQ,SAAS,cAAc,SAAS;AAC9C,OAAM,cAAc;CAEpB,MAAM,UAAU,SAAS,cAAc,IAAI;AAC3C,SAAQ,cACN;AACF,SAAQ,MAAM,SAAS;CAEvB,MAAM,UAAU,SAAS,cAAc,UAAU;AACjD,SAAQ,OAAO,0BAA0B;CAEzC,MAAM,iBAAiB,SAAS,cAAc,UAAU;AACxD,gBAAe,cAAc;CAE7B,MAAM,MAAM,SAAS,cAAc,MAAM;AACzC,KAAI,cAAc;AAClB,KAAI,MAAM,SAAS;AACnB,KAAI,MAAM,aAAa;AACvB,KAAI,MAAM,YAAY;CAEtB,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,OAAO;AACd,QAAO,cAAc;AACrB,QAAO,iBAAiB,eAAe,OAAO,CAAC;AAE/C,SAAQ,OAAO,gBAAgB,IAAI;AACnC,SAAQ,OAAO,OAAO,SAAS,SAAS,OAAO;AAC/C,QAAO;;AAGT,SAAgB,oBAAoB,UAAyC;AAC3E,cAAa;EACX,MAAM,gBAAgB,SAAS;AAC/B,MAAI,CAAC,cACH;AAEF,gBAAc,QAAQ;AACtB,gBAAc,WAAW;AACzB,uBAAqB;AACnB,YAAS,eAAe;IACxB;;;AAIN,SAAgB,oBACd,UACA,OACA,SACM;CACN,MAAM,gBAAgB,SAAS;AAC/B,KACE,iBACA,OAAO,GAAG,cAAc,OAAO,MAAM,IACrC,cAAc,SAEd;AAGF,KAAI,eAAe;AACjB,gBAAc,QAAQ;AACtB,gBAAc,WAAW;;AAG3B,KAAI;AACF,YAAU,MAAM;UACT,WAAW;AAClB,SAAO,MAAM,+CAA+C,UAAU;;AAGxE,QAAO,MAAM,6CAA6C,MAAM"}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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"}
|