@ahrowe/ui 0.15.0 → 0.16.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/dist/esm/common/flip/flip.mjs +2 -0
- package/dist/esm/common/flip/flip.mjs.map +1 -0
- package/dist/esm/common/flip/flip.module.mjs +2 -0
- package/dist/esm/common/flip/flip.module.mjs.map +1 -0
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs +1 -1
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs.map +1 -1
- package/dist/esm/common/input/input.mjs +1 -1
- package/dist/esm/common/input/input.mjs.map +1 -1
- package/dist/esm/common/numberInput/numberInput.mjs +1 -1
- package/dist/esm/common/numberInput/numberInput.mjs.map +1 -1
- package/dist/esm/common/revealLens/revealLens.mjs +2 -0
- package/dist/esm/common/revealLens/revealLens.mjs.map +1 -0
- package/dist/esm/common/revealLens/revealLens.module.mjs +2 -0
- package/dist/esm/common/revealLens/revealLens.module.mjs.map +1 -0
- package/dist/esm/common/tilt/tilt.mjs +2 -0
- package/dist/esm/common/tilt/tilt.mjs.map +1 -0
- package/dist/esm/common/tilt/tilt.module.mjs +2 -0
- package/dist/esm/common/tilt/tilt.module.mjs.map +1 -0
- package/dist/esm/common/tilt/tiltMath.mjs +2 -0
- package/dist/esm/common/tilt/tiltMath.mjs.map +1 -0
- package/dist/esm/common/tiltGroup/tiltGroup.mjs +2 -0
- package/dist/esm/common/tiltGroup/tiltGroup.mjs.map +1 -0
- package/dist/esm/common/timeline/timeline.module.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/index.cjs +6 -6
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/package/common/configProvider/configProvider.types.d.ts +6 -0
- package/dist/types/package/common/flip/flip.d.ts +3 -0
- package/dist/types/package/common/flip/flip.types.d.ts +42 -0
- package/dist/types/package/common/flip/index.d.ts +2 -0
- package/dist/types/package/common/revealLens/index.d.ts +2 -0
- package/dist/types/package/common/revealLens/revealLens.d.ts +4 -0
- package/dist/types/package/common/revealLens/revealLens.types.d.ts +24 -0
- package/dist/types/package/common/tilt/index.d.ts +4 -0
- package/dist/types/package/common/tilt/tilt.d.ts +3 -0
- package/dist/types/package/common/tilt/tilt.types.d.ts +38 -0
- package/dist/types/package/common/tilt/tiltMath.d.ts +24 -0
- package/dist/types/package/common/tiltGroup/index.d.ts +3 -0
- package/dist/types/package/common/tiltGroup/tiltGroup.d.ts +6 -0
- package/dist/types/package/common/tiltGroup/tiltGroup.types.d.ts +16 -0
- package/dist/types/package/index.d.ts +8 -0
- package/docs/CLAUDE.md +3 -0
- package/docs/Dropdown.md +1 -1
- package/docs/Flip.md +97 -0
- package/docs/FloatingMenu.md +2 -0
- package/docs/Input.md +25 -2
- package/docs/InputDropdown.md +1 -1
- package/docs/RevealLens.md +67 -0
- package/docs/Tilt.md +124 -0
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
|
|
3
|
+
import { TiltAxisLock } from './tiltMath';
|
|
4
|
+
export type TiltTrackingMode = 'self' | 'group';
|
|
5
|
+
export type TiltSlots = 'root' | 'perspective' | 'surface';
|
|
6
|
+
export interface TiltHandle {
|
|
7
|
+
/** Directly drives the tilt rotation, clamped to `maxTilt`/`maxTiltX`/`maxTiltY`. For custom choreography (e.g. shuffling). */
|
|
8
|
+
setTilt: (rotateX: number, rotateY: number) => void;
|
|
9
|
+
/** Returns to flat. */
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface TiltProps extends HtmlProps {
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
/** Shared clamp for both tilt axes, in degrees. @default 15 */
|
|
15
|
+
maxTilt?: number;
|
|
16
|
+
/** Overrides `maxTilt` for the X axis (tilts top/bottom edges, driven by vertical cursor position). */
|
|
17
|
+
maxTiltX?: number;
|
|
18
|
+
/** Overrides `maxTilt` for the Y axis (tilts left/right edges, driven by horizontal cursor position). */
|
|
19
|
+
maxTiltY?: number;
|
|
20
|
+
/** CSS `perspective` in px, applied to the perspective wrapper. @default 800 */
|
|
21
|
+
perspective?: number;
|
|
22
|
+
/** Easing curve applied to the normalized cursor offset (range roughly [-1, 1]) before scaling by `maxTilt`. @default defaultTiltEasing */
|
|
23
|
+
easing?: (t: number) => number;
|
|
24
|
+
/** Disables tilt on one axis entirely. @default 'none' */
|
|
25
|
+
lockAxis?: TiltAxisLock;
|
|
26
|
+
/** Cosmetic `scale()` applied together with the tilt while tracking. @default 1 */
|
|
27
|
+
scaleOnHover?: number;
|
|
28
|
+
/** `'self'` tracks the pointer over this element's own bounding box; `'group'` reads a shared pointer position from an ancestor `TiltGroup`, falling back to `'self'` if none exists. @default 'self' */
|
|
29
|
+
trackingMode?: TiltTrackingMode;
|
|
30
|
+
/** Disables tilt tracking entirely. @default false */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Fires on every applied tilt update. */
|
|
33
|
+
onTiltChange?: (rotateX: number, rotateY: number) => void;
|
|
34
|
+
className?: string;
|
|
35
|
+
style?: CSSProperties;
|
|
36
|
+
classNames?: SlotClassNames<TiltSlots>;
|
|
37
|
+
styles?: SlotStyles<TiltSlots>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type TiltAxisLock = 'x' | 'y' | 'none';
|
|
2
|
+
export interface ComputeTiltOptions {
|
|
3
|
+
maxTiltX: number;
|
|
4
|
+
maxTiltY: number;
|
|
5
|
+
easing: (t: number) => number;
|
|
6
|
+
lockAxis: TiltAxisLock;
|
|
7
|
+
}
|
|
8
|
+
export interface TiltResult {
|
|
9
|
+
rotateX: number;
|
|
10
|
+
rotateY: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Eases faster near the edges (|t| close to 1) and flatter near the center (|t| close to 0),
|
|
14
|
+
* while staying odd-symmetric so tilt direction always matches cursor direction.
|
|
15
|
+
*/
|
|
16
|
+
export declare function defaultTiltEasing(t: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* Computes the tilt rotation for a pointer at (x, y) relative to `rect`'s top-left corner.
|
|
19
|
+
* Vertical cursor position drives rotateX (tilts top/bottom edges), horizontal drives rotateY.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeTilt(x: number, y: number, rect: {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}, { maxTiltX, maxTiltY, easing, lockAxis }: ComputeTiltOptions): TiltResult;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { TiltGroupContextValue, TiltGroupProps } from './tiltGroup.types';
|
|
3
|
+
export declare const TiltGroupContext: import('react').Context<TiltGroupContextValue | null>;
|
|
4
|
+
export declare function useTiltGroup(): TiltGroupContextValue | null;
|
|
5
|
+
declare function TiltGroup({ children, className, style, ...rest }: TiltGroupProps): ReactElement;
|
|
6
|
+
export default TiltGroup;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { HtmlProps } from '../types/slots.types';
|
|
3
|
+
export interface TiltGroupContextValue {
|
|
4
|
+
/** Last known pointer position, in coordinates relative to the group container's top-left corner. `null` once the pointer has left. */
|
|
5
|
+
pointer: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
} | null;
|
|
9
|
+
/** The group container's bounding rect, refreshed on pointer enter/move. */
|
|
10
|
+
rect: DOMRect | null;
|
|
11
|
+
}
|
|
12
|
+
export interface TiltGroupProps extends HtmlProps {
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: CSSProperties;
|
|
16
|
+
}
|
|
@@ -51,6 +51,8 @@ export { default as ErrorBoundary } from './common/errorBoundary';
|
|
|
51
51
|
export * from './common/errorBoundary';
|
|
52
52
|
export { default as Fab } from './common/fab';
|
|
53
53
|
export * from './common/fab';
|
|
54
|
+
export { default as Flip } from './common/flip';
|
|
55
|
+
export * from './common/flip';
|
|
54
56
|
export { default as FloatingMenu } from './common/floatingMenu';
|
|
55
57
|
export * from './common/floatingMenu';
|
|
56
58
|
export { default as IconPicker } from './common/iconPicker';
|
|
@@ -86,6 +88,8 @@ export { default as RadioGroup } from './common/radioGroup';
|
|
|
86
88
|
export * from './common/radioGroup';
|
|
87
89
|
export { default as Rating } from './common/rating';
|
|
88
90
|
export * from './common/rating';
|
|
91
|
+
export { default as RevealLens } from './common/revealLens';
|
|
92
|
+
export * from './common/revealLens';
|
|
89
93
|
export { default as Ripple } from './common/ripple';
|
|
90
94
|
export * from './common/ripple';
|
|
91
95
|
export { default as RoomDrawer } from './common/roomDrawer';
|
|
@@ -117,6 +121,10 @@ export * from './common/textarea';
|
|
|
117
121
|
export { default as ThemeProvider } from './common/themeProvider';
|
|
118
122
|
export * from './common/themeProvider';
|
|
119
123
|
export { useTheme } from './common/themeProvider/useTheme';
|
|
124
|
+
export { default as Tilt } from './common/tilt';
|
|
125
|
+
export * from './common/tilt';
|
|
126
|
+
export { default as TiltGroup } from './common/tiltGroup';
|
|
127
|
+
export * from './common/tiltGroup';
|
|
120
128
|
export { default as TimeInput } from './common/timeInput';
|
|
121
129
|
export * from './common/timeInput';
|
|
122
130
|
export { default as Timeline } from './common/timeline';
|
package/docs/CLAUDE.md
CHANGED
|
@@ -119,6 +119,7 @@ Slot keys per component are documented in each component's doc file below.
|
|
|
119
119
|
@EmptyState.md
|
|
120
120
|
@ErrorBoundary.md
|
|
121
121
|
@Fab.md
|
|
122
|
+
@Flip.md
|
|
122
123
|
@FloatingMenu.md
|
|
123
124
|
@FormValidator.md
|
|
124
125
|
@FormValidatorGroup.md
|
|
@@ -139,6 +140,7 @@ Slot keys per component are documented in each component's doc file below.
|
|
|
139
140
|
@ProgressBar.md
|
|
140
141
|
@RadioGroup.md
|
|
141
142
|
@Rating.md
|
|
143
|
+
@RevealLens.md
|
|
142
144
|
@Ripple.md
|
|
143
145
|
@RoomDrawer.md
|
|
144
146
|
@RoomViewer.md
|
|
@@ -154,6 +156,7 @@ Slot keys per component are documented in each component's doc file below.
|
|
|
154
156
|
@TabHeader.md
|
|
155
157
|
@Textarea.md
|
|
156
158
|
@ThemeProvider.md
|
|
159
|
+
@Tilt.md
|
|
157
160
|
@TimeInput.md
|
|
158
161
|
@Timeline.md
|
|
159
162
|
@Toast.md
|
package/docs/Dropdown.md
CHANGED
|
@@ -56,4 +56,4 @@ const statusValidator = new FormValidator('', [Validators.required()]);
|
|
|
56
56
|
| `useMatLabelStyle` | `boolean` | Material-style floating label |
|
|
57
57
|
| `alwaysFloatLabel` | `boolean` | Keep the material label floated in the border notch even when nothing is selected, so an empty dropdown reads as blank instead of showing the label as if it were the selected value. Only applies with `useMatLabelStyle` (default) |
|
|
58
58
|
|
|
59
|
-
The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). It flips to open upward automatically when there's no room below, and hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view.
|
|
59
|
+
The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). It flips to open upward automatically when there's no room below, and hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view. It also re-measures when its own content changes size while open, so a list that shrinks (e.g. filtered down to fewer entries) stays anchored to the trigger instead of hanging above it.
|
package/docs/Flip.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Flip
|
|
2
|
+
|
|
3
|
+
**When to use:** Flip a front/back pair of content 180°, like a physical playing card. Works completely standalone (it establishes its own `perspective`), or nested inside `Tilt` (with `perspective="none"`, since `Tilt` already establishes one) for a flippable, tilting card. Use it for a reveal-the-back interaction, a card in a hand of cards, or any two-sided toggle that should feel like a physical flip rather than a cross-fade.
|
|
4
|
+
|
|
5
|
+
**Import:** `import { Flip } from '@ahrowe/ui'`
|
|
6
|
+
**Types:** `import type { FlipProps, FlipHandle } from '@ahrowe/ui'`
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { Flip } from '@ahrowe/ui';
|
|
11
|
+
|
|
12
|
+
// Controlled, click to flip
|
|
13
|
+
const [flipped, setFlipped] = useState(false);
|
|
14
|
+
|
|
15
|
+
<Flip
|
|
16
|
+
style={{ width: 220, height: 280 }}
|
|
17
|
+
flipped={flipped}
|
|
18
|
+
flipOnClick
|
|
19
|
+
onFlip={setFlipped}
|
|
20
|
+
front={<div>Click to flip</div>}
|
|
21
|
+
back={<div>Here's the back!</div>}
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Direction:** `flipAxis` picks which edge the flip hinges on (`'y'`, the default, hinges on the left/right edges; `'x'` hinges on the top/bottom edges). `reverse` flips the rotation sign on that same axis (+180deg vs -180deg), so the flip sweeps the opposite way without changing which edge it hinges on, useful for a hand of cards where neighboring cards should visually sweep toward each other rather than all sweeping the same direction:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<Flip flipAxis="y" front={<Face />} back={<Back />} /> {/* sweeps left-right */}
|
|
29
|
+
<Flip flipAxis="y" reverse front={<Face />} back={<Back />} /> {/* sweeps right-left */}
|
|
30
|
+
<Flip flipAxis="x" front={<Face />} back={<Back />} /> {/* sweeps top-bottom */}
|
|
31
|
+
<Flip flipAxis="x" reverse front={<Face />} back={<Back />} /> {/* sweeps bottom-top */}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Imperative control (custom animation, e.g. a shuffle):**
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { useRef } from 'react';
|
|
38
|
+
import { Flip } from '@ahrowe/ui';
|
|
39
|
+
import type { FlipHandle } from '@ahrowe/ui';
|
|
40
|
+
|
|
41
|
+
const cardRef = useRef<FlipHandle>(null);
|
|
42
|
+
|
|
43
|
+
<Flip ref={cardRef} style={{ width: 220, height: 280 }} front={<Face />} back={<Back />} />
|
|
44
|
+
|
|
45
|
+
cardRef.current?.flip(); // toggles flip (uncontrolled instances only)
|
|
46
|
+
cardRef.current?.setFlipped(true); // explicit set (uncontrolled instances only)
|
|
47
|
+
cardRef.current?.reset(); // back to unflipped (front-facing)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`flip()`/`setFlipped()` are no-ops (with a dev-only console warning) on a **controlled** instance (one with a `flipped` prop); update the `flipped` prop instead. `onFlip` fires only for a user-initiated flip (click or ref call), never for an externally-driven `flipped` prop change.
|
|
51
|
+
|
|
52
|
+
**Composing with `Tilt`:**
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { Tilt, Flip } from '@ahrowe/ui';
|
|
56
|
+
|
|
57
|
+
<Tilt style={{ width: 220, height: 280 }}>
|
|
58
|
+
<Flip style={{ width: '100%', height: '100%' }} perspective="none" front={<Face />} back={<Back />} />
|
|
59
|
+
</Tilt>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`Tilt` already establishes a `perspective` on its own wrapper, so pass `perspective="none"` to `Flip` when nesting it there to avoid two stacked perspective contexts. `Flip` doesn't inherit `Tilt`'s size on its own (it has no size of its own until you give it one), so size it to `100%` to fill `Tilt`'s surface.
|
|
63
|
+
|
|
64
|
+
**Sizing:** `Flip` doesn't impose a size of its own — give the root an explicit size via `style`/`className` (e.g. `style={{ width: 220, height: 280 }}`) whenever `front`/`back` are sized in percentages (`width: '100%'`) and need something to size against. Left unset, `Flip` sizes itself to fit its content instead of collapsing to 0×0, taking the larger of `front`/`back`'s natural size (both faces are stacked in the same CSS grid cell, so the wider/taller one sets the size for both, even though only the front is visible at rest).
|
|
65
|
+
|
|
66
|
+
**Motion:** the flip always animates over `flipDuration`, whether triggered by a click, a controlled prop change, or the ref API. Under `prefers-reduced-motion: reduce`, it flips instantly instead. `onFlipEnd` fires once the flip settles on its new face — after the transition completes normally, or synchronously under reduced motion, since no transition runs to wait for.
|
|
67
|
+
|
|
68
|
+
**Keyboard accessibility:** when `flipOnClick` is set, the root gets `role="button"`, `tabIndex={0}`, and `aria-pressed` reflecting the current flip state, and Enter/Space both toggle the flip the same way a click does. None of this is applied when `flipOnClick` is `false`, since the element isn't interactive.
|
|
69
|
+
|
|
70
|
+
**Key props:**
|
|
71
|
+
|
|
72
|
+
| Prop | Type | Description |
|
|
73
|
+
|------|------|-------------|
|
|
74
|
+
| `front` | `ReactNode` | Front-face content (required) |
|
|
75
|
+
| `back` | `ReactNode` | Back-face content (required) |
|
|
76
|
+
| `flipped` | `boolean` | Controlled flip state |
|
|
77
|
+
| `defaultFlipped` | `boolean` | Initial flip state when uncontrolled (default `false`) |
|
|
78
|
+
| `flipOnClick` | `boolean` | Clicking toggles flip (uncontrolled) or calls `onFlip` (controlled) (default `false`) |
|
|
79
|
+
| `flipAxis` | `'x' \| 'y'` | Which edge the flip hinges on (default `'y'`) |
|
|
80
|
+
| `reverse` | `boolean` | Flips the rotation sign on `flipAxis`, so the sweep direction reverses (default `false`) |
|
|
81
|
+
| `flipDuration` | `number` | Flip transition duration in ms (default `600`) |
|
|
82
|
+
| `perspective` | `number \| 'none'` | CSS `perspective` in px on the perspective wrapper; pass `'none'` when nesting inside something that already establishes one, e.g. `Tilt` (default `800`) |
|
|
83
|
+
| `disabled` | `boolean` | Disables `flipOnClick`; programmatic flips via prop or ref still work (default `false`) |
|
|
84
|
+
| `onFlip` | `(flipped: boolean) => void` | Fires for a user-initiated flip (click or ref call), not fired for an externally-driven `flipped` prop change |
|
|
85
|
+
| `onFlipEnd` | `(flipped: boolean) => void` | Fires once the flip transition finishes settling on the new face, for any trigger. Fires synchronously under `prefers-reduced-motion: reduce` |
|
|
86
|
+
|
|
87
|
+
**`FlipHandle`** (via `ref`):
|
|
88
|
+
|
|
89
|
+
| Method | Description |
|
|
90
|
+
|--------|-------------|
|
|
91
|
+
| `flip()` | Toggles the flip state (uncontrolled instances only) |
|
|
92
|
+
| `setFlipped(value)` | Explicitly sets the flip state (uncontrolled instances only) |
|
|
93
|
+
| `reset()` | Returns to unflipped (front-facing) |
|
|
94
|
+
|
|
95
|
+
**Global defaults:** `Flip` adopts `ConfigProvider`, e.g. `defaultProps={{ Flip: { flipDuration: 400 } }}`. See [ConfigProvider.md](ConfigProvider.md).
|
|
96
|
+
|
|
97
|
+
**Slots:** `root` `perspective` `surface` `front` `back`
|
package/docs/FloatingMenu.md
CHANGED
|
@@ -52,6 +52,8 @@ import { FloatingMenu, Align } from '@ahrowe/ui';
|
|
|
52
52
|
|
|
53
53
|
**Slots:** `root` `trigger` `menu` `menuContainer`
|
|
54
54
|
|
|
55
|
+
**Positioning:** the menu is portaled and tracks the trigger across scroll and resize, flipping above it when there's no room below. It re-measures whenever `content` changes size while open, so a menu holding a list that grows or shrinks (filtering, async loading) stays anchored to the trigger and re-evaluates whether it still needs to open upward.
|
|
56
|
+
|
|
55
57
|
**Closing behaviour:** by default, clicking anywhere in `content` closes the menu — including inside a nested overlay that renders through its own portal (e.g. a `Dropdown` or another `FloatingMenu` used inside `content`), even though that overlay's DOM lives outside `content`'s own subtree. Re-clicking the trigger while open is a clean toggle: it closes the menu (unless `dontCloseOnChildClick` is set, in which case it's a no-op — the trigger owns its own open/close entirely, so it never fights with an outside-click check). Set `dontCloseOnChildClick` when `content` needs several interactions before the user is done (a multi-checkbox toggle, a color picker's slider, a calendar) — the consumer is then responsible for closing explicitly, e.g. calling `onOpenChange(false)` from the handler that reacts to a final selection.
|
|
56
58
|
|
|
57
59
|
**Keyboard:** the menu is portaled to the end of the DOM, so Tab can't reach it in visual order on its own. When `content` has real focusable elements, Escape closes the menu and returns focus to the trigger; Tab past the last focusable element (or Shift+Tab past the first) closes the menu and continues focus as if it sat right after the trigger. This doesn't include auto-focusing the first element on open — content ranges from menus to live controls (e.g. `ColorPicker`'s hue slider), where grabbing focus on open would let a stray arrow-key press change a value the user never touched. A consumer that wants that (like `SplitButton` focusing its first enabled action) implements it itself.
|
package/docs/Input.md
CHANGED
|
@@ -29,9 +29,12 @@ const emailValidator = new FormValidator('', [Validators.required(), Validators.
|
|
|
29
29
|
// Multiline (textarea)
|
|
30
30
|
<Input label="Notes" multiline rows={4} value={notes} onChange={setNotes} />
|
|
31
31
|
|
|
32
|
-
// Phone
|
|
32
|
+
// Phone — sets type="tel" and the numeric dial-pad keyboard, no formatting of its own
|
|
33
33
|
<Input label="Phone" type={InputType.Phone} formValidator={phoneValidator} />
|
|
34
34
|
|
|
35
|
+
// Formatted while typing, via parse — an IBAN grouped in blocks of four
|
|
36
|
+
<Input label="IBAN" parse={formatIban} value={iban} onChange={setIban} />
|
|
37
|
+
|
|
35
38
|
// With suffix
|
|
36
39
|
<Input label="Amount" suffix="€" value={amount} onChange={setAmount} />
|
|
37
40
|
|
|
@@ -65,6 +68,24 @@ const emailValidator = new FormValidator('', [Validators.required(), Validators.
|
|
|
65
68
|
|
|
66
69
|
**Mobile keyboard layout:** `Email`, `Phone`, and `Search` each set the matching `inputMode` (`email` / `tel` / `search`), so mobile browsers show the right on-screen keyboard — an `@`-and-`.com` layout for email, a numeric dial pad for phone, a "Go"/search action key for search. `Email`'s native `type` is deliberately left as `text` rather than `email`, to avoid the browser's own "please match the requested format" validation UI fighting the `formValidator`-driven error tooltip this component already shows — `inputMode="email"` alone is enough to get the correct keyboard. Pass `inputMode` directly to override the computed default for any other case.
|
|
67
70
|
|
|
71
|
+
**Formatting while typing (`parse`):** `parse` runs on every change, before the value reaches `onChange` and `formValidator`, and its return value is what gets stored and rendered. That makes it the hook for keeping a field in a canonical shape as the user types, rather than fixing it up on blur or submit:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// Uppercase, drop anything that isn't a letter or digit, group in blocks of four,
|
|
75
|
+
// cap at the 34-character IBAN maximum
|
|
76
|
+
const formatIban = (value: string) =>
|
|
77
|
+
value
|
|
78
|
+
.replace(/[^a-zA-Z0-9]/g, '')
|
|
79
|
+
.toUpperCase()
|
|
80
|
+
.slice(0, 34)
|
|
81
|
+
.replace(/(.{4})/g, '$1 ')
|
|
82
|
+
.trim();
|
|
83
|
+
|
|
84
|
+
<Input label="IBAN" type={InputType.Iban} parse={formatIban} value={iban} onChange={setIban} />
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The stored value is the formatted one, spaces included, so strip them before a length check or checksum (`value.replace(/\s/g, '')`). Two limits worth knowing: inserting characters in the middle of an already-formatted value can move the caret to the end, since the whole value is rewritten (append-only typing is unaffected), and `parse` can't reject a keystroke, only rewrite the result. For a fixed-width field where per-position rules and caret handling matter more (a date, a card number), drive a mask library through `customInput` instead.
|
|
88
|
+
|
|
68
89
|
**Key props:**
|
|
69
90
|
|
|
70
91
|
| Prop | Type | Description |
|
|
@@ -94,6 +115,8 @@ const emailValidator = new FormValidator('', [Validators.required(), Validators.
|
|
|
94
115
|
| `readOnly` | `boolean` | |
|
|
95
116
|
| `autoFocus` | `boolean` | |
|
|
96
117
|
| `placeholder` | `string` | |
|
|
97
|
-
| `parse` | `(value: string) => string` | Transform value before onChange |
|
|
118
|
+
| `parse` | `(value: string) => string` | Transform the value on every change, before `onChange`/`formValidator` — also what gets rendered (see above) |
|
|
119
|
+
| `isUppercase` | `boolean` | Uppercases the field's rendering (CSS only — the stored value is unchanged; use `parse` to uppercase the value itself) |
|
|
120
|
+
| `customInput` | `ReactElement` | Element rendered in place of the internal `<input>`, cloned with all of Input's props. The escape hatch for a mask library; note `autoFocus` and `autoResize` stop working, since Input cannot attach its ref |
|
|
98
121
|
|
|
99
122
|
**Slots:** `label` `container` `input` `suffix` `upperRightLabel` `fieldset`
|
package/docs/InputDropdown.md
CHANGED
|
@@ -64,4 +64,4 @@ interface InputDropdownItem {
|
|
|
64
64
|
|
|
65
65
|
**Slots:** `root` `dropdown` `item`
|
|
66
66
|
|
|
67
|
-
The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). It flips to open upward automatically when there's no room below, and hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view.
|
|
67
|
+
The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). It flips to open upward automatically when there's no room below, and hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view. It also re-measures when its own content changes size while open, so a list that shrinks (e.g. filtered down to fewer entries) stays anchored to the trigger instead of hanging above it.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# RevealLens
|
|
2
|
+
|
|
3
|
+
**When to use:** A two-layer reveal/magnifier effect — an `overlay` covers a `background` by default, and moving the pointer over the component uncovers the `background` in an area around the cursor. `background`/`overlay` are plain `ReactNode`s, so either can be an image, a solid colour block, or any other component (even an animated one) — swap two images on hover, reveal a crisp image over a blurred version, or magnify any custom content.
|
|
4
|
+
|
|
5
|
+
**Import:** `import { RevealLens } from '@ahrowe/ui'`
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { RevealLens } from '@ahrowe/ui';
|
|
9
|
+
|
|
10
|
+
// Swap two images on hover
|
|
11
|
+
<RevealLens
|
|
12
|
+
style={{ width: 480, height: 300 }}
|
|
13
|
+
background={<img src="/photo-alt.jpg" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
|
|
14
|
+
overlay={<img src="/photo.jpg" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
// Crisp image revealed over a blurred version — a magnifying-glass effect
|
|
18
|
+
<RevealLens
|
|
19
|
+
style={{ width: 480, height: 300 }}
|
|
20
|
+
background={<img src="/photo.jpg" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
|
|
21
|
+
overlay={
|
|
22
|
+
<img
|
|
23
|
+
src="/photo.jpg"
|
|
24
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover', filter: 'blur(8px)' }}
|
|
25
|
+
/>
|
|
26
|
+
}
|
|
27
|
+
radius={90}
|
|
28
|
+
feather={40}
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
// Any component as either layer
|
|
32
|
+
<RevealLens
|
|
33
|
+
style={{ width: 480, height: 300 }}
|
|
34
|
+
background={<AnimatedLogo />}
|
|
35
|
+
overlay={<div style={{ background: 'var(--primary-color)' }}>Hover to reveal</div>}
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
// Hard edge (no soft feather)
|
|
39
|
+
<RevealLens background={<Crisp />} overlay={<Faded />} radius={80} feather={0} />
|
|
40
|
+
|
|
41
|
+
// A fully custom CSS mask instead of the built-in circle
|
|
42
|
+
<RevealLens
|
|
43
|
+
background={<Crisp />}
|
|
44
|
+
overlay={<Faded />}
|
|
45
|
+
maskImage="linear-gradient(to right, transparent, white)"
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Sizing:** `background` and `overlay` are stacked in the same CSS grid cell rather than given a size of their own, so give the root an explicit size via `style`/`className` (e.g. `style={{ width: 480, height: 300 }}`) whenever they're sized in percentages (`width: '100%'`, as in the examples above) and need something to size against. Left unset, `RevealLens` sizes itself to fit its content, taking the larger of `background`/`overlay`'s natural size.
|
|
50
|
+
|
|
51
|
+
**Touch:** built on Pointer Events, so it works the same for mouse and touch — dragging a finger across the component moves the reveal like a magnifier. The reveal snaps back to the overlay when the pointer/touch ends or leaves.
|
|
52
|
+
|
|
53
|
+
**Key props:**
|
|
54
|
+
|
|
55
|
+
| Prop | Type | Description |
|
|
56
|
+
|------|------|-------------|
|
|
57
|
+
| `background` | `ReactNode` | Bottom layer, revealed around the pointer (required) |
|
|
58
|
+
| `overlay` | `ReactNode` | Top layer, covers `background` by default (required) |
|
|
59
|
+
| `radius` | `number` | Reveal radius in px (default `120`) |
|
|
60
|
+
| `feather` | `number` | Soft edge width in px; `0` gives a hard edge (default `24`) |
|
|
61
|
+
| `shape` | `'circle' \| 'custom'` | Documents the reveal shape being used (default `'circle'`); the actual mask is a circle unless `maskImage` is set |
|
|
62
|
+
| `maskImage` | `string` | Raw CSS `mask-image` value, used verbatim instead of the built-in circle gradient — the escape hatch for a non-circular reveal |
|
|
63
|
+
| `disabled` | `boolean` | Disables pointer tracking — `overlay` stays fully visible (default `false`) |
|
|
64
|
+
|
|
65
|
+
**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ RevealLens: { radius: 150 } }}`. See [ConfigProvider.md](ConfigProvider.md).
|
|
66
|
+
|
|
67
|
+
**Slots:** `root` `background` `overlay`
|
package/docs/Tilt.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Tilt
|
|
2
|
+
|
|
3
|
+
**When to use:** Wrap content that should tilt toward the cursor in 3D. Pointer position drives a `rotateX`/`rotateY` tilt that flattens out near the center and ramps up (with an eased, non-linear curve) toward the edges. Use it for clean UI polish, like a row of pricing/plan cards that each tilt independently on hover, or as a low-level primitive for custom animation, like a hand of cards that all react to one shared cursor. Compose with `Flip` for a card that both tilts and flips.
|
|
4
|
+
|
|
5
|
+
**Import:** `import { Tilt, TiltGroup } from '@ahrowe/ui'`
|
|
6
|
+
**Types:** `import type { TiltProps, TiltHandle, TiltGroupProps } from '@ahrowe/ui'`
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { Tilt } from '@ahrowe/ui';
|
|
10
|
+
|
|
11
|
+
// Basic hover tilt
|
|
12
|
+
<Tilt style={{ width: 220, height: 280 }}>
|
|
13
|
+
<div>Hover me</div>
|
|
14
|
+
</Tilt>
|
|
15
|
+
|
|
16
|
+
// Limit the tilt angle, and tilt less on one axis than the other, good for a row
|
|
17
|
+
// of cards that should feel subtle rather than dramatic
|
|
18
|
+
<Tilt style={{ width: 180, height: 240 }} maxTilt={10} maxTiltX={4}>
|
|
19
|
+
<PlanCard />
|
|
20
|
+
</Tilt>
|
|
21
|
+
|
|
22
|
+
// Lock tilt to a single axis
|
|
23
|
+
<Tilt style={{ width: 220, height: 280 }} lockAxis="y">
|
|
24
|
+
<div>Only tilts left/right</div>
|
|
25
|
+
</Tilt>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Imperative control (custom animation):**
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { useRef } from 'react';
|
|
32
|
+
import { Tilt } from '@ahrowe/ui';
|
|
33
|
+
import type { TiltHandle } from '@ahrowe/ui';
|
|
34
|
+
|
|
35
|
+
const tiltRef = useRef<TiltHandle>(null);
|
|
36
|
+
|
|
37
|
+
<Tilt ref={tiltRef} style={{ width: 220, height: 280 }}>
|
|
38
|
+
<div>Content</div>
|
|
39
|
+
</Tilt>
|
|
40
|
+
|
|
41
|
+
tiltRef.current?.setTilt(8, -12); // drive rotation directly, clamped to maxTilt/maxTiltX/maxTiltY
|
|
42
|
+
tiltRef.current?.reset(); // back to flat
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Tracking a shared cursor with `TiltGroup` (a hand of cards):**
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { Tilt, TiltGroup } from '@ahrowe/ui';
|
|
49
|
+
|
|
50
|
+
<TiltGroup style={{ position: 'relative', width: 480, height: 220 }}>
|
|
51
|
+
{cards.map((card, i) => (
|
|
52
|
+
<div key={card.id} style={{ position: 'absolute', left: '50%', transform: `rotate(${fanAngle(i)}deg)` }}>
|
|
53
|
+
<Tilt trackingMode="group">
|
|
54
|
+
<CardFace card={card} />
|
|
55
|
+
</Tilt>
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</TiltGroup>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
By default (`trackingMode="self"`), a `Tilt` only tilts while the pointer is over its own bounding box. With `trackingMode="group"`, it instead reads the cursor position from an ancestor `TiltGroup` and reacts to it relative to *its own* rect, so a whole hand of cards responds to one shared cursor, each computing its own tilt from the same pointer position, the way light falling across a fanned hand of physical cards would. `TiltGroup` renders a single plain `div`; give it its own size/position via `style`/`className`. If `trackingMode="group"` is set with no ancestor `TiltGroup`, it falls back to `"self"` tracking and logs a one-time dev warning.
|
|
62
|
+
|
|
63
|
+
**Composing with `Flip` (a tilting, flippable card):**
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { Tilt, Flip } from '@ahrowe/ui';
|
|
67
|
+
|
|
68
|
+
<Tilt style={{ width: 220, height: 280 }}>
|
|
69
|
+
<Flip style={{ width: '100%', height: '100%' }} perspective="none" front={<Face />} back={<Back />} />
|
|
70
|
+
</Tilt>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`Flip` sizes itself to fit `front`/`back`'s content by default, which usually isn't what you want nested inside `Tilt`, so size it to `100%` to fill `Tilt`'s surface instead. See [Flip.md](Flip.md) for the rest of `Flip`'s API.
|
|
74
|
+
|
|
75
|
+
**Sizing:** `Tilt` doesn't impose a size. Give the root an explicit size via `style`/`className` (e.g. `style={{ width: 220, height: 280 }}`).
|
|
76
|
+
|
|
77
|
+
**Put the card's visual surface on the content, not the root:** only the `surface` slot actually receives the 3D rotation, the root just establishes position and size. A background, shadow, or border-radius set via `className`/`style` on `Tilt` itself stays flat while the content tilts inside it. Put that styling on `children` (or target the `surface` slot via `classNames`/`styles`) so the whole visible surface tilts as one:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// Wrong: the box stays flat, only the text inside it tilts
|
|
81
|
+
<Tilt className={styles.card} style={{ width: 220, height: 280 }}>Hover me</Tilt>
|
|
82
|
+
|
|
83
|
+
// Right: the visual surface is on the content that actually rotates
|
|
84
|
+
<Tilt style={{ width: 220, height: 280 }}>
|
|
85
|
+
<div className={styles.cardFace}>Hover me</div>
|
|
86
|
+
</Tilt>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Tilt tuning:** `maxTilt` (and the per-axis `maxTiltX`/`maxTiltY` overrides) clamp the rotation in degrees; the normalized cursor offset is passed through an easing curve (default export `defaultTiltEasing`) before being scaled by the clamp, so tilt stays flatter near the center and ramps up faster near the edges rather than scaling linearly. Pass your own `easing` function (`(t: number) => number`, `t` roughly in `[-1, 1]`) for a different feel.
|
|
90
|
+
|
|
91
|
+
**Motion:** tilt tracking is throttled to at most one update per animation frame, and eases in smoothly on entry/leave via a JS-driven catch-up (not a CSS transition, since CSS transitions can't gracefully follow a continuously-moving target). Under `prefers-reduced-motion: reduce`, live tilt tracking is disabled entirely and the element stays flat.
|
|
92
|
+
|
|
93
|
+
**Key props:**
|
|
94
|
+
|
|
95
|
+
| Prop | Type | Description |
|
|
96
|
+
|------|------|-------------|
|
|
97
|
+
| `children` | `ReactNode` | Content that tilts |
|
|
98
|
+
| `maxTilt` | `number` | Shared clamp for both axes, in degrees (default `15`) |
|
|
99
|
+
| `maxTiltX` / `maxTiltY` | `number` | Per-axis override for `maxTilt` |
|
|
100
|
+
| `perspective` | `number` | CSS `perspective` in px on the perspective wrapper (default `800`) |
|
|
101
|
+
| `easing` | `(t: number) => number` | Easing curve applied to the normalized cursor offset (default `defaultTiltEasing`) |
|
|
102
|
+
| `lockAxis` | `'x' \| 'y' \| 'none'` | Disables tilt on one axis entirely (default `'none'`) |
|
|
103
|
+
| `scaleOnHover` | `number` | Cosmetic `scale()` applied together with the tilt while tracking (default `1`) |
|
|
104
|
+
| `trackingMode` | `'self' \| 'group'` | `'self'` tracks this element's own bounding box; `'group'` reads a shared pointer from an ancestor `TiltGroup` (default `'self'`) |
|
|
105
|
+
| `disabled` | `boolean` | Disables tilt tracking entirely (default `false`) |
|
|
106
|
+
| `onTiltChange` | `(rotateX: number, rotateY: number) => void` | Fires on every applied tilt update |
|
|
107
|
+
|
|
108
|
+
**`TiltHandle`** (via `ref`):
|
|
109
|
+
|
|
110
|
+
| Method | Description |
|
|
111
|
+
|--------|-------------|
|
|
112
|
+
| `setTilt(rotateX, rotateY)` | Directly drives the tilt rotation, clamped to `maxTilt`/`maxTiltX`/`maxTiltY` |
|
|
113
|
+
| `reset()` | Returns to flat |
|
|
114
|
+
|
|
115
|
+
**`TiltGroupProps`:**
|
|
116
|
+
|
|
117
|
+
| Prop | Type | Description |
|
|
118
|
+
|------|------|-------------|
|
|
119
|
+
| `children` | `ReactNode` | The `Tilt`s (and any other content) inside the tracked area |
|
|
120
|
+
| `className` / `style` | | Root element; give it its own size/position |
|
|
121
|
+
|
|
122
|
+
**Global defaults:** `Tilt` adopts `ConfigProvider`, e.g. `defaultProps={{ Tilt: { maxTilt: 10 } }}`. See [ConfigProvider.md](ConfigProvider.md). `TiltGroup` doesn't adopt `ConfigProvider`; it has no prop surface beyond `children`/`className`/`style`.
|
|
123
|
+
|
|
124
|
+
**Slots:** `root` `perspective` `surface`
|