@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 +21 -0
- package/README.md +19 -0
- package/dist/index.d.mts +372 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +561 -0
- package/dist/index.mjs.map +1 -0
- package/dist/style.css +24 -0
- package/package.json +64 -0
- package/readme.body.md +1 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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":";;;;;;AAGA,SAAgB,cACd,QACY;CACZ,OAAO;AACT;AAEA,SAAgB,WAA6B,SAAqB;CAChE,MAAM,QAAQA,YAAa,QAAQ,OAAO;CAC1C,MAAM,QAAQ,UAAkB;EAC9B,MAAM,OAAO,QAAQ,OAAO,MAAM,QAAQ,KAAK;EAC/C,IAAI,SAAS,KAAA,GAAW,MAAM,QAAQ;CACxC;CACA,OAAO,CAAC,OAAO,IAAI;AACrB;;;ACdA,SAAgB,GAAG,GAAG,SAA4D;CAChF,OAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;AAEA,SAAgB,YAAe,GAAG,MAA8C;CAC9E,QAAQ,UAAU;EAChB,KAAK,SAAS,QAAQ;GACpB,IAAI,CAAC,KAAK;GACV,IAAI,OAAO,QAAQ,YACjB,IAAI,KAAK;QAER,IAAoC,UAAU;EAEnD,CAAC;CACH;AACF;AAIA,SAAgB,WAA8C,GAAG,WAA4B;CAC3F,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,SAAS,WAClB,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,KAAK,GAC3C,IAAI,IAAI,WAAW,IAAI,KAAK,OAAO,QAAQ,cAAc,OAAO,OAAO,SAAS,YAAY;EAC1F,MAAM,WAAW,OAAO;EACxB,OAAO,QAAQ,GAAG,SAAoB;GACpC,SAAS,GAAG,IAAI;GACf,IAAsB,GAAG,IAAI;EAChC;CACF,OACE,OAAO,OAAO;CAIpB,OAAO;AACT;;;ACzBA,IAAa,gBAAb,cAAmC,UAAqC;CACtE,QAAe,EAAE,OAAO,KAAK;CAE7B,OAAO,yBAAyB,OAAqB;EACnD,OAAO,EAAE,MAAM;CACjB;CAEA,kBAAkB,OAAc,MAAiB;EAC/C,KAAK,MAAM,UAAU,OAAO,IAAI;CAClC;CAEA,cAAc;EACZ,KAAK,SAAS,EAAE,OAAO,KAAK,CAAC;CAC/B;CAEA,SAAS;EACP,MAAM,EAAE,UAAU,KAAK;EACvB,IAAI,UAAU,MAAM;GAClB,MAAM,EAAE,aAAa,KAAK;GAC1B,OAAO,OAAO,aAAa,aAAa,SAAS,OAAO,KAAK,KAAK,IAAI;EACxE;EACA,OAAO,KAAK,MAAM;CACpB;AACF;;;AC5BA,SAAgB,iBAAiB,EAAE,UAAU,YAAmC;CAC9E,OACE,oBAAC,UAAD;EAAU,UAAU,YAAY,oBAAC,QAAD;GAAM,aAAU;GAAO,cAAW;EAAW,CAAA;EAC1E;CACO,CAAA;AAEd;;;ACHA,SAAgB,OAAO,EAAE,UAAU,aAA0B;CAC3D,WAAW;CACX,MAAM,UAAU,UAAU,KAAK;CAC/B,MAAM,eAAe,OAA0C,IAAI;CAEnE,sBAAsB;EACpB,IAAI,OAAO,aAAa,aAAa;EACrC,aAAa,UAAU,aAAa,SAAS;EAC7C,QAAQ,QAAQ;EAChB,aAAa;GACX,QAAQ,QAAQ;EAClB;CACF,CAAC;CAED,IAAI,CAAC,QAAQ,SAAS,aAAa,YAAY,MAAM,OAAO;CAC5D,OAAO,aAAa,UAAU,aAAa,OAAO;AACpD;;;ACtBA,IAAA,iCAAY;CAAA,aAAA;CAAA,QAAA;AAAA;;;ACIZ,SAAgB,eAAe,EAC7B,UACA,aACA,WACA,GAAG,SACmB;CAItB,OACE,oBAAC,QAAD;EAAM,WAJQ;GAACC,+BAAO;GAAM,eAAeA,+BAAO;GAAW;EAAS,EACrE,OAAO,OAAO,EACd,KAAK,GAEiB;EAAG,GAAI;EAC3B;CACG,CAAA;AAEV;;;AClBA,MAAM,YACJ;AASF,SAAgB,WAAW,EAAE,UAAU,SAAS,cAAc,aAA8B;CAC1F,WAAW;CACX,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,mBAAmB,OAAuB,IAAI;CAEpD,sBAAsB;EACpB,MAAM,YAAY,aAAa;EAC/B,IAAI,CAAC,WAAW;EAChB,IAAI,cAAc,iBAAiB,UAAU,SAAS;EACtD,IAAI,WAEF,UADwB,cAA2B,SAC/C,GAAG,MAAM;EAEf,IAAI,CAAC,SAAS;EACd,SAAS,cAAc,GAAkB;GACvC,IAAI,EAAE,QAAQ,OAAO;GACrB,MAAM,YAAY,MAAM,KAAK,UAAW,iBAA8B,SAAS,CAAC;GAChF,IAAI,UAAU,WAAW,GAAG;GAC5B,MAAM,QAAQ,UAAU;GACxB,MAAM,OAAO,UAAU,UAAU,SAAS;GAC1C,IAAI,EAAE;QACA,SAAS,kBAAkB,OAAO;KACpC,EAAE,eAAe;KACjB,KAAK,MAAM;IACb;UAEA,IAAI,SAAS,kBAAkB,MAAM;IACnC,EAAE,eAAe;IACjB,MAAM,MAAM;GACd;EAEJ;EACA,SAAS,iBAAiB,WAAW,aAAa;EAClD,aAAa;GACX,SAAS,oBAAoB,WAAW,aAAa;GACrD,IAAI,gBAAgB,iBAAiB,mBAAmB,aACtD,iBAAiB,QAAQ,MAAM;EAEnC;CACF,CAAC;CAED,OAAO,oBAAC,OAAD;EAAK,KAAK;EAAe;CAAc,CAAA;AAChD;;;;;;;;;;;;;;;ACtBA,MAAa,OAAO,WAA+B,SAAS,KAAK,OAAO,cAAc;CACpF,MAAM,EAAE,UAAU,WAAW,eAAe,OAAO,WAAW,GAAG,cAAc;CAE/E,IAAI,CAAC,eAAe,QAAQ,KAAK,SAAS,MAAM,QAAQ,MAAM,GAC5D,MAAM,IAAI,MACR,yFACF;CAGF,MAAM,QAAQ;CACd,MAAM,aAAa,MAAM;CACzB,MAAM,SAAS,WAAoC,WAAW,UAAU;CAExE,OAAO,eAAe,GACpB,eACA,WAAW,YACb;CACA,OAAO,WAAW;EAChB,GAAK,aAA2C,CAAC;EACjD,GAAK,WAAW,YAA0C,CAAC;CAC7D;CAIA,OAAO,SAAS,YAAY,cADV,WAAW,UAAuC,MAAM,GACxB;CAElD,OAAO,aAAa,OAAO,MAAM;AACnC,CAAC;;;;;;;;;;;;;;;;ACnCD,SAAgB,sBACd,SACgC;CAChC,MAAM,EAAE,OAAO,cAAc,aAAa;CAC1C,MAAM,eAAe,UAAU,KAAA;CAE/B,MAAM,MAAM,UAAc,eAAe,QAAQ,YAAkB;CAGnE,MAAM,cAAc,OAAO,QAAQ;CACnC,YAAY,UAAU;CAGtB,IAAI,cAAc,IAAI,QAAQ;CAE9B,MAAM,YAAY,SAAkB;EAClC,IAAI,CAAC,cAAc,IAAI,QAAQ;EAC/B,YAAY,UAAU,IAAI;CAC5B;CAEA,OAAO,CAAC,KAAK,QAAQ;AACvB;;;AC5CA,SAAS,aAAa,OAAwB;CAC5C,IAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY,OAAO;CACrF,OAAO,OAAO,WAAW,KAAK,EAAE;AAClC;;;;;;AAOA,SAAgB,cAAc,OAAwC;CACpE,MAAM,UAAU,UAAU,aAAa,KAAK,CAAC;CAE7C,sBAAsB;EACpB,IAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;EAC9E,MAAM,MAAM,OAAO,WAAW,KAAK;EACnC,QAAQ,QAAQ,IAAI;EACpB,MAAM,WAAW,UAAqC;GACpD,QAAQ,QAAQ,MAAM;EACxB;EACA,IAAI,iBAAiB,UAAU,OAAO;EACtC,aAAa,IAAI,oBAAoB,UAAU,OAAO;CACxD,CAAC;CAED,OAAO;AACT;;;;;;;;ACpBA,SAAgB,cAAc,QAAyC;CACrE,sBAAsB;EAEpB,IAAI,EADa,OAAO,WAAW,YAAY,SAAS,OAAO,UAC9C,OAAO,aAAa,aAAa;EAElD,MAAM,OAAO,SAAS;EACtB,MAAM,eAAe,KAAK,MAAM;EAChC,MAAM,uBAAuB,KAAK,MAAM;EAExC,MAAM,iBAAiB,OAAO,aAAa,SAAS,gBAAgB;EACpE,KAAK,MAAM,WAAW;EACtB,IAAI,iBAAiB,GAAG;GACtB,MAAM,UAAU,OAAO,SAAS,OAAO,iBAAiB,IAAI,EAAE,kBAAkB,EAAE,KAAK;GACvF,KAAK,MAAM,mBAAmB,GAAG,UAAU,eAAe;EAC5D;EAEA,aAAa;GACX,KAAK,MAAM,WAAW;GACtB,KAAK,MAAM,mBAAmB;EAChC;CACF,CAAC;AACH;;;;;;;ACtBA,SAAgB,MAAM,SAAS,WAAmB;CAEhD,OAAO,GAAG,OAAO,GADNC,QACU,EAAE,QAAQ,MAAM,EAAE;AACzC;;;;;;;;ACSA,SAAgB,aAAa,UAA+B,CAAC,GAAuB;CAClF,MAAM,EAAE,UAAU,QAAS;CAC3B,MAAM,SAAS,UAAU,KAAK;CAC9B,MAAM,WAAW,OAA6C,IAAI;CAElE,sBAAsB;EACpB,aAAa;GACX,IAAI,SAAS,SAAS,aAAa,SAAS,OAAO;EACrD;CACF,CAAC;CAED,MAAM,OAAO,OAAO,SAAgC;EAClD,IAAI,OAAO,cAAc,eAAe,CAAC,UAAU,WAAW;EAC9D,MAAM,UAAU,UAAU,UAAU,IAAI;EACxC,OAAO,QAAQ;EACf,IAAI,SAAS,SAAS,aAAa,SAAS,OAAO;EACnD,SAAS,UAAU,iBAAiB;GAClC,OAAO,QAAQ;EACjB,GAAG,OAAO;CACZ;CAEA,OAAO;EAAE;EAAQ;CAAK;AACxB;;;AC/BA,MAAM,SAAuB,CAAC;;;;;;AAiB9B,SAAgB,iBAAiB,EAC/B,UACA,WACA,uBACA,kBAAkB,QACM;CACxB,WAAW;CACX,MAAM,UAAU,OAAuB,IAAI;CAC3C,MAAM,eAAe,OAAO,SAAS;CACrC,aAAa,UAAU;CAEvB,sBAAsB;EACpB,IAAI,OAAO,aAAa,aAAa;EACrC,MAAM,QAAoB,EAAE,eAAe,QAAQ,QAAQ;EAC3D,OAAO,KAAK,KAAK;EAGjB,MAAM,cAAuB;GAC3B,MAAM,OAAO,QAAQ;GACrB,IAAI,CAAC,MAAM,OAAO;GAClB,KAAK,MAAM,SAAS,QAAQ;IAC1B,IAAI,UAAU,OAAO;IACrB,MAAM,YAAY,MAAM,QAAQ;IAChC,IAAI,aAAa,KAAK,SAAS,SAAS,GAAG,OAAO;GACpD;GACA,OAAO;EACT;EAEA,MAAM,iBAAiB,UAA8B;GACnD,IAAI,yBAAyB,CAAC,MAAM,GAAG;GACvC,MAAM,OAAO,QAAQ;GACrB,IAAI,CAAC,MAAM;GACX,IAAI,MAAM,kBAAkB,QAAQ,CAAC,KAAK,SAAS,MAAM,MAAM,GAC7D,aAAa,UAAU;EAE3B;EACA,MAAM,aAAa,UAA+B;GAChD,IAAI,CAAC,mBAAmB,MAAM,QAAQ,YAAY,CAAC,MAAM,GAAG;GAC5D,aAAa,UAAU;EACzB;EAEA,SAAS,iBAAiB,eAAe,eAAe,IAAI;EAC5D,SAAS,iBAAiB,WAAW,SAAS;EAC9C,aAAa;GACX,SAAS,oBAAoB,eAAe,eAAe,IAAI;GAC/D,SAAS,oBAAoB,WAAW,SAAS;GACjD,MAAM,MAAM,OAAO,QAAQ,KAAK;GAChC,IAAI,QAAQ,IAAI,OAAO,OAAO,KAAK,CAAC;EACtC;CACF,CAAC;CAED,OAAO,oBAAC,OAAD;EAAK,KAAK;EAAU;CAAc,CAAA;AAC3C;;;;;;;;;AC9CA,SAAgB,eAAe,UAAiC,CAAC,GAAyB;CACxF,MAAM,EAAE,cAAc,cAAc,OAAO,OAAO,eAAe,MAAM;CACvE,MAAM,cAAc,UAAU,YAAY;CAC1C,MAAM,WAAW,OAA+B,CAAC,CAAC;CAElD,MAAM,cAAc,UAAwB;EAC1C,YAAY,QAAQ;EACpB,SAAS,QAAQ,QAAQ,MAAM;CACjC;CAEA,MAAM,iBAAiB,OAAyC,UAAwB;EACtF,MAAM,QAAQ,SAAS,QAAQ;EAC/B,IAAI,UAAU,GAAG;EACjB,MAAM,aAAa,gBAAgB;EACnC,MAAM,WAAW,gBAAgB;EACjC,IAAI,OAAO;EACX,IAAK,cAAc,MAAM,QAAQ,gBAAkB,YAAY,MAAM,QAAQ,aAC3E,OAAO,QAAQ;OACV,IAAK,cAAc,MAAM,QAAQ,eAAiB,YAAY,MAAM,QAAQ,WACjF,OAAO,QAAQ;OACV,IAAI,MAAM,QAAQ,QACvB,OAAO;OACF,IAAI,MAAM,QAAQ,OACvB,OAAO,QAAQ;OAEf;EAEF,MAAM,eAAe;EACrB,IAAI,OAAO,GAAG,OAAO,OAAO,QAAQ,IAAI;EACxC,IAAI,QAAQ,OAAO,OAAO,OAAO,IAAI,QAAQ;EAC7C,WAAW,IAAI;CACjB;CAEA,MAAM,gBAAgB,WAAoC;EACxD,UAAU,YAAY,UAAU,QAAQ,IAAI;EAC5C,YAAY,UAAU,cAAc,OAAO,KAAK;EAChD,eAAe;GACb,YAAY,QAAQ;EACtB;EACA,MAAM,OAAO;GACX,SAAS,QAAQ,SAAS;EAC5B;CACF;CAEA,OAAO;EACL;EACA;EACA,iBAAiB,UAAkB;GACjC,YAAY,QAAQ;EACtB;CACF;AACF;;;;;;;;;;AC/DA,SAAgB,SAAS,EAAE,SAAS,YAA2B;CAC7D,WAAW;CACX,MAAM,aAAa,OAAO,YAAY,YAAY,UAAU,QAAQ;CAIpE,MAAM,aAAa,UAAU,UAAU;CACvC,WAAW,QAAQ;CAEnB,MAAM,YAAY,UAAU,UAAU;CACtC,MAAM,QAAQ,UAA6B,aAAa,SAAS,QAAQ;CACzE,MAAM,UAAU,OAA2B,IAAI;CAE/C,sBAAsB;EACpB,IAAI,WAAW,OAAO;GACpB,UAAU,QAAQ;GAClB,MAAM,QAAQ;GACd;EACF;EACA,MAAM,QAAQ;EACd,MAAM,OAAO,QAAQ;EACrB,IAAI,CAAC,QAAQ,OAAO,qBAAqB,aAAa;GACpD,UAAU,QAAQ;GAClB;EACF;EACA,MAAM,SAAS,iBAAiB,IAAI;EAMpC,IAAI,EAJD,OAAO,kBAAkB,UACxB,OAAO,kBAAkB,MACzB,OAAO,sBAAsB,QAC9B,OAAO,uBAAuB,QAAQ,OAAO,uBAAuB,KACxD;GACb,UAAU,QAAQ;GAClB;EACF;EACA,MAAM,aAAa,UAAuB;GACxC,IAAI,MAAM,WAAW,MAAM,UAAU,QAAQ;EAC/C;EACA,KAAK,iBAAiB,gBAAgB,SAAS;EAC/C,KAAK,iBAAiB,iBAAiB,SAAS;EAChD,aAAa;GACX,KAAK,oBAAoB,gBAAgB,SAAS;GAClD,KAAK,oBAAoB,iBAAiB,SAAS;EACrD;CACF,CAAC;CAED,IAAI,CAAC,UAAU,OAAO,OAAO;CAC7B,IAAI,CAAC,eAAe,QAAQ,GAC1B,MAAM,IAAI,MAAM,qDAAqD;CAEvE,MAAM,QAAQ;CACd,MAAM,WAAY,MAAM,MAAM,UAAuC,MAAM;CAC3E,OAAO,aAAa,OAAO;EACzB,cAAc,MAAM;EACpB,KAAK,YAAY,WAAW,OAA2B;GACrD,QAAQ,UAAU;EACpB,CAAC;CACH,CAA4B;AAC9B;;;ACrDA,SAAS,oBAA6B;CACpC,OACE,OAAO,QAAQ,eACf,OAAO,IAAI,aAAa,cACxB,IAAI,SAAS,kBAAkB;AAEnC;AAEA,SAAS,eAAe,WAAsE;CAC5F,MAAM,CAAC,MAAM,OAAO,UAAU,MAAM,GAAG;CACvC,OAAO;EAAE;EAAM,OAAO,OAAO;CAAS;AACxC;;AAGA,SAAgB,gBACd,QAQA,UACA,WAC+B;CAC/B,MAAM,EAAE,MAAM,UAAU,eAAe,SAAS;CAChD,IAAI,MAAM;CACV,IAAI,OAAO;CACX,IAAI,SAAS,SAAS,SAAS,UAAU;EACvC,MAAM,SAAS,WAAW,OAAO,SAAS,OAAO,MAAM,SAAS;EAChE,OACE,UAAU,UACN,OAAO,OACP,UAAU,QACR,OAAO,QAAQ,SAAS,QACxB,OAAO,QAAQ,OAAO,QAAQ,SAAS,SAAS;CAC1D,OAAO;EACL,OAAO,SAAS,UAAU,OAAO,QAAQ,OAAO,OAAO,SAAS;EAChE,MACE,UAAU,UACN,OAAO,MACP,UAAU,QACR,OAAO,SAAS,SAAS,SACzB,OAAO,OAAO,OAAO,SAAS,SAAS,UAAU;CAC3D;CACA,OAAO;EAAE;EAAK;CAAK;AACrB;AAEA,SAAS,gBAAgB,MAAkB,OAAmC;CAC5E,MAAM,QAAuB;EAC3B,UAAU;EACV,iBAAiB;EACjB,kBAAkB;CACpB;CACA,IAAI,SAAS,UAAU,MAAM,MAAM;CACnC,IAAI,SAAS,OAAO,MAAM,SAAS;CACnC,IAAI,SAAS,SAAS,MAAM,OAAO;CACnC,IAAI,SAAS,QAAQ,MAAM,QAAQ;CACnC,IAAI,SAAS,SAAS,SAAS,UAAU;EACvC,MAAM,OAAO,UAAU,QAAQ,KAAA,IAAY,UAAU,UAAU,iBAAiB;EAChF,IAAI,UAAU,OAAO,MAAM,QAAQ;CACrC,OAAO;EACL,MAAM,MAAM,UAAU,QAAQ,KAAA,IAAY,UAAU,UAAU,gBAAgB;EAC9E,IAAI,UAAU,OAAO,MAAM,SAAS;CACtC;CACA,OAAO;AACT;;;;;;;AAQA,SAAgB,kBAAkB,SAA4D;CAC5F,MAAM,EAAE,WAAW,aAAa,YAAY,UAAU,UAAU,SAAS;CACzE,WAAW;CACX,MAAM,YAAY,kBAAkB;CACpC,MAAM,OAAO,MAAM,QAAQ;CAC3B,MAAM,MAAM,UAAgD,IAAI;CAEhE,sBAAsB;EACpB,MAAM,YAAY,OAAO,YAAY,YAAY,UAAU,QAAQ;EACnE,IAAI,aAAa,CAAC,aAAa,OAAO,WAAW,aAAa;EAE9D,MAAM,eAAqB;GACzB,MAAM,IAAI,UAAU;GACpB,MAAM,IAAI,YAAY;GACtB,IAAI,CAAC,KAAK,CAAC,GAAG;GACd,IAAI,QAAQ,gBAAgB,EAAE,sBAAsB,GAAG,EAAE,sBAAsB,GAAG,SAAS;EAC7F;EACA,OAAO;EACP,OAAO,iBAAiB,UAAU,QAAQ,IAAI;EAC9C,OAAO,iBAAiB,UAAU,MAAM;EACxC,aAAa;GACX,OAAO,oBAAoB,UAAU,QAAQ,IAAI;GACjD,OAAO,oBAAoB,UAAU,MAAM;EAC7C;CACF,CAAC;CAED,IAAI,WAAW;EACb,MAAM,EAAE,MAAM,UAAU,eAAe,SAAS;EAChD,OAAO;GACL,aAAa,EAAE,YAAY,KAAK,OAAO;GACvC,eAAe;IACb,gBAAgB,KAAK;IACrB,GAAG,gBAAgB,MAAM,KAAK;GAChC;GACA,oBAAoB;EACtB;CACF;CAEA,MAAM,IAAI,IAAI;CACd,OAAO;EACL,aAAa,CAAC;EACd,eAAe,IACX;GAAE,UAAU;GAAS,KAAK,EAAE;GAAK,MAAM,EAAE;EAAK,IAC9C;GAAE,UAAU;GAAS,YAAY;EAAS;EAC9C,oBAAoB;CACtB;AACF;;;ACtHA,MAAa,UAAU"}
|
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.
|