@cascivo/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 urbanisierung
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ <!-- generated by scripts/readme/generate.ts — edit readme.body.md, not this file -->
2
+
3
+ # @cascivo/core
4
+
5
+ > Micro-FSM and Preact Signals primitives powering every cascade component
6
+
7
+ [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/urbanisierung/cascivo)
8
+
9
+ Core runtime for cascade — micro-FSM engine, Preact Signals integration (`useSignal`, `useComputed`, `useSignalEffect`, `useSignals`), and base utilities shared across all components.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pnpm add @cascivo/core
15
+ ```
16
+
17
+ ---
18
+
19
+ [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/urbanisierung/cascivo) · AI agents: use [`@cascivo/mcp`](https://github.com/urbanisierung/cascivo/tree/main/packages/mcp) and [`registry.json`](https://github.com/urbanisierung/cascivo/blob/main/registry.json) · MIT
@@ -0,0 +1,372 @@
1
+ import { ReadonlySignal, Signal, batch, computed, effect, signal, useComputed, useSignal, useSignalEffect } from "@preact/signals-react";
2
+ import { useSignals } from "@preact/signals-react/runtime";
3
+ import { CSSProperties, Component, ErrorInfo, HTMLAttributes, ReactElement, ReactNode, Ref, RefCallback, RefObject } from "react";
4
+
5
+ //#region src/types.d.ts
6
+ interface StateConfig {
7
+ on?: Record<string, string>;
8
+ }
9
+ interface MachineConfig<S extends string = string, I extends S = S> {
10
+ initial: I;
11
+ states: Record<S, StateConfig>;
12
+ }
13
+ interface Machine<S extends string = string> {
14
+ initial: S;
15
+ states: Record<S, StateConfig>;
16
+ }
17
+ interface PropMeta {
18
+ name: string;
19
+ type: string;
20
+ required: boolean;
21
+ default?: string;
22
+ description?: string;
23
+ }
24
+ /**
25
+ * WCAG conformance level with explicit version.
26
+ * 'AA' / 'AAA' are deprecated aliases for '2.1-AA' / '2.1-AAA' — migrate to versioned form.
27
+ */
28
+ type WcagLevel = '2.1-AA' | '2.2-AA' | '2.2-AAA' | 'AA' | 'AAA';
29
+ interface AccessibilityMeta {
30
+ role: string;
31
+ /** WCAG conformance level. Use versioned form: '2.1-AA' | '2.2-AA'. */
32
+ wcag: WcagLevel;
33
+ keyboard: string[];
34
+ /**
35
+ * ARIA Authoring Practices Guide pattern this component conforms to.
36
+ * E.g. 'tabs', 'dialog-modal', 'combobox', 'accordion', 'slider'.
37
+ * Omit for components that don't map to an APG pattern (charts, composites).
38
+ */
39
+ apgPattern?: string;
40
+ /**
41
+ * True if the component explicitly handles forced-colors (Windows High Contrast)
42
+ * via @media (forced-colors: active) CSS rules.
43
+ */
44
+ forcedColors?: boolean;
45
+ /**
46
+ * True if the component explicitly respects prefers-reduced-motion.
47
+ * Omit/false for components with no animation.
48
+ */
49
+ reducedMotion?: boolean;
50
+ }
51
+ interface ExampleMeta {
52
+ title: string;
53
+ code: string;
54
+ description?: string;
55
+ }
56
+ interface ComponentMeta {
57
+ name: string;
58
+ description: string;
59
+ category: 'inputs' | 'display' | 'overlay' | 'navigation' | 'feedback' | 'layout' | 'block' | 'chart';
60
+ states: string[];
61
+ variants: string[];
62
+ sizes: string[];
63
+ props: PropMeta[];
64
+ tokens: string[];
65
+ accessibility: AccessibilityMeta;
66
+ examples: ExampleMeta[];
67
+ dependencies: string[];
68
+ tags: string[];
69
+ intent?: ComponentIntent;
70
+ }
71
+ interface IntentAntiPattern {
72
+ bad: string;
73
+ good?: string;
74
+ why: string;
75
+ }
76
+ type IntentRelationship = 'alternative' | 'pairs-with' | 'contains' | 'contained-by';
77
+ interface IntentRelated {
78
+ name: string;
79
+ relationship: IntentRelationship;
80
+ reason: string;
81
+ }
82
+ interface IntentFlexibility {
83
+ area: string;
84
+ level: 'strict' | 'flexible';
85
+ note: string;
86
+ }
87
+ interface IntentContent {
88
+ tone: string;
89
+ notes?: string;
90
+ }
91
+ interface ComponentIntent {
92
+ whenToUse: string[];
93
+ whenNotToUse: string[];
94
+ antiPatterns: IntentAntiPattern[];
95
+ related: IntentRelated[];
96
+ a11yRationale: string;
97
+ content?: IntentContent;
98
+ flexibility: IntentFlexibility[];
99
+ }
100
+ //#endregion
101
+ //#region src/machine.d.ts
102
+ declare function createMachine<S extends string, I extends S = S>(config: MachineConfig<S, I>): Machine<S>;
103
+ declare function useMachine<S extends string>(machine: Machine<S>): readonly [import("@preact/signals-core").Signal<S>, (event: string) => void];
104
+ //#endregion
105
+ //#region src/utils.d.ts
106
+ declare function cn(...classes: (string | undefined | null | false | 0)[]): string;
107
+ declare function composeRefs<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T>;
108
+ declare function mergeProps<T extends Record<string, unknown>>(...propsList: Partial<T>[]): T;
109
+ //#endregion
110
+ //#region src/error-boundary.d.ts
111
+ interface ErrorBoundaryProps {
112
+ children: ReactNode;
113
+ fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
114
+ onError?: (error: Error, info: ErrorInfo) => void;
115
+ }
116
+ interface State {
117
+ error: Error | null;
118
+ }
119
+ declare class ErrorBoundary extends Component<ErrorBoundaryProps, State> {
120
+ state: State;
121
+ static getDerivedStateFromError(error: Error): State;
122
+ componentDidCatch(error: Error, info: ErrorInfo): void;
123
+ reset: () => void;
124
+ render(): ReactNode;
125
+ }
126
+ //#endregion
127
+ //#region src/suspense-boundary.d.ts
128
+ interface SuspenseBoundaryProps {
129
+ children: ReactNode;
130
+ fallback?: ReactNode;
131
+ }
132
+ declare function SuspenseBoundary({
133
+ children,
134
+ fallback
135
+ }: SuspenseBoundaryProps): import("react").JSX.Element;
136
+ //#endregion
137
+ //#region src/portal.d.ts
138
+ interface PortalProps {
139
+ children: ReactNode;
140
+ container?: Element | DocumentFragment;
141
+ }
142
+ declare function Portal({
143
+ children,
144
+ container
145
+ }: PortalProps): import("react").ReactPortal | null;
146
+ //#endregion
147
+ //#region src/visually-hidden.d.ts
148
+ interface VisuallyHiddenProps extends HTMLAttributes<HTMLSpanElement> {
149
+ children: ReactNode;
150
+ showOnFocus?: boolean;
151
+ }
152
+ declare function VisuallyHidden({
153
+ children,
154
+ showOnFocus,
155
+ className,
156
+ ...props
157
+ }: VisuallyHiddenProps): import("react").JSX.Element;
158
+ //#endregion
159
+ //#region src/focus-scope.d.ts
160
+ interface FocusScopeProps {
161
+ children: ReactNode;
162
+ trapped?: boolean;
163
+ restoreFocus?: boolean;
164
+ autoFocus?: boolean;
165
+ }
166
+ declare function FocusScope({
167
+ children,
168
+ trapped,
169
+ restoreFocus,
170
+ autoFocus
171
+ }: FocusScopeProps): import("react").JSX.Element;
172
+ //#endregion
173
+ //#region src/slot.d.ts
174
+ interface SlotProps {
175
+ children?: ReactNode;
176
+ className?: string;
177
+ style?: CSSProperties;
178
+ [key: string]: unknown;
179
+ }
180
+ /**
181
+ * Slot renders its single child element while merging the Slot's own props and ref onto it —
182
+ * the Radix-style `asChild` composition primitive, built on cascivo's existing
183
+ * `mergeProps`/`composeRefs`/`cn`.
184
+ *
185
+ * - Non-handler props: child wins (so the consumer's element controls its own identity).
186
+ * - `on*` handlers: chained (Slot's runs, then the child's) via `mergeProps`.
187
+ * - `className`: unioned via `cn`. `style`: shallow-merged (child wins on conflicts).
188
+ * - `ref`: composed so both the Slot consumer's ref and the child's own ref receive the node.
189
+ *
190
+ * Fails fast if `children` is not exactly one valid React element.
191
+ */
192
+ declare const Slot: import("react").ForwardRefExoticComponent<Omit<SlotProps, "ref"> & import("react").RefAttributes<unknown>>;
193
+ //#endregion
194
+ //#region src/controllable.d.ts
195
+ interface UseControllableSignalOptions<T> {
196
+ /** Controlled value. When provided, the component is controlled for its whole life. */
197
+ value?: T | undefined;
198
+ /** Initial value for uncontrolled use. */
199
+ defaultValue?: T | undefined;
200
+ /** Called on every write, in both controlled and uncontrolled modes. */
201
+ onChange?: ((value: T) => void) | undefined;
202
+ }
203
+ /**
204
+ * Codifies the controlled/uncontrolled pattern documented by hand in CLAUDE.md
205
+ * (`const x = useSignal(open); x.value = open`).
206
+ *
207
+ * Returns a `[signal, setValue]` pair:
208
+ * - **Controlled** (`value` provided): the signal mirrors `value` on every render; `setValue`
209
+ * does not mutate the signal locally — it only routes the request through `onChange` so the
210
+ * parent owns the state (React semantics).
211
+ * - **Uncontrolled** (`value` undefined): the signal owns the state seeded from `defaultValue`;
212
+ * `setValue` updates it locally and also calls `onChange`.
213
+ *
214
+ * Whether a component is controlled is fixed for its life, exactly like React.
215
+ */
216
+ declare function useControllableSignal<T>(options: UseControllableSignalOptions<T>): [Signal<T>, (next: T) => void];
217
+ //#endregion
218
+ //#region src/media-query.d.ts
219
+ /**
220
+ * `useMediaQuery(query)` → a `ReadonlySignal<boolean>` that tracks whether the media query matches.
221
+ * SSR-safe (defaults to `false` when `window`/`matchMedia` is unavailable). Subscribes to changes
222
+ * inside `useSignalEffect`; the listener is cleaned up on teardown.
223
+ */
224
+ declare function useMediaQuery(query: string): ReadonlySignal<boolean>;
225
+ //#endregion
226
+ //#region src/scroll-lock.d.ts
227
+ /**
228
+ * Locks `document.body` scroll while `locked` is true, compensating for the scrollbar width to
229
+ * avoid a layout shift. Accepts a plain boolean or a `Signal<boolean>` (reactive). The previous
230
+ * inline styles are restored on unlock/teardown. DOM work runs inside `useSignalEffect`.
231
+ */
232
+ declare function useScrollLock(locked: Signal<boolean> | boolean): void;
233
+ //#endregion
234
+ //#region src/use-id.d.ts
235
+ /**
236
+ * SSR-safe stable id for aria wiring (Label/Field). Wraps React's `useId` and strips the
237
+ * non-selector-safe colons React emits, prefixing for readability (default `cascivo`).
238
+ */
239
+ declare function useId(prefix?: string): string;
240
+ //#endregion
241
+ //#region src/clipboard.d.ts
242
+ interface UseClipboardOptions {
243
+ /** How long `copied` stays true after a successful copy, in ms. */
244
+ timeout?: number;
245
+ }
246
+ interface UseClipboardReturn {
247
+ copied: ReadonlySignal<boolean>;
248
+ copy: (text: string) => Promise<void>;
249
+ }
250
+ /**
251
+ * Copy-to-clipboard helper that de-dupes the inline logic in `copy-button`.
252
+ * `copy(text)` writes via `navigator.clipboard.writeText`; `copied` flips true then resets after
253
+ * `timeout`. The reset timer is cleared on unmount via `useSignalEffect` (no `useEffect`).
254
+ */
255
+ declare function useClipboard(options?: UseClipboardOptions): UseClipboardReturn;
256
+ //#endregion
257
+ //#region src/dismissable-layer.d.ts
258
+ interface DismissableLayerProps {
259
+ children: ReactNode;
260
+ /** Called when an outside pointer lands or Escape is pressed (when this is the top-most layer). */
261
+ onDismiss?: () => void;
262
+ /** Disable the outside-pointer dismissal (Escape still works unless escapeDismisses is false). */
263
+ disableOutsidePointer?: boolean;
264
+ /** Whether Escape dismisses the top-most layer. Default true. */
265
+ escapeDismisses?: boolean;
266
+ }
267
+ /**
268
+ * Wraps its children and dismisses on an outside pointer press or Escape. One tested implementation
269
+ * of the dismissal logic every overlay reinvents. Nested layers form a stack — only the top-most
270
+ * responds to Escape and outside pointers. All listeners run inside `useSignalEffect`.
271
+ */
272
+ declare function DismissableLayer({
273
+ children,
274
+ onDismiss,
275
+ disableOutsidePointer,
276
+ escapeDismisses
277
+ }: DismissableLayerProps): import("react").JSX.Element;
278
+ //#endregion
279
+ //#region src/roving-focus.d.ts
280
+ type RovingOrientation = 'horizontal' | 'vertical' | 'both';
281
+ interface UseRovingFocusOptions {
282
+ orientation?: RovingOrientation;
283
+ /** Wrap focus from last→first and first→last. Default false. */
284
+ loop?: boolean;
285
+ /** Initial active index. Default 0. */
286
+ defaultIndex?: number;
287
+ }
288
+ interface RovingItemProps {
289
+ tabIndex: number;
290
+ onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
291
+ onFocus: () => void;
292
+ ref: (el: HTMLElement | null) => void;
293
+ }
294
+ interface UseRovingFocusReturn {
295
+ activeIndex: ReadonlySignal<number>;
296
+ getItemProps: (index: number) => RovingItemProps;
297
+ setActiveIndex: (index: number) => void;
298
+ }
299
+ /**
300
+ * Roving tabindex without context. The parent calls `useRovingFocus(...)` and spreads
301
+ * `getItemProps(i)` onto each item. Only the active item is tabbable (`tabIndex={0}`); arrow keys
302
+ * move focus (Home/End jump; `loop` wraps). State lives in a signal — the consuming component reads
303
+ * it during render, so React apps must call `useSignals()` (per the project rule).
304
+ */
305
+ declare function useRovingFocus(options?: UseRovingFocusOptions): UseRovingFocusReturn;
306
+ //#endregion
307
+ //#region src/presence.d.ts
308
+ interface PresenceProps {
309
+ /** Whether the content should be present. Accepts a boolean or a Signal. */
310
+ present: boolean | Signal<boolean>;
311
+ /** A single element child; receives `data-state="open|closed"` and a composed ref. */
312
+ children: ReactElement;
313
+ }
314
+ /**
315
+ * Defers unmount until an exit animation/transition finishes. While `present` is true the child is
316
+ * mounted with `data-state="open"`; when it flips to false the child gets `data-state="closed"` and
317
+ * stays mounted until its `animationend`/`transitionend` fires (or unmounts immediately if there is
318
+ * no animation). CSS drives the visuals via `data-state`; this only manages mount timing. No
319
+ * `useEffect` — the present prop is mirrored into a signal and watched with `useSignalEffect`.
320
+ */
321
+ declare function Presence({
322
+ present,
323
+ children
324
+ }: PresenceProps): ReactElement<Record<string, unknown>, string | import("react").JSXElementConstructor<any>> | null;
325
+ //#endregion
326
+ //#region src/anchor.d.ts
327
+ type AnchorSide = 'top' | 'bottom' | 'left' | 'right';
328
+ type AnchorAlign = 'start' | 'center' | 'end';
329
+ type AnchorPlacement = AnchorSide | `${AnchorSide}-${'start' | 'end'}`;
330
+ interface UseAnchorPositionOptions {
331
+ anchorRef: RefObject<HTMLElement | null>;
332
+ floatingRef: RefObject<HTMLElement | null>;
333
+ placement?: AnchorPlacement;
334
+ /** Gate positioning (e.g. only while open). Default true. */
335
+ enabled?: boolean | Signal<boolean>;
336
+ }
337
+ interface UseAnchorPositionReturn {
338
+ /** Spread onto the anchor element (CSS-anchor path sets `anchor-name`). */
339
+ anchorStyle: CSSProperties;
340
+ /** Spread onto the floating element. */
341
+ floatingStyle: CSSProperties;
342
+ /** Whether the CSS anchor-positioning primary path is active. */
343
+ cssAnchorSupported: boolean;
344
+ }
345
+ /** JS fallback math — position `floating` relative to `anchor` for the given placement. */
346
+ declare function computePosition(anchor: {
347
+ top: number;
348
+ left: number;
349
+ right: number;
350
+ bottom: number;
351
+ width: number;
352
+ height: number;
353
+ }, floating: {
354
+ width: number;
355
+ height: number;
356
+ }, placement: AnchorPlacement): {
357
+ top: number;
358
+ left: number;
359
+ };
360
+ /**
361
+ * Positions a floating element relative to an anchor. Primary path: CSS anchor positioning
362
+ * (Chrome-leading) — emits `anchor-name`/`position-anchor` + `anchor()` insets. Fallback (behind
363
+ * `CSS.supports`): JS `getBoundingClientRect` placement recomputed on scroll/resize inside
364
+ * `useSignalEffect`. Small by design — not a collision/flip engine (deferred).
365
+ */
366
+ declare function useAnchorPosition(options: UseAnchorPositionOptions): UseAnchorPositionReturn;
367
+ //#endregion
368
+ //#region src/index.d.ts
369
+ declare const VERSION = "0.0.0";
370
+ //#endregion
371
+ export { type AccessibilityMeta, type AnchorAlign, type AnchorPlacement, type AnchorSide, type ComponentIntent, type ComponentMeta, DismissableLayer, type DismissableLayerProps, ErrorBoundary, type ErrorBoundaryProps, type ExampleMeta, FocusScope, type FocusScopeProps, type IntentAntiPattern, type IntentContent, type IntentFlexibility, type IntentRelated, type IntentRelationship, type Machine, type MachineConfig, Portal, type PortalProps, Presence, type PresenceProps, type PropMeta, type ReadonlySignal, type RovingItemProps, type RovingOrientation, type Signal, Slot, type SlotProps, type StateConfig, SuspenseBoundary, type SuspenseBoundaryProps, type UseAnchorPositionOptions, type UseAnchorPositionReturn, type UseClipboardOptions, type UseClipboardReturn, type UseControllableSignalOptions, type UseRovingFocusOptions, type UseRovingFocusReturn, VERSION, VisuallyHidden, type VisuallyHiddenProps, type WcagLevel, batch, cn, composeRefs, computePosition, computed, createMachine, effect, mergeProps, signal, useAnchorPosition, useClipboard, useComputed, useControllableSignal, useId, useMachine, useMediaQuery, useRovingFocus, useScrollLock, useSignal, useSignalEffect, useSignals };
372
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/machine.ts","../src/utils.ts","../src/error-boundary.tsx","../src/suspense-boundary.tsx","../src/portal.tsx","../src/visually-hidden.tsx","../src/focus-scope.tsx","../src/slot.tsx","../src/controllable.ts","../src/media-query.ts","../src/scroll-lock.ts","../src/use-id.ts","../src/clipboard.ts","../src/dismissable-layer.tsx","../src/roving-focus.ts","../src/presence.tsx","../src/anchor.tsx","../src/index.ts"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,561 @@
1
+ import { batch, computed, effect, signal, useComputed, useSignal, useSignal as useSignal$1, useSignalEffect } from "@preact/signals-react";
2
+ import { useSignals } from "@preact/signals-react/runtime";
3
+ import { Children, Component, Suspense, cloneElement, forwardRef, isValidElement, useId as useId$1, useRef } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { createPortal } from "react-dom";
6
+ //#region src/machine.ts
7
+ function createMachine(config) {
8
+ return config;
9
+ }
10
+ function useMachine(machine) {
11
+ const state = useSignal$1(machine.initial);
12
+ const send = (event) => {
13
+ const next = machine.states[state.value]?.on?.[event];
14
+ if (next !== void 0) state.value = next;
15
+ };
16
+ return [state, send];
17
+ }
18
+ //#endregion
19
+ //#region src/utils.ts
20
+ function cn(...classes) {
21
+ return classes.filter(Boolean).join(" ");
22
+ }
23
+ function composeRefs(...refs) {
24
+ return (value) => {
25
+ refs.forEach((ref) => {
26
+ if (!ref) return;
27
+ if (typeof ref === "function") ref(value);
28
+ else ref.current = value;
29
+ });
30
+ };
31
+ }
32
+ function mergeProps(...propsList) {
33
+ const result = {};
34
+ for (const props of propsList) for (const [key, val] of Object.entries(props)) if (key.startsWith("on") && typeof val === "function" && typeof result[key] === "function") {
35
+ const existing = result[key];
36
+ result[key] = (...args) => {
37
+ existing(...args);
38
+ val(...args);
39
+ };
40
+ } else result[key] = val;
41
+ return result;
42
+ }
43
+ //#endregion
44
+ //#region src/error-boundary.tsx
45
+ var ErrorBoundary = class extends Component {
46
+ state = { error: null };
47
+ static getDerivedStateFromError(error) {
48
+ return { error };
49
+ }
50
+ componentDidCatch(error, info) {
51
+ this.props.onError?.(error, info);
52
+ }
53
+ reset = () => {
54
+ this.setState({ error: null });
55
+ };
56
+ render() {
57
+ const { error } = this.state;
58
+ if (error !== null) {
59
+ const { fallback } = this.props;
60
+ return typeof fallback === "function" ? fallback(error, this.reset) : fallback;
61
+ }
62
+ return this.props.children;
63
+ }
64
+ };
65
+ //#endregion
66
+ //#region src/suspense-boundary.tsx
67
+ function SuspenseBoundary({ children, fallback }) {
68
+ return /* @__PURE__ */ jsx(Suspense, {
69
+ fallback: fallback ?? /* @__PURE__ */ jsx("span", {
70
+ "aria-busy": "true",
71
+ "aria-label": "Loading"
72
+ }),
73
+ children
74
+ });
75
+ }
76
+ //#endregion
77
+ //#region src/portal.tsx
78
+ function Portal({ children, container }) {
79
+ useSignals();
80
+ const mounted = useSignal(false);
81
+ const containerRef = useRef(null);
82
+ useSignalEffect(() => {
83
+ if (typeof document === "undefined") return;
84
+ containerRef.current = container ?? document.body;
85
+ mounted.value = true;
86
+ return () => {
87
+ mounted.value = false;
88
+ };
89
+ });
90
+ if (!mounted.value || containerRef.current === null) return null;
91
+ return createPortal(children, containerRef.current);
92
+ }
93
+ //#endregion
94
+ //#region src/visually-hidden.module.css
95
+ var visually_hidden_module_default = {
96
+ "focusable": "rYIFkW_focusable",
97
+ "root": "rYIFkW_root"
98
+ };
99
+ //#endregion
100
+ //#region src/visually-hidden.tsx
101
+ function VisuallyHidden({ children, showOnFocus, className, ...props }) {
102
+ return /* @__PURE__ */ jsx("span", {
103
+ className: [
104
+ visually_hidden_module_default.root,
105
+ showOnFocus && visually_hidden_module_default.focusable,
106
+ className
107
+ ].filter(Boolean).join(" "),
108
+ ...props,
109
+ children
110
+ });
111
+ }
112
+ //#endregion
113
+ //#region src/focus-scope.tsx
114
+ const FOCUSABLE = "a[href],area[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),[tabindex]:not([tabindex=\"-1\"])";
115
+ function FocusScope({ children, trapped, restoreFocus, autoFocus }) {
116
+ useSignals();
117
+ const containerRef = useRef(null);
118
+ const previousFocusRef = useRef(null);
119
+ useSignalEffect(() => {
120
+ const container = containerRef.current;
121
+ if (!container) return;
122
+ if (restoreFocus) previousFocusRef.current = document.activeElement;
123
+ if (autoFocus) container.querySelector(FOCUSABLE)?.focus();
124
+ if (!trapped) return;
125
+ function handleKeyDown(e) {
126
+ if (e.key !== "Tab") return;
127
+ const focusable = Array.from(container.querySelectorAll(FOCUSABLE));
128
+ if (focusable.length === 0) return;
129
+ const first = focusable[0];
130
+ const last = focusable[focusable.length - 1];
131
+ if (e.shiftKey) {
132
+ if (document.activeElement === first) {
133
+ e.preventDefault();
134
+ last.focus();
135
+ }
136
+ } else if (document.activeElement === last) {
137
+ e.preventDefault();
138
+ first.focus();
139
+ }
140
+ }
141
+ document.addEventListener("keydown", handleKeyDown);
142
+ return () => {
143
+ document.removeEventListener("keydown", handleKeyDown);
144
+ if (restoreFocus && previousFocusRef.current instanceof HTMLElement) previousFocusRef.current.focus();
145
+ };
146
+ });
147
+ return /* @__PURE__ */ jsx("div", {
148
+ ref: containerRef,
149
+ children
150
+ });
151
+ }
152
+ //#endregion
153
+ //#region src/slot.tsx
154
+ /**
155
+ * Slot renders its single child element while merging the Slot's own props and ref onto it —
156
+ * the Radix-style `asChild` composition primitive, built on cascivo's existing
157
+ * `mergeProps`/`composeRefs`/`cn`.
158
+ *
159
+ * - Non-handler props: child wins (so the consumer's element controls its own identity).
160
+ * - `on*` handlers: chained (Slot's runs, then the child's) via `mergeProps`.
161
+ * - `className`: unioned via `cn`. `style`: shallow-merged (child wins on conflicts).
162
+ * - `ref`: composed so both the Slot consumer's ref and the child's own ref receive the node.
163
+ *
164
+ * Fails fast if `children` is not exactly one valid React element.
165
+ */
166
+ const Slot = forwardRef(function Slot(props, forwardedRef) {
167
+ const { children, className: slotClassName, style: slotStyle, ...slotProps } = props;
168
+ if (!isValidElement(children) || Children.count(children) !== 1) throw new Error("Slot expects exactly one valid React element child (asChild requires a single element).");
169
+ const child = children;
170
+ const childProps = child.props;
171
+ const merged = mergeProps(slotProps, childProps);
172
+ merged["className"] = cn(slotClassName, childProps["className"]);
173
+ merged["style"] = {
174
+ ...slotStyle ?? {},
175
+ ...childProps["style"] ?? {}
176
+ };
177
+ merged["ref"] = composeRefs(forwardedRef, childProps["ref"] ?? child.ref);
178
+ return cloneElement(child, merged);
179
+ });
180
+ //#endregion
181
+ //#region src/controllable.ts
182
+ /**
183
+ * Codifies the controlled/uncontrolled pattern documented by hand in CLAUDE.md
184
+ * (`const x = useSignal(open); x.value = open`).
185
+ *
186
+ * Returns a `[signal, setValue]` pair:
187
+ * - **Controlled** (`value` provided): the signal mirrors `value` on every render; `setValue`
188
+ * does not mutate the signal locally — it only routes the request through `onChange` so the
189
+ * parent owns the state (React semantics).
190
+ * - **Uncontrolled** (`value` undefined): the signal owns the state seeded from `defaultValue`;
191
+ * `setValue` updates it locally and also calls `onChange`.
192
+ *
193
+ * Whether a component is controlled is fixed for its life, exactly like React.
194
+ */
195
+ function useControllableSignal(options) {
196
+ const { value, defaultValue, onChange } = options;
197
+ const isControlled = value !== void 0;
198
+ const sig = useSignal(isControlled ? value : defaultValue);
199
+ const onChangeRef = useRef(onChange);
200
+ onChangeRef.current = onChange;
201
+ if (isControlled) sig.value = value;
202
+ const setValue = (next) => {
203
+ if (!isControlled) sig.value = next;
204
+ onChangeRef.current?.(next);
205
+ };
206
+ return [sig, setValue];
207
+ }
208
+ //#endregion
209
+ //#region src/media-query.ts
210
+ function initialMatch(query) {
211
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
212
+ return window.matchMedia(query).matches;
213
+ }
214
+ /**
215
+ * `useMediaQuery(query)` → a `ReadonlySignal<boolean>` that tracks whether the media query matches.
216
+ * SSR-safe (defaults to `false` when `window`/`matchMedia` is unavailable). Subscribes to changes
217
+ * inside `useSignalEffect`; the listener is cleaned up on teardown.
218
+ */
219
+ function useMediaQuery(query) {
220
+ const matches = useSignal(initialMatch(query));
221
+ useSignalEffect(() => {
222
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
223
+ const mql = window.matchMedia(query);
224
+ matches.value = mql.matches;
225
+ const handler = (event) => {
226
+ matches.value = event.matches;
227
+ };
228
+ mql.addEventListener("change", handler);
229
+ return () => mql.removeEventListener("change", handler);
230
+ });
231
+ return matches;
232
+ }
233
+ //#endregion
234
+ //#region src/scroll-lock.ts
235
+ /**
236
+ * Locks `document.body` scroll while `locked` is true, compensating for the scrollbar width to
237
+ * avoid a layout shift. Accepts a plain boolean or a `Signal<boolean>` (reactive). The previous
238
+ * inline styles are restored on unlock/teardown. DOM work runs inside `useSignalEffect`.
239
+ */
240
+ function useScrollLock(locked) {
241
+ useSignalEffect(() => {
242
+ if (!(typeof locked === "boolean" ? locked : locked.value) || typeof document === "undefined") return;
243
+ const body = document.body;
244
+ const prevOverflow = body.style.overflow;
245
+ const prevPaddingInlineEnd = body.style.paddingInlineEnd;
246
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
247
+ body.style.overflow = "hidden";
248
+ if (scrollbarWidth > 0) {
249
+ const current = Number.parseInt(window.getComputedStyle(body).paddingInlineEnd, 10) || 0;
250
+ body.style.paddingInlineEnd = `${current + scrollbarWidth}px`;
251
+ }
252
+ return () => {
253
+ body.style.overflow = prevOverflow;
254
+ body.style.paddingInlineEnd = prevPaddingInlineEnd;
255
+ };
256
+ });
257
+ }
258
+ //#endregion
259
+ //#region src/use-id.ts
260
+ /**
261
+ * SSR-safe stable id for aria wiring (Label/Field). Wraps React's `useId` and strips the
262
+ * non-selector-safe colons React emits, prefixing for readability (default `cascivo`).
263
+ */
264
+ function useId(prefix = "cascivo") {
265
+ return `${prefix}-${useId$1().replace(/:/g, "")}`;
266
+ }
267
+ //#endregion
268
+ //#region src/clipboard.ts
269
+ /**
270
+ * Copy-to-clipboard helper that de-dupes the inline logic in `copy-button`.
271
+ * `copy(text)` writes via `navigator.clipboard.writeText`; `copied` flips true then resets after
272
+ * `timeout`. The reset timer is cleared on unmount via `useSignalEffect` (no `useEffect`).
273
+ */
274
+ function useClipboard(options = {}) {
275
+ const { timeout = 2e3 } = options;
276
+ const copied = useSignal(false);
277
+ const timerRef = useRef(null);
278
+ useSignalEffect(() => {
279
+ return () => {
280
+ if (timerRef.current) clearTimeout(timerRef.current);
281
+ };
282
+ });
283
+ const copy = async (text) => {
284
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
285
+ await navigator.clipboard.writeText(text);
286
+ copied.value = true;
287
+ if (timerRef.current) clearTimeout(timerRef.current);
288
+ timerRef.current = setTimeout(() => {
289
+ copied.value = false;
290
+ }, timeout);
291
+ };
292
+ return {
293
+ copied,
294
+ copy
295
+ };
296
+ }
297
+ //#endregion
298
+ //#region src/dismissable-layer.tsx
299
+ const layers = [];
300
+ /**
301
+ * Wraps its children and dismisses on an outside pointer press or Escape. One tested implementation
302
+ * of the dismissal logic every overlay reinvents. Nested layers form a stack — only the top-most
303
+ * responds to Escape and outside pointers. All listeners run inside `useSignalEffect`.
304
+ */
305
+ function DismissableLayer({ children, onDismiss, disableOutsidePointer, escapeDismisses = true }) {
306
+ useSignals();
307
+ const rootRef = useRef(null);
308
+ const onDismissRef = useRef(onDismiss);
309
+ onDismissRef.current = onDismiss;
310
+ useSignalEffect(() => {
311
+ if (typeof document === "undefined") return;
312
+ const entry = { getRoot: () => rootRef.current };
313
+ layers.push(entry);
314
+ const isTop = () => {
315
+ const root = rootRef.current;
316
+ if (!root) return false;
317
+ for (const other of layers) {
318
+ if (other === entry) continue;
319
+ const otherRoot = other.getRoot();
320
+ if (otherRoot && root.contains(otherRoot)) return false;
321
+ }
322
+ return true;
323
+ };
324
+ const handlePointer = (event) => {
325
+ if (disableOutsidePointer || !isTop()) return;
326
+ const root = rootRef.current;
327
+ if (!root) return;
328
+ if (event.target instanceof Node && !root.contains(event.target)) onDismissRef.current?.();
329
+ };
330
+ const handleKey = (event) => {
331
+ if (!escapeDismisses || event.key !== "Escape" || !isTop()) return;
332
+ onDismissRef.current?.();
333
+ };
334
+ document.addEventListener("pointerdown", handlePointer, true);
335
+ document.addEventListener("keydown", handleKey);
336
+ return () => {
337
+ document.removeEventListener("pointerdown", handlePointer, true);
338
+ document.removeEventListener("keydown", handleKey);
339
+ const idx = layers.indexOf(entry);
340
+ if (idx !== -1) layers.splice(idx, 1);
341
+ };
342
+ });
343
+ return /* @__PURE__ */ jsx("div", {
344
+ ref: rootRef,
345
+ children
346
+ });
347
+ }
348
+ //#endregion
349
+ //#region src/roving-focus.ts
350
+ /**
351
+ * Roving tabindex without context. The parent calls `useRovingFocus(...)` and spreads
352
+ * `getItemProps(i)` onto each item. Only the active item is tabbable (`tabIndex={0}`); arrow keys
353
+ * move focus (Home/End jump; `loop` wraps). State lives in a signal — the consuming component reads
354
+ * it during render, so React apps must call `useSignals()` (per the project rule).
355
+ */
356
+ function useRovingFocus(options = {}) {
357
+ const { orientation = "horizontal", loop = false, defaultIndex = 0 } = options;
358
+ const activeIndex = useSignal(defaultIndex);
359
+ const itemsRef = useRef([]);
360
+ const focusIndex = (index) => {
361
+ activeIndex.value = index;
362
+ itemsRef.current[index]?.focus();
363
+ };
364
+ const handleKeyDown = (event, index) => {
365
+ const count = itemsRef.current.length;
366
+ if (count === 0) return;
367
+ const horizontal = orientation !== "vertical";
368
+ const vertical = orientation !== "horizontal";
369
+ let next = index;
370
+ if (horizontal && event.key === "ArrowRight" || vertical && event.key === "ArrowDown") next = index + 1;
371
+ else if (horizontal && event.key === "ArrowLeft" || vertical && event.key === "ArrowUp") next = index - 1;
372
+ else if (event.key === "Home") next = 0;
373
+ else if (event.key === "End") next = count - 1;
374
+ else return;
375
+ event.preventDefault();
376
+ if (next < 0) next = loop ? count - 1 : 0;
377
+ if (next >= count) next = loop ? 0 : count - 1;
378
+ focusIndex(next);
379
+ };
380
+ const getItemProps = (index) => ({
381
+ tabIndex: activeIndex.value === index ? 0 : -1,
382
+ onKeyDown: (event) => handleKeyDown(event, index),
383
+ onFocus: () => {
384
+ activeIndex.value = index;
385
+ },
386
+ ref: (el) => {
387
+ itemsRef.current[index] = el;
388
+ }
389
+ });
390
+ return {
391
+ activeIndex,
392
+ getItemProps,
393
+ setActiveIndex: (index) => {
394
+ activeIndex.value = index;
395
+ }
396
+ };
397
+ }
398
+ //#endregion
399
+ //#region src/presence.tsx
400
+ /**
401
+ * Defers unmount until an exit animation/transition finishes. While `present` is true the child is
402
+ * mounted with `data-state="open"`; when it flips to false the child gets `data-state="closed"` and
403
+ * stays mounted until its `animationend`/`transitionend` fires (or unmounts immediately if there is
404
+ * no animation). CSS drives the visuals via `data-state`; this only manages mount timing. No
405
+ * `useEffect` — the present prop is mirrored into a signal and watched with `useSignalEffect`.
406
+ */
407
+ function Presence({ present, children }) {
408
+ useSignals();
409
+ const presentVal = typeof present === "boolean" ? present : present.value;
410
+ const presentSig = useSignal(presentVal);
411
+ presentSig.value = presentVal;
412
+ const isMounted = useSignal(presentVal);
413
+ const state = useSignal(presentVal ? "open" : "closed");
414
+ const nodeRef = useRef(null);
415
+ useSignalEffect(() => {
416
+ if (presentSig.value) {
417
+ isMounted.value = true;
418
+ state.value = "open";
419
+ return;
420
+ }
421
+ state.value = "closed";
422
+ const node = nodeRef.current;
423
+ if (!node || typeof getComputedStyle === "undefined") {
424
+ isMounted.value = false;
425
+ return;
426
+ }
427
+ const styles = getComputedStyle(node);
428
+ if (!(styles.animationName !== "none" && styles.animationName !== "" && styles.animationDuration !== "0s" || styles.transitionDuration !== "0s" && styles.transitionDuration !== "")) {
429
+ isMounted.value = false;
430
+ return;
431
+ }
432
+ const handleEnd = (event) => {
433
+ if (event.target === node) isMounted.value = false;
434
+ };
435
+ node.addEventListener("animationend", handleEnd);
436
+ node.addEventListener("transitionend", handleEnd);
437
+ return () => {
438
+ node.removeEventListener("animationend", handleEnd);
439
+ node.removeEventListener("transitionend", handleEnd);
440
+ };
441
+ });
442
+ if (!isMounted.value) return null;
443
+ if (!isValidElement(children)) throw new Error("Presence expects a single valid React element child");
444
+ const child = children;
445
+ const childRef = child.props["ref"] ?? child.ref;
446
+ return cloneElement(child, {
447
+ "data-state": state.value,
448
+ ref: composeRefs(childRef, (el) => {
449
+ nodeRef.current = el;
450
+ })
451
+ });
452
+ }
453
+ //#endregion
454
+ //#region src/anchor.tsx
455
+ function supportsCssAnchor() {
456
+ return typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("anchor-name: --x");
457
+ }
458
+ function parsePlacement(placement) {
459
+ const [side, end] = placement.split("-");
460
+ return {
461
+ side,
462
+ align: end ?? "center"
463
+ };
464
+ }
465
+ /** JS fallback math — position `floating` relative to `anchor` for the given placement. */
466
+ function computePosition(anchor, floating, placement) {
467
+ const { side, align } = parsePlacement(placement);
468
+ let top = 0;
469
+ let left = 0;
470
+ if (side === "top" || side === "bottom") {
471
+ top = side === "bottom" ? anchor.bottom : anchor.top - floating.height;
472
+ left = align === "start" ? anchor.left : align === "end" ? anchor.right - floating.width : anchor.left + (anchor.width - floating.width) / 2;
473
+ } else {
474
+ left = side === "right" ? anchor.right : anchor.left - floating.width;
475
+ top = align === "start" ? anchor.top : align === "end" ? anchor.bottom - floating.height : anchor.top + (anchor.height - floating.height) / 2;
476
+ }
477
+ return {
478
+ top,
479
+ left
480
+ };
481
+ }
482
+ function cssAnchorInsets(side, align) {
483
+ const style = {
484
+ position: "fixed",
485
+ insetBlockStart: "auto",
486
+ insetInlineStart: "auto"
487
+ };
488
+ if (side === "bottom") style.top = "anchor(bottom)";
489
+ if (side === "top") style.bottom = "anchor(top)";
490
+ if (side === "right") style.left = "anchor(right)";
491
+ if (side === "left") style.right = "anchor(left)";
492
+ if (side === "top" || side === "bottom") {
493
+ style.left = align === "end" ? void 0 : align === "start" ? "anchor(left)" : "anchor(center)";
494
+ if (align === "end") style.right = "anchor(right)";
495
+ } else {
496
+ style.top = align === "end" ? void 0 : align === "start" ? "anchor(top)" : "anchor(center)";
497
+ if (align === "end") style.bottom = "anchor(bottom)";
498
+ }
499
+ return style;
500
+ }
501
+ /**
502
+ * Positions a floating element relative to an anchor. Primary path: CSS anchor positioning
503
+ * (Chrome-leading) — emits `anchor-name`/`position-anchor` + `anchor()` insets. Fallback (behind
504
+ * `CSS.supports`): JS `getBoundingClientRect` placement recomputed on scroll/resize inside
505
+ * `useSignalEffect`. Small by design — not a collision/flip engine (deferred).
506
+ */
507
+ function useAnchorPosition(options) {
508
+ const { anchorRef, floatingRef, placement = "bottom", enabled = true } = options;
509
+ useSignals();
510
+ const supported = supportsCssAnchor();
511
+ const name = useId("anchor");
512
+ const pos = useSignal(null);
513
+ useSignalEffect(() => {
514
+ const isEnabled = typeof enabled === "boolean" ? enabled : enabled.value;
515
+ if (supported || !isEnabled || typeof window === "undefined") return;
516
+ const update = () => {
517
+ const a = anchorRef.current;
518
+ const f = floatingRef.current;
519
+ if (!a || !f) return;
520
+ pos.value = computePosition(a.getBoundingClientRect(), f.getBoundingClientRect(), placement);
521
+ };
522
+ update();
523
+ window.addEventListener("scroll", update, true);
524
+ window.addEventListener("resize", update);
525
+ return () => {
526
+ window.removeEventListener("scroll", update, true);
527
+ window.removeEventListener("resize", update);
528
+ };
529
+ });
530
+ if (supported) {
531
+ const { side, align } = parsePlacement(placement);
532
+ return {
533
+ anchorStyle: { anchorName: `--${name}` },
534
+ floatingStyle: {
535
+ positionAnchor: `--${name}`,
536
+ ...cssAnchorInsets(side, align)
537
+ },
538
+ cssAnchorSupported: true
539
+ };
540
+ }
541
+ const p = pos.value;
542
+ return {
543
+ anchorStyle: {},
544
+ floatingStyle: p ? {
545
+ position: "fixed",
546
+ top: p.top,
547
+ left: p.left
548
+ } : {
549
+ position: "fixed",
550
+ visibility: "hidden"
551
+ },
552
+ cssAnchorSupported: false
553
+ };
554
+ }
555
+ //#endregion
556
+ //#region src/index.ts
557
+ const VERSION = "0.0.0";
558
+ //#endregion
559
+ export { DismissableLayer, ErrorBoundary, FocusScope, Portal, Presence, Slot, SuspenseBoundary, VERSION, VisuallyHidden, batch, cn, composeRefs, computePosition, computed, createMachine, effect, mergeProps, signal, useAnchorPosition, useClipboard, useComputed, useControllableSignal, useId, useMachine, useMediaQuery, useRovingFocus, useScrollLock, useSignal, useSignalEffect, useSignals };
560
+
561
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["useSignal","styles","useReactId"],"sources":["../src/machine.ts","../src/utils.ts","../src/error-boundary.tsx","../src/suspense-boundary.tsx","../src/portal.tsx","../src/visually-hidden.module.css","../src/visually-hidden.tsx","../src/focus-scope.tsx","../src/slot.tsx","../src/controllable.ts","../src/media-query.ts","../src/scroll-lock.ts","../src/use-id.ts","../src/clipboard.ts","../src/dismissable-layer.tsx","../src/roving-focus.ts","../src/presence.tsx","../src/anchor.tsx","../src/index.ts"],"sourcesContent":["import { useSignal } from '@preact/signals-react'\nimport type { Machine, MachineConfig } from './types.ts'\n\nexport function createMachine<S extends string, I extends S = S>(\n config: MachineConfig<S, I>,\n): Machine<S> {\n return config as Machine<S>\n}\n\nexport function useMachine<S extends string>(machine: Machine<S>) {\n const state = useSignal<S>(machine.initial)\n const send = (event: string) => {\n const next = machine.states[state.value]?.on?.[event] as S | undefined\n if (next !== undefined) state.value = next\n }\n return [state, send] as const\n}\n","import type { Ref, RefCallback, MutableRefObject } from 'react'\n\nexport function cn(...classes: (string | undefined | null | false | 0)[]): string {\n return classes.filter(Boolean).join(' ')\n}\n\nexport function composeRefs<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> {\n return (value) => {\n refs.forEach((ref) => {\n if (!ref) return\n if (typeof ref === 'function') {\n ref(value)\n } else {\n ;(ref as MutableRefObject<T | null>).current = value\n }\n })\n }\n}\n\ntype EventHandler = (...args: unknown[]) => unknown\n\nexport function mergeProps<T extends Record<string, unknown>>(...propsList: Partial<T>[]): T {\n const result: Record<string, unknown> = {}\n for (const props of propsList) {\n for (const [key, val] of Object.entries(props)) {\n if (key.startsWith('on') && typeof val === 'function' && typeof result[key] === 'function') {\n const existing = result[key] as EventHandler\n result[key] = (...args: unknown[]) => {\n existing(...args)\n ;(val as EventHandler)(...args)\n }\n } else {\n result[key] = val\n }\n }\n }\n return result as T\n}\n","import { Component, type ErrorInfo, type ReactNode } from 'react'\n\nexport interface ErrorBoundaryProps {\n children: ReactNode\n fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode)\n onError?: (error: Error, info: ErrorInfo) => void\n}\n\ninterface State {\n error: Error | null\n}\n\nexport class ErrorBoundary extends Component<ErrorBoundaryProps, State> {\n state: State = { error: null }\n\n static getDerivedStateFromError(error: Error): State {\n return { error }\n }\n\n componentDidCatch(error: Error, info: ErrorInfo) {\n this.props.onError?.(error, info)\n }\n\n reset = () => {\n this.setState({ error: null })\n }\n\n render() {\n const { error } = this.state\n if (error !== null) {\n const { fallback } = this.props\n return typeof fallback === 'function' ? fallback(error, this.reset) : fallback\n }\n return this.props.children\n }\n}\n","import { Suspense, type ReactNode } from 'react'\n\nexport interface SuspenseBoundaryProps {\n children: ReactNode\n fallback?: ReactNode\n}\n\nexport function SuspenseBoundary({ children, fallback }: SuspenseBoundaryProps) {\n return (\n <Suspense fallback={fallback ?? <span aria-busy=\"true\" aria-label=\"Loading\" />}>\n {children}\n </Suspense>\n )\n}\n","'use client'\nimport { useRef, type ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport { useSignal, useSignalEffect, useSignals } from './signals.ts'\n\nexport interface PortalProps {\n children: ReactNode\n container?: Element | DocumentFragment\n}\n\nexport function Portal({ children, container }: PortalProps) {\n useSignals()\n const mounted = useSignal(false)\n const containerRef = useRef<Element | DocumentFragment | null>(null)\n\n useSignalEffect(() => {\n if (typeof document === 'undefined') return\n containerRef.current = container ?? document.body\n mounted.value = true\n return () => {\n mounted.value = false\n }\n })\n\n if (!mounted.value || containerRef.current === null) return null\n return createPortal(children, containerRef.current)\n}\n","@layer cascade.component {\n .root {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n .focusable:focus,\n .focusable:focus-within {\n position: static;\n width: auto;\n height: auto;\n padding: var(--cascivo-space-2) var(--cascivo-space-3);\n margin: 0;\n overflow: visible;\n clip: auto;\n white-space: normal;\n }\n}\n","import type { ReactNode, HTMLAttributes } from 'react'\nimport styles from './visually-hidden.module.css'\n\nexport interface VisuallyHiddenProps extends HTMLAttributes<HTMLSpanElement> {\n children: ReactNode\n showOnFocus?: boolean\n}\n\nexport function VisuallyHidden({\n children,\n showOnFocus,\n className,\n ...props\n}: VisuallyHiddenProps) {\n const classes = [styles.root, showOnFocus && styles.focusable, className]\n .filter(Boolean)\n .join(' ')\n return (\n <span className={classes} {...props}>\n {children}\n </span>\n )\n}\n","'use client'\nimport { useRef, type ReactNode } from 'react'\nimport { useSignalEffect, useSignals } from './signals.ts'\n\nconst FOCUSABLE =\n 'a[href],area[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),[tabindex]:not([tabindex=\"-1\"])'\n\nexport interface FocusScopeProps {\n children: ReactNode\n trapped?: boolean\n restoreFocus?: boolean\n autoFocus?: boolean\n}\n\nexport function FocusScope({ children, trapped, restoreFocus, autoFocus }: FocusScopeProps) {\n useSignals()\n const containerRef = useRef<HTMLDivElement>(null)\n const previousFocusRef = useRef<Element | null>(null)\n\n useSignalEffect(() => {\n const container = containerRef.current\n if (!container) return\n if (restoreFocus) previousFocusRef.current = document.activeElement\n if (autoFocus) {\n const first = container.querySelector<HTMLElement>(FOCUSABLE)\n first?.focus()\n }\n if (!trapped) return\n function handleKeyDown(e: KeyboardEvent) {\n if (e.key !== 'Tab') return\n const focusable = Array.from(container!.querySelectorAll<HTMLElement>(FOCUSABLE))\n if (focusable.length === 0) return\n const first = focusable[0]!\n const last = focusable[focusable.length - 1]!\n if (e.shiftKey) {\n if (document.activeElement === first) {\n e.preventDefault()\n last.focus()\n }\n } else {\n if (document.activeElement === last) {\n e.preventDefault()\n first.focus()\n }\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => {\n document.removeEventListener('keydown', handleKeyDown)\n if (restoreFocus && previousFocusRef.current instanceof HTMLElement) {\n previousFocusRef.current.focus()\n }\n }\n })\n\n return <div ref={containerRef}>{children}</div>\n}\n","'use client'\nimport {\n Children,\n cloneElement,\n forwardRef,\n isValidElement,\n type CSSProperties,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react'\nimport { cn, composeRefs, mergeProps } from './utils.ts'\n\nexport interface SlotProps {\n children?: ReactNode\n className?: string\n style?: CSSProperties\n [key: string]: unknown\n}\n\ntype ElementWithRef = ReactElement<Record<string, unknown>> & { ref?: Ref<unknown> }\n\n/**\n * Slot renders its single child element while merging the Slot's own props and ref onto it —\n * the Radix-style `asChild` composition primitive, built on cascivo's existing\n * `mergeProps`/`composeRefs`/`cn`.\n *\n * - Non-handler props: child wins (so the consumer's element controls its own identity).\n * - `on*` handlers: chained (Slot's runs, then the child's) via `mergeProps`.\n * - `className`: unioned via `cn`. `style`: shallow-merged (child wins on conflicts).\n * - `ref`: composed so both the Slot consumer's ref and the child's own ref receive the node.\n *\n * Fails fast if `children` is not exactly one valid React element.\n */\nexport const Slot = forwardRef<unknown, SlotProps>(function Slot(props, forwardedRef) {\n const { children, className: slotClassName, style: slotStyle, ...slotProps } = props\n\n if (!isValidElement(children) || Children.count(children) !== 1) {\n throw new Error(\n 'Slot expects exactly one valid React element child (asChild requires a single element).',\n )\n }\n\n const child = children as ElementWithRef\n const childProps = child.props\n const merged = mergeProps<Record<string, unknown>>(slotProps, childProps)\n\n merged['className'] = cn(\n slotClassName as string | undefined,\n childProps['className'] as string | undefined,\n )\n merged['style'] = {\n ...((slotStyle as CSSProperties | undefined) ?? {}),\n ...((childProps['style'] as CSSProperties | undefined) ?? {}),\n }\n // React 19 exposes ref as a regular prop; React 18 keeps it on the element. Prefer props.ref to\n // avoid touching the deprecated `element.ref` getter when it isn't needed.\n const childRef = (childProps['ref'] as Ref<unknown> | undefined) ?? child.ref\n merged['ref'] = composeRefs(forwardedRef, childRef)\n\n return cloneElement(child, merged)\n})\n","'use client'\nimport { useRef } from 'react'\nimport { useSignal, type Signal } from './signals.ts'\n\nexport interface UseControllableSignalOptions<T> {\n /** Controlled value. When provided, the component is controlled for its whole life. */\n value?: T | undefined\n /** Initial value for uncontrolled use. */\n defaultValue?: T | undefined\n /** Called on every write, in both controlled and uncontrolled modes. */\n onChange?: ((value: T) => void) | undefined\n}\n\n/**\n * Codifies the controlled/uncontrolled pattern documented by hand in CLAUDE.md\n * (`const x = useSignal(open); x.value = open`).\n *\n * Returns a `[signal, setValue]` pair:\n * - **Controlled** (`value` provided): the signal mirrors `value` on every render; `setValue`\n * does not mutate the signal locally — it only routes the request through `onChange` so the\n * parent owns the state (React semantics).\n * - **Uncontrolled** (`value` undefined): the signal owns the state seeded from `defaultValue`;\n * `setValue` updates it locally and also calls `onChange`.\n *\n * Whether a component is controlled is fixed for its life, exactly like React.\n */\nexport function useControllableSignal<T>(\n options: UseControllableSignalOptions<T>,\n): [Signal<T>, (next: T) => void] {\n const { value, defaultValue, onChange } = options\n const isControlled = value !== undefined\n\n const sig = useSignal<T>((isControlled ? value : defaultValue) as T)\n\n // Keep onChange current without an effect (the onCloseRef idiom from CLAUDE.md).\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n\n // Controlled: mirror the prop into the signal each render (no-op if unchanged).\n if (isControlled) sig.value = value as T\n\n const setValue = (next: T): void => {\n if (!isControlled) sig.value = next\n onChangeRef.current?.(next)\n }\n\n return [sig, setValue]\n}\n","'use client'\nimport { useSignal, useSignalEffect, type ReadonlySignal } from './signals.ts'\n\nfunction initialMatch(query: string): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false\n return window.matchMedia(query).matches\n}\n\n/**\n * `useMediaQuery(query)` → a `ReadonlySignal<boolean>` that tracks whether the media query matches.\n * SSR-safe (defaults to `false` when `window`/`matchMedia` is unavailable). Subscribes to changes\n * inside `useSignalEffect`; the listener is cleaned up on teardown.\n */\nexport function useMediaQuery(query: string): ReadonlySignal<boolean> {\n const matches = useSignal(initialMatch(query))\n\n useSignalEffect(() => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return\n const mql = window.matchMedia(query)\n matches.value = mql.matches\n const handler = (event: MediaQueryListEvent): void => {\n matches.value = event.matches\n }\n mql.addEventListener('change', handler)\n return () => mql.removeEventListener('change', handler)\n })\n\n return matches\n}\n","'use client'\nimport { useSignalEffect, type Signal } from './signals.ts'\n\n/**\n * Locks `document.body` scroll while `locked` is true, compensating for the scrollbar width to\n * avoid a layout shift. Accepts a plain boolean or a `Signal<boolean>` (reactive). The previous\n * inline styles are restored on unlock/teardown. DOM work runs inside `useSignalEffect`.\n */\nexport function useScrollLock(locked: Signal<boolean> | boolean): void {\n useSignalEffect(() => {\n const isLocked = typeof locked === 'boolean' ? locked : locked.value\n if (!isLocked || typeof document === 'undefined') return\n\n const body = document.body\n const prevOverflow = body.style.overflow\n const prevPaddingInlineEnd = body.style.paddingInlineEnd\n\n const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth\n body.style.overflow = 'hidden'\n if (scrollbarWidth > 0) {\n const current = Number.parseInt(window.getComputedStyle(body).paddingInlineEnd, 10) || 0\n body.style.paddingInlineEnd = `${current + scrollbarWidth}px`\n }\n\n return () => {\n body.style.overflow = prevOverflow\n body.style.paddingInlineEnd = prevPaddingInlineEnd\n }\n })\n}\n","'use client'\nimport { useId as useReactId } from 'react'\n\n/**\n * SSR-safe stable id for aria wiring (Label/Field). Wraps React's `useId` and strips the\n * non-selector-safe colons React emits, prefixing for readability (default `cascivo`).\n */\nexport function useId(prefix = 'cascivo'): string {\n const id = useReactId()\n return `${prefix}-${id.replace(/:/g, '')}`\n}\n","'use client'\nimport { useRef } from 'react'\nimport { useSignal, useSignalEffect, type ReadonlySignal } from './signals.ts'\n\nexport interface UseClipboardOptions {\n /** How long `copied` stays true after a successful copy, in ms. */\n timeout?: number\n}\n\nexport interface UseClipboardReturn {\n copied: ReadonlySignal<boolean>\n copy: (text: string) => Promise<void>\n}\n\n/**\n * Copy-to-clipboard helper that de-dupes the inline logic in `copy-button`.\n * `copy(text)` writes via `navigator.clipboard.writeText`; `copied` flips true then resets after\n * `timeout`. The reset timer is cleared on unmount via `useSignalEffect` (no `useEffect`).\n */\nexport function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {\n const { timeout = 2000 } = options\n const copied = useSignal(false)\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n useSignalEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n })\n\n const copy = async (text: string): Promise<void> => {\n if (typeof navigator === 'undefined' || !navigator.clipboard) return\n await navigator.clipboard.writeText(text)\n copied.value = true\n if (timerRef.current) clearTimeout(timerRef.current)\n timerRef.current = setTimeout(() => {\n copied.value = false\n }, timeout)\n }\n\n return { copied, copy }\n}\n","'use client'\nimport { useRef, type ReactNode } from 'react'\nimport { useSignalEffect, useSignals } from './signals.ts'\n\n// Module-level registry so nested layers (a dropdown inside a modal) dismiss top-first. Effects\n// mount child-first, so a stack would invert nesting; instead we resolve the top-most layer by DOM\n// depth — a layer responds only when no other mounted layer's root is nested inside its own root.\ninterface LayerEntry {\n getRoot: () => HTMLElement | null\n}\nconst layers: LayerEntry[] = []\n\nexport interface DismissableLayerProps {\n children: ReactNode\n /** Called when an outside pointer lands or Escape is pressed (when this is the top-most layer). */\n onDismiss?: () => void\n /** Disable the outside-pointer dismissal (Escape still works unless escapeDismisses is false). */\n disableOutsidePointer?: boolean\n /** Whether Escape dismisses the top-most layer. Default true. */\n escapeDismisses?: boolean\n}\n\n/**\n * Wraps its children and dismisses on an outside pointer press or Escape. One tested implementation\n * of the dismissal logic every overlay reinvents. Nested layers form a stack — only the top-most\n * responds to Escape and outside pointers. All listeners run inside `useSignalEffect`.\n */\nexport function DismissableLayer({\n children,\n onDismiss,\n disableOutsidePointer,\n escapeDismisses = true,\n}: DismissableLayerProps) {\n useSignals()\n const rootRef = useRef<HTMLDivElement>(null)\n const onDismissRef = useRef(onDismiss)\n onDismissRef.current = onDismiss\n\n useSignalEffect(() => {\n if (typeof document === 'undefined') return\n const entry: LayerEntry = { getRoot: () => rootRef.current }\n layers.push(entry)\n\n // Top-most = deepest in the DOM: no other mounted layer's root sits inside this one's root.\n const isTop = (): boolean => {\n const root = rootRef.current\n if (!root) return false\n for (const other of layers) {\n if (other === entry) continue\n const otherRoot = other.getRoot()\n if (otherRoot && root.contains(otherRoot)) return false\n }\n return true\n }\n\n const handlePointer = (event: PointerEvent): void => {\n if (disableOutsidePointer || !isTop()) return\n const root = rootRef.current\n if (!root) return\n if (event.target instanceof Node && !root.contains(event.target)) {\n onDismissRef.current?.()\n }\n }\n const handleKey = (event: KeyboardEvent): void => {\n if (!escapeDismisses || event.key !== 'Escape' || !isTop()) return\n onDismissRef.current?.()\n }\n\n document.addEventListener('pointerdown', handlePointer, true)\n document.addEventListener('keydown', handleKey)\n return () => {\n document.removeEventListener('pointerdown', handlePointer, true)\n document.removeEventListener('keydown', handleKey)\n const idx = layers.indexOf(entry)\n if (idx !== -1) layers.splice(idx, 1)\n }\n })\n\n return <div ref={rootRef}>{children}</div>\n}\n","'use client'\nimport { useRef } from 'react'\nimport { useSignal, type ReadonlySignal } from './signals.ts'\n\nexport type RovingOrientation = 'horizontal' | 'vertical' | 'both'\n\nexport interface UseRovingFocusOptions {\n orientation?: RovingOrientation\n /** Wrap focus from last→first and first→last. Default false. */\n loop?: boolean\n /** Initial active index. Default 0. */\n defaultIndex?: number\n}\n\nexport interface RovingItemProps {\n tabIndex: number\n onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void\n onFocus: () => void\n ref: (el: HTMLElement | null) => void\n}\n\nexport interface UseRovingFocusReturn {\n activeIndex: ReadonlySignal<number>\n getItemProps: (index: number) => RovingItemProps\n setActiveIndex: (index: number) => void\n}\n\n/**\n * Roving tabindex without context. The parent calls `useRovingFocus(...)` and spreads\n * `getItemProps(i)` onto each item. Only the active item is tabbable (`tabIndex={0}`); arrow keys\n * move focus (Home/End jump; `loop` wraps). State lives in a signal — the consuming component reads\n * it during render, so React apps must call `useSignals()` (per the project rule).\n */\nexport function useRovingFocus(options: UseRovingFocusOptions = {}): UseRovingFocusReturn {\n const { orientation = 'horizontal', loop = false, defaultIndex = 0 } = options\n const activeIndex = useSignal(defaultIndex)\n const itemsRef = useRef<(HTMLElement | null)[]>([])\n\n const focusIndex = (index: number): void => {\n activeIndex.value = index\n itemsRef.current[index]?.focus()\n }\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>, index: number): void => {\n const count = itemsRef.current.length\n if (count === 0) return\n const horizontal = orientation !== 'vertical'\n const vertical = orientation !== 'horizontal'\n let next = index\n if ((horizontal && event.key === 'ArrowRight') || (vertical && event.key === 'ArrowDown')) {\n next = index + 1\n } else if ((horizontal && event.key === 'ArrowLeft') || (vertical && event.key === 'ArrowUp')) {\n next = index - 1\n } else if (event.key === 'Home') {\n next = 0\n } else if (event.key === 'End') {\n next = count - 1\n } else {\n return\n }\n event.preventDefault()\n if (next < 0) next = loop ? count - 1 : 0\n if (next >= count) next = loop ? 0 : count - 1\n focusIndex(next)\n }\n\n const getItemProps = (index: number): RovingItemProps => ({\n tabIndex: activeIndex.value === index ? 0 : -1,\n onKeyDown: (event) => handleKeyDown(event, index),\n onFocus: () => {\n activeIndex.value = index\n },\n ref: (el) => {\n itemsRef.current[index] = el\n },\n })\n\n return {\n activeIndex,\n getItemProps,\n setActiveIndex: (index: number) => {\n activeIndex.value = index\n },\n }\n}\n","'use client'\nimport { cloneElement, isValidElement, useRef, type ReactElement, type Ref } from 'react'\nimport { useSignal, useSignalEffect, useSignals, type Signal } from './signals.ts'\nimport { composeRefs } from './utils.ts'\n\nexport interface PresenceProps {\n /** Whether the content should be present. Accepts a boolean or a Signal. */\n present: boolean | Signal<boolean>\n /** A single element child; receives `data-state=\"open|closed\"` and a composed ref. */\n children: ReactElement\n}\n\ntype ElementWithRef = ReactElement<Record<string, unknown>> & { ref?: Ref<unknown> }\n\n/**\n * Defers unmount until an exit animation/transition finishes. While `present` is true the child is\n * mounted with `data-state=\"open\"`; when it flips to false the child gets `data-state=\"closed\"` and\n * stays mounted until its `animationend`/`transitionend` fires (or unmounts immediately if there is\n * no animation). CSS drives the visuals via `data-state`; this only manages mount timing. No\n * `useEffect` — the present prop is mirrored into a signal and watched with `useSignalEffect`.\n */\nexport function Presence({ present, children }: PresenceProps) {\n useSignals()\n const presentVal = typeof present === 'boolean' ? present : present.value\n\n // Mirror the (boolean or signal) present value into a local signal so the effect reacts to both\n // prop changes (re-render) and signal changes.\n const presentSig = useSignal(presentVal)\n presentSig.value = presentVal\n\n const isMounted = useSignal(presentVal)\n const state = useSignal<'open' | 'closed'>(presentVal ? 'open' : 'closed')\n const nodeRef = useRef<HTMLElement | null>(null)\n\n useSignalEffect(() => {\n if (presentSig.value) {\n isMounted.value = true\n state.value = 'open'\n return\n }\n state.value = 'closed'\n const node = nodeRef.current\n if (!node || typeof getComputedStyle === 'undefined') {\n isMounted.value = false\n return\n }\n const styles = getComputedStyle(node)\n const animates =\n (styles.animationName !== 'none' &&\n styles.animationName !== '' &&\n styles.animationDuration !== '0s') ||\n (styles.transitionDuration !== '0s' && styles.transitionDuration !== '')\n if (!animates) {\n isMounted.value = false\n return\n }\n const handleEnd = (event: Event): void => {\n if (event.target === node) isMounted.value = false\n }\n node.addEventListener('animationend', handleEnd)\n node.addEventListener('transitionend', handleEnd)\n return () => {\n node.removeEventListener('animationend', handleEnd)\n node.removeEventListener('transitionend', handleEnd)\n }\n })\n\n if (!isMounted.value) return null\n if (!isValidElement(children)) {\n throw new Error('Presence expects a single valid React element child')\n }\n const child = children as ElementWithRef\n const childRef = (child.props['ref'] as Ref<unknown> | undefined) ?? child.ref\n return cloneElement(child, {\n 'data-state': state.value,\n ref: composeRefs(childRef, (el: HTMLElement | null) => {\n nodeRef.current = el\n }),\n } as Record<string, unknown>)\n}\n","'use client'\nimport { type CSSProperties, type RefObject } from 'react'\nimport { useSignal, useSignalEffect, useSignals, type Signal } from './signals.ts'\nimport { useId } from './use-id.ts'\n\nexport type AnchorSide = 'top' | 'bottom' | 'left' | 'right'\nexport type AnchorAlign = 'start' | 'center' | 'end'\nexport type AnchorPlacement = AnchorSide | `${AnchorSide}-${'start' | 'end'}`\n\nexport interface UseAnchorPositionOptions {\n anchorRef: RefObject<HTMLElement | null>\n floatingRef: RefObject<HTMLElement | null>\n placement?: AnchorPlacement\n /** Gate positioning (e.g. only while open). Default true. */\n enabled?: boolean | Signal<boolean>\n}\n\nexport interface UseAnchorPositionReturn {\n /** Spread onto the anchor element (CSS-anchor path sets `anchor-name`). */\n anchorStyle: CSSProperties\n /** Spread onto the floating element. */\n floatingStyle: CSSProperties\n /** Whether the CSS anchor-positioning primary path is active. */\n cssAnchorSupported: boolean\n}\n\nfunction supportsCssAnchor(): boolean {\n return (\n typeof CSS !== 'undefined' &&\n typeof CSS.supports === 'function' &&\n CSS.supports('anchor-name: --x')\n )\n}\n\nfunction parsePlacement(placement: AnchorPlacement): { side: AnchorSide; align: AnchorAlign } {\n const [side, end] = placement.split('-') as [AnchorSide, 'start' | 'end' | undefined]\n return { side, align: end ?? 'center' }\n}\n\n/** JS fallback math — position `floating` relative to `anchor` for the given placement. */\nexport function computePosition(\n anchor: {\n top: number\n left: number\n right: number\n bottom: number\n width: number\n height: number\n },\n floating: { width: number; height: number },\n placement: AnchorPlacement,\n): { top: number; left: number } {\n const { side, align } = parsePlacement(placement)\n let top = 0\n let left = 0\n if (side === 'top' || side === 'bottom') {\n top = side === 'bottom' ? anchor.bottom : anchor.top - floating.height\n left =\n align === 'start'\n ? anchor.left\n : align === 'end'\n ? anchor.right - floating.width\n : anchor.left + (anchor.width - floating.width) / 2\n } else {\n left = side === 'right' ? anchor.right : anchor.left - floating.width\n top =\n align === 'start'\n ? anchor.top\n : align === 'end'\n ? anchor.bottom - floating.height\n : anchor.top + (anchor.height - floating.height) / 2\n }\n return { top, left }\n}\n\nfunction cssAnchorInsets(side: AnchorSide, align: AnchorAlign): CSSProperties {\n const style: CSSProperties = {\n position: 'fixed',\n insetBlockStart: 'auto',\n insetInlineStart: 'auto',\n }\n if (side === 'bottom') style.top = 'anchor(bottom)'\n if (side === 'top') style.bottom = 'anchor(top)'\n if (side === 'right') style.left = 'anchor(right)'\n if (side === 'left') style.right = 'anchor(left)'\n if (side === 'top' || side === 'bottom') {\n style.left = align === 'end' ? undefined : align === 'start' ? 'anchor(left)' : 'anchor(center)'\n if (align === 'end') style.right = 'anchor(right)'\n } else {\n style.top = align === 'end' ? undefined : align === 'start' ? 'anchor(top)' : 'anchor(center)'\n if (align === 'end') style.bottom = 'anchor(bottom)'\n }\n return style\n}\n\n/**\n * Positions a floating element relative to an anchor. Primary path: CSS anchor positioning\n * (Chrome-leading) — emits `anchor-name`/`position-anchor` + `anchor()` insets. Fallback (behind\n * `CSS.supports`): JS `getBoundingClientRect` placement recomputed on scroll/resize inside\n * `useSignalEffect`. Small by design — not a collision/flip engine (deferred).\n */\nexport function useAnchorPosition(options: UseAnchorPositionOptions): UseAnchorPositionReturn {\n const { anchorRef, floatingRef, placement = 'bottom', enabled = true } = options\n useSignals()\n const supported = supportsCssAnchor()\n const name = useId('anchor')\n const pos = useSignal<{ top: number; left: number } | null>(null)\n\n useSignalEffect(() => {\n const isEnabled = typeof enabled === 'boolean' ? enabled : enabled.value\n if (supported || !isEnabled || typeof window === 'undefined') return\n\n const update = (): void => {\n const a = anchorRef.current\n const f = floatingRef.current\n if (!a || !f) return\n pos.value = computePosition(a.getBoundingClientRect(), f.getBoundingClientRect(), placement)\n }\n update()\n window.addEventListener('scroll', update, true)\n window.addEventListener('resize', update)\n return () => {\n window.removeEventListener('scroll', update, true)\n window.removeEventListener('resize', update)\n }\n })\n\n if (supported) {\n const { side, align } = parsePlacement(placement)\n return {\n anchorStyle: { anchorName: `--${name}` } as CSSProperties,\n floatingStyle: {\n positionAnchor: `--${name}`,\n ...cssAnchorInsets(side, align),\n } as CSSProperties,\n cssAnchorSupported: true,\n }\n }\n\n const p = pos.value\n return {\n anchorStyle: {},\n floatingStyle: p\n ? { position: 'fixed', top: p.top, left: p.left }\n : { position: 'fixed', visibility: 'hidden' },\n cssAnchorSupported: false,\n }\n}\n","export { createMachine, useMachine } from './machine.ts'\nexport {\n signal,\n computed,\n effect,\n batch,\n useSignal,\n useComputed,\n useSignalEffect,\n useSignals,\n} from './signals.ts'\nexport type { Signal, ReadonlySignal } from './signals.ts'\nexport { cn, composeRefs, mergeProps } from './utils.ts'\nexport type {\n Machine,\n MachineConfig,\n StateConfig,\n ComponentMeta,\n PropMeta,\n AccessibilityMeta,\n WcagLevel,\n ExampleMeta,\n ComponentIntent,\n IntentAntiPattern,\n IntentRelated,\n IntentFlexibility,\n IntentContent,\n IntentRelationship,\n} from './types.ts'\nexport const VERSION = '0.0.0'\nexport { ErrorBoundary } from './error-boundary.tsx'\nexport type { ErrorBoundaryProps } from './error-boundary.tsx'\nexport { SuspenseBoundary } from './suspense-boundary.tsx'\nexport type { SuspenseBoundaryProps } from './suspense-boundary.tsx'\nexport { Portal } from './portal.tsx'\nexport type { PortalProps } from './portal.tsx'\nexport { VisuallyHidden } from './visually-hidden.tsx'\nexport type { VisuallyHiddenProps } from './visually-hidden.tsx'\nexport { FocusScope } from './focus-scope.tsx'\nexport type { FocusScopeProps } from './focus-scope.tsx'\nexport { Slot } from './slot.tsx'\nexport type { SlotProps } from './slot.tsx'\nexport { useControllableSignal } from './controllable.ts'\nexport type { UseControllableSignalOptions } from './controllable.ts'\nexport { useMediaQuery } from './media-query.ts'\nexport { useScrollLock } from './scroll-lock.ts'\nexport { useId } from './use-id.ts'\nexport { useClipboard } from './clipboard.ts'\nexport type { UseClipboardOptions, UseClipboardReturn } from './clipboard.ts'\nexport { DismissableLayer } from './dismissable-layer.tsx'\nexport type { DismissableLayerProps } from './dismissable-layer.tsx'\nexport { useRovingFocus } from './roving-focus.ts'\nexport type {\n UseRovingFocusOptions,\n UseRovingFocusReturn,\n RovingItemProps,\n RovingOrientation,\n} from './roving-focus.ts'\nexport { Presence } from './presence.tsx'\nexport type { PresenceProps } from './presence.tsx'\nexport { useAnchorPosition, computePosition } from './anchor.tsx'\nexport type {\n UseAnchorPositionOptions,\n UseAnchorPositionReturn,\n AnchorPlacement,\n AnchorSide,\n AnchorAlign,\n} from './anchor.tsx'\n"],"mappings":"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"}
package/dist/style.css ADDED
@@ -0,0 +1,24 @@
1
+ @layer cascade.component {
2
+ .rYIFkW_root {
3
+ clip: rect(0, 0, 0, 0);
4
+ white-space: nowrap;
5
+ border: 0;
6
+ width: 1px;
7
+ height: 1px;
8
+ margin: -1px;
9
+ padding: 0;
10
+ position: absolute;
11
+ overflow: hidden;
12
+ }
13
+
14
+ .rYIFkW_focusable:focus, .rYIFkW_focusable:focus-within {
15
+ width: auto;
16
+ height: auto;
17
+ padding: var(--cascivo-space-2) var(--cascivo-space-3);
18
+ clip: auto;
19
+ white-space: normal;
20
+ margin: 0;
21
+ position: static;
22
+ overflow: visible;
23
+ }
24
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@cascivo/core",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Micro-FSM and Preact Signals primitives powering every cascade component",
6
+ "keywords": [
7
+ "cascivo",
8
+ "css",
9
+ "design-system",
10
+ "fsm",
11
+ "preact-signals",
12
+ "react",
13
+ "signals"
14
+ ],
15
+ "homepage": "https://github.com/urbanisierung/cascivo/tree/main/packages/core#readme",
16
+ "bugs": "https://github.com/urbanisierung/cascivo/issues",
17
+ "license": "MIT",
18
+ "author": "urbanisierung",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/urbanisierung/cascivo.git",
22
+ "directory": "packages/core"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "types": "./dist/index.d.mts",
30
+ "exports": {
31
+ ".": {
32
+ "import": "./dist/index.mjs",
33
+ "types": "./dist/index.d.mts",
34
+ "default": "./dist/index.mjs"
35
+ }
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "provenance": true
40
+ },
41
+ "devDependencies": {
42
+ "@preact/signals-react": "^3",
43
+ "@testing-library/react": "^16.3.2",
44
+ "@tsdown/css": "^0.22.2",
45
+ "@types/react": "^19.2.17",
46
+ "@types/react-dom": "^19.2.3",
47
+ "jsdom": "^29",
48
+ "react": "^19.2.7",
49
+ "react-dom": "^19.2.7",
50
+ "typescript": "^5",
51
+ "vite-plus": "^0.1.24"
52
+ },
53
+ "peerDependencies": {
54
+ "@preact/signals-react": ">=2.0.0",
55
+ "react": ">=18.0.0",
56
+ "react-dom": ">=18.0.0"
57
+ },
58
+ "scripts": {
59
+ "build": "vp pack",
60
+ "check": "tsc --noEmit",
61
+ "test": "vp test",
62
+ "dev": "vp pack --watch"
63
+ }
64
+ }
package/readme.body.md ADDED
@@ -0,0 +1 @@
1
+ Core runtime for cascade — micro-FSM engine, Preact Signals integration (`useSignal`, `useComputed`, `useSignalEffect`, `useSignals`), and base utilities shared across all components.