@mrmeg/expo-ui 0.22.0 → 0.24.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/CHANGELOG.md +89 -0
- package/LLM_USAGE.md +166 -157
- package/README.md +433 -346
- package/dist/components/Accordion.js +1 -1
- package/dist/components/BottomSheet.js +1 -1
- package/dist/components/Card.js +7 -7
- package/dist/components/Checkbox.js +1 -1
- package/dist/components/Dialog.js +24 -5
- package/dist/components/DismissKeyboard.d.ts +14 -3
- package/dist/components/DismissKeyboard.js +17 -48
- package/dist/components/EmptyState.js +2 -2
- package/dist/components/InputOTP.js +4 -1
- package/dist/components/Item.d.ts +3 -2
- package/dist/components/Item.js +8 -6
- package/dist/components/RadioGroup.js +1 -1
- package/dist/components/StatCard.js +1 -1
- package/dist/components/TextInput.js +189 -42
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/keyboardDismiss.d.ts +37 -0
- package/dist/components/keyboardDismiss.js +170 -0
- package/dist/components/keyboardFocusRegistry.d.ts +10 -14
- package/dist/components/keyboardFocusRegistry.js +8 -24
- package/dist/constants/spacing.d.ts +16 -6
- package/dist/constants/spacing.js +17 -6
- package/llms-full.md +134 -124
- package/llms.txt +24 -22
- package/package.json +3 -3
package/dist/components/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./Checkbox.js";
|
|
|
11
11
|
export * from "./Collapsible.js";
|
|
12
12
|
export * from "./Dialog.js";
|
|
13
13
|
export * from "./DismissKeyboard.js";
|
|
14
|
+
export * from "./keyboardDismiss.js";
|
|
14
15
|
export * from "./Drawer.js";
|
|
15
16
|
export * from "./DropdownMenu.js";
|
|
16
17
|
export * from "./EmptyState.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type GestureResponderEvent, type ViewProps } from "react-native";
|
|
2
|
+
/**
|
|
3
|
+
* `onStartShouldSetResponder` for a native text-field surface: decline the
|
|
4
|
+
* responder (the native field keeps every touch phase) but tag the touch so an
|
|
5
|
+
* enclosing tap-away boundary leaves it alone. The package `TextInput` uses
|
|
6
|
+
* this through {@link useTextInputSurfaceResponder}.
|
|
7
|
+
*/
|
|
8
|
+
export declare function markTextInputTouchStart(event: GestureResponderEvent): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Hide the software keyboard for whatever is focused. Prefers the registered
|
|
11
|
+
* native field's own blur handle (window-independent), then falls back to
|
|
12
|
+
* `KeyboardController.dismiss()`, which resigns at the IME level and covers RN
|
|
13
|
+
* or third-party inputs. No-op on web, which has no software keyboard to hide.
|
|
14
|
+
*/
|
|
15
|
+
export declare function dismissKeyboard(): void;
|
|
16
|
+
export type TextInputSurfaceResponderProps = Pick<ViewProps, "onStartShouldSetResponder" | "onTouchStart" | "onTouchMove" | "onTouchEnd" | "onTouchCancel">;
|
|
17
|
+
/**
|
|
18
|
+
* View props for the painted surface around a native (`@expo/ui`) text field.
|
|
19
|
+
*
|
|
20
|
+
* Two jobs. It tags every touch that begins on the surface so the tap-away
|
|
21
|
+
* boundary leaves it alone (see {@link markTextInputTouchStart}). And it
|
|
22
|
+
* focuses the field on release of a tap that no child claimed: the SwiftUI
|
|
23
|
+
* `TextField` only hit-tests its text line, so taps on the box's vertical
|
|
24
|
+
* padding never reach it, and a tap on the field's `hitSlop` never can. Calling
|
|
25
|
+
* `focus` is idempotent when the native field already took the tap, and the eye
|
|
26
|
+
* toggle is a `Pressable` that claims the responder first, so it never fires
|
|
27
|
+
* for that.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useTextInputSurfaceResponder(focus: () => void): TextInputSurfaceResponderProps;
|
|
30
|
+
export type KeyboardDismissResponderProps = Pick<ViewProps, "onStartShouldSetResponder" | "onTouchStart" | "onTouchMove" | "onTouchEnd" | "onTouchCancel">;
|
|
31
|
+
/**
|
|
32
|
+
* View props for a tap-away keyboard-dismiss boundary. Spread onto the wrapper
|
|
33
|
+
* `View` that encloses the screen (or a form). Nesting is safe: the innermost
|
|
34
|
+
* boundary owns each touch, so the keyboard is dismissed once. Returns `{}` on
|
|
35
|
+
* web.
|
|
36
|
+
*/
|
|
37
|
+
export declare function useKeyboardDismissResponder(): KeyboardDismissResponderProps;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tap-away keyboard dismissal that never steals a touch.
|
|
3
|
+
*
|
|
4
|
+
* React Native's own `keyboardShouldPersistTaps="handled"` is the model: let the
|
|
5
|
+
* deepest interactive view win the responder negotiation, and blur on *release*
|
|
6
|
+
* only when nothing else took the tap and the finger did not scroll. RN cannot
|
|
7
|
+
* apply that to the native `@expo/ui` field (it is invisible to
|
|
8
|
+
* `TextInputState`), so this module re-creates the same decision with two
|
|
9
|
+
* signals that need no rectangles and no responder ownership:
|
|
10
|
+
*
|
|
11
|
+
* 1. `onStartShouldSetResponder` in the *bubble* phase is only ever called on a
|
|
12
|
+
* view when no descendant claimed the touch. Pressables, RN TextInputs and
|
|
13
|
+
* scroll views all claim, so being asked at all means "this tap is on dead
|
|
14
|
+
* space or on a native view that does not participate".
|
|
15
|
+
* 2. The package `TextInput` tags touches that begin on its surface
|
|
16
|
+
* (`markTextInputTouchStart`). Its bubble handler runs before any enclosing
|
|
17
|
+
* boundary's, so the boundary can tell a tap on *any* native field — focused
|
|
18
|
+
* or not — from dead space, and leave focus handoff, double-tap selection
|
|
19
|
+
* and the eye toggle to the platform.
|
|
20
|
+
*
|
|
21
|
+
* The boundary always answers `false`, so no JS responder is ever set: native
|
|
22
|
+
* controls (SwiftUI/Compose fields, sliders) keep receiving every touch phase
|
|
23
|
+
* on both platforms, and scroll views take over drags exactly as before.
|
|
24
|
+
* Dismissal happens in `onTouchEnd`, a plain bubbling event that fires whether
|
|
25
|
+
* or not anyone became the responder; a drag past the slop or a cancel drops it.
|
|
26
|
+
*/
|
|
27
|
+
import { useMemo, useRef } from "react";
|
|
28
|
+
import { Platform } from "react-native";
|
|
29
|
+
import { KeyboardController, useKeyboardState } from "./keyboardController";
|
|
30
|
+
import { dismissKeyboardFocusedInput, hasKeyboardFocusedInput } from "./keyboardFocusRegistry.js";
|
|
31
|
+
/** Finger travel (pt) after which a touch counts as a scroll, not a tap. */
|
|
32
|
+
const MOVE_SLOP = 10;
|
|
33
|
+
function keyOf(event) {
|
|
34
|
+
const { identifier, timestamp } = event.nativeEvent;
|
|
35
|
+
return { identifier, timestamp };
|
|
36
|
+
}
|
|
37
|
+
/** Same native event: the responder negotiation hands one event to every listener. */
|
|
38
|
+
function isSameEvent(key, event) {
|
|
39
|
+
return (key != null &&
|
|
40
|
+
key.identifier === event.nativeEvent.identifier &&
|
|
41
|
+
key.timestamp === event.nativeEvent.timestamp);
|
|
42
|
+
}
|
|
43
|
+
let textInputTouch = null;
|
|
44
|
+
let claimedTouch = null;
|
|
45
|
+
/**
|
|
46
|
+
* `onStartShouldSetResponder` for a native text-field surface: decline the
|
|
47
|
+
* responder (the native field keeps every touch phase) but tag the touch so an
|
|
48
|
+
* enclosing tap-away boundary leaves it alone. The package `TextInput` uses
|
|
49
|
+
* this through {@link useTextInputSurfaceResponder}.
|
|
50
|
+
*/
|
|
51
|
+
export function markTextInputTouchStart(event) {
|
|
52
|
+
textInputTouch = keyOf(event);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Hide the software keyboard for whatever is focused. Prefers the registered
|
|
57
|
+
* native field's own blur handle (window-independent), then falls back to
|
|
58
|
+
* `KeyboardController.dismiss()`, which resigns at the IME level and covers RN
|
|
59
|
+
* or third-party inputs. No-op on web, which has no software keyboard to hide.
|
|
60
|
+
*/
|
|
61
|
+
export function dismissKeyboard() {
|
|
62
|
+
if (Platform.OS === "web")
|
|
63
|
+
return;
|
|
64
|
+
if (!dismissKeyboardFocusedInput()) {
|
|
65
|
+
void KeyboardController.dismiss();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function isSameTouch(tap, event) {
|
|
69
|
+
return tap != null && tap.identifier === event.nativeEvent.identifier;
|
|
70
|
+
}
|
|
71
|
+
function movedPastSlop(tap, event) {
|
|
72
|
+
return (Math.abs(event.nativeEvent.pageX - tap.pageX) > MOVE_SLOP ||
|
|
73
|
+
Math.abs(event.nativeEvent.pageY - tap.pageY) > MOVE_SLOP);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* View props for the painted surface around a native (`@expo/ui`) text field.
|
|
77
|
+
*
|
|
78
|
+
* Two jobs. It tags every touch that begins on the surface so the tap-away
|
|
79
|
+
* boundary leaves it alone (see {@link markTextInputTouchStart}). And it
|
|
80
|
+
* focuses the field on release of a tap that no child claimed: the SwiftUI
|
|
81
|
+
* `TextField` only hit-tests its text line, so taps on the box's vertical
|
|
82
|
+
* padding never reach it, and a tap on the field's `hitSlop` never can. Calling
|
|
83
|
+
* `focus` is idempotent when the native field already took the tap, and the eye
|
|
84
|
+
* toggle is a `Pressable` that claims the responder first, so it never fires
|
|
85
|
+
* for that.
|
|
86
|
+
*/
|
|
87
|
+
export function useTextInputSurfaceResponder(focus) {
|
|
88
|
+
const focusRef = useRef(focus);
|
|
89
|
+
focusRef.current = focus;
|
|
90
|
+
const tap = useRef(null);
|
|
91
|
+
return useMemo(() => ({
|
|
92
|
+
onStartShouldSetResponder: (event) => {
|
|
93
|
+
markTextInputTouchStart(event);
|
|
94
|
+
tap.current = { ...keyOf(event), pageX: event.nativeEvent.pageX, pageY: event.nativeEvent.pageY };
|
|
95
|
+
return false;
|
|
96
|
+
},
|
|
97
|
+
onTouchStart: (event) => {
|
|
98
|
+
if (tap.current && !isSameEvent(tap.current, event))
|
|
99
|
+
tap.current = null;
|
|
100
|
+
},
|
|
101
|
+
onTouchMove: (event) => {
|
|
102
|
+
if (isSameTouch(tap.current, event) && movedPastSlop(tap.current, event))
|
|
103
|
+
tap.current = null;
|
|
104
|
+
},
|
|
105
|
+
onTouchEnd: (event) => {
|
|
106
|
+
if (!isSameTouch(tap.current, event))
|
|
107
|
+
return;
|
|
108
|
+
tap.current = null;
|
|
109
|
+
focusRef.current();
|
|
110
|
+
},
|
|
111
|
+
onTouchCancel: () => {
|
|
112
|
+
tap.current = null;
|
|
113
|
+
},
|
|
114
|
+
}), []);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* View props for a tap-away keyboard-dismiss boundary. Spread onto the wrapper
|
|
118
|
+
* `View` that encloses the screen (or a form). Nesting is safe: the innermost
|
|
119
|
+
* boundary owns each touch, so the keyboard is dismissed once. Returns `{}` on
|
|
120
|
+
* web.
|
|
121
|
+
*/
|
|
122
|
+
export function useKeyboardDismissResponder() {
|
|
123
|
+
const isVisible = useKeyboardState((state) => state.isVisible);
|
|
124
|
+
const isVisibleRef = useRef(isVisible);
|
|
125
|
+
isVisibleRef.current = isVisible;
|
|
126
|
+
const pending = useRef(null);
|
|
127
|
+
return useMemo(() => {
|
|
128
|
+
if (Platform.OS === "web")
|
|
129
|
+
return {};
|
|
130
|
+
return {
|
|
131
|
+
onStartShouldSetResponder: (event) => {
|
|
132
|
+
pending.current = null;
|
|
133
|
+
// `hasKeyboardFocusedInput` covers the window where a field has focus but
|
|
134
|
+
// keyboard-controller has not reported the keyboard yet (or a hardware
|
|
135
|
+
// keyboard is attached and there is no software keyboard to observe).
|
|
136
|
+
if (!isVisibleRef.current && !hasKeyboardFocusedInput())
|
|
137
|
+
return false;
|
|
138
|
+
if (isSameEvent(textInputTouch, event))
|
|
139
|
+
return false;
|
|
140
|
+
// An inner boundary already runs this touch (bubble order is inner-first).
|
|
141
|
+
if (isSameEvent(claimedTouch, event))
|
|
142
|
+
return false;
|
|
143
|
+
claimedTouch = keyOf(event);
|
|
144
|
+
pending.current = { ...claimedTouch, pageX: event.nativeEvent.pageX, pageY: event.nativeEvent.pageY };
|
|
145
|
+
return false;
|
|
146
|
+
},
|
|
147
|
+
onTouchStart: (event) => {
|
|
148
|
+
// Bubbling touch events run after the negotiation for the same native
|
|
149
|
+
// event, so a fresh pending tap matches here; anything else is stale
|
|
150
|
+
// (e.g. a touch whose end never reached this view).
|
|
151
|
+
if (pending.current && !isSameEvent(pending.current, event))
|
|
152
|
+
pending.current = null;
|
|
153
|
+
},
|
|
154
|
+
onTouchMove: (event) => {
|
|
155
|
+
if (isSameTouch(pending.current, event) && movedPastSlop(pending.current, event)) {
|
|
156
|
+
pending.current = null;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
onTouchEnd: (event) => {
|
|
160
|
+
if (!isSameTouch(pending.current, event))
|
|
161
|
+
return;
|
|
162
|
+
pending.current = null;
|
|
163
|
+
dismissKeyboard();
|
|
164
|
+
},
|
|
165
|
+
onTouchCancel: () => {
|
|
166
|
+
pending.current = null;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}, []);
|
|
170
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Registry of the currently focused native (`@expo/ui`) text field.
|
|
3
|
+
*
|
|
4
|
+
* React Native's `TextInputState` never learns about SwiftUI / Compose fields,
|
|
5
|
+
* so `Keyboard.dismiss()` cannot blur them. The package `TextInput` registers a
|
|
6
|
+
* blur handle here on focus; tap-away dismissal (`keyboardDismiss.ts`) and the
|
|
7
|
+
* `BottomSheet` overlay resign the field through it.
|
|
8
|
+
*/
|
|
4
9
|
export type KeyboardFocusedInputToken = object;
|
|
5
10
|
/**
|
|
6
11
|
* Subscribe to focus presence changes. Pairs with {@link hasKeyboardFocusedInput}
|
|
@@ -9,16 +14,8 @@ export type KeyboardFocusedInputToken = object;
|
|
|
9
14
|
* keyboards raised inside a native sheet's isolated window.
|
|
10
15
|
*/
|
|
11
16
|
export declare function subscribeKeyboardFocus(listener: () => void): () => void;
|
|
12
|
-
export declare function
|
|
13
|
-
export declare function
|
|
14
|
-
export declare function getKeyboardFocusedInputLayout(): {
|
|
15
|
-
x: number;
|
|
16
|
-
y: number;
|
|
17
|
-
width: number;
|
|
18
|
-
height: number;
|
|
19
|
-
absoluteX: number;
|
|
20
|
-
absoluteY: number;
|
|
21
|
-
} | undefined;
|
|
17
|
+
export declare function setKeyboardFocusedInput(token: KeyboardFocusedInputToken, blur: () => void): void;
|
|
18
|
+
export declare function clearKeyboardFocusedInput(token: KeyboardFocusedInputToken): void;
|
|
22
19
|
/** Whether any native field currently holds focus. Stable boolean for snapshots. */
|
|
23
20
|
export declare function hasKeyboardFocusedInput(): boolean;
|
|
24
21
|
/**
|
|
@@ -26,4 +23,3 @@ export declare function hasKeyboardFocusedInput(): boolean;
|
|
|
26
23
|
* `true` if a field was focused and a blur handle was available.
|
|
27
24
|
*/
|
|
28
25
|
export declare function dismissKeyboardFocusedInput(): boolean;
|
|
29
|
-
export declare function isTouchInsideKeyboardFocusedInput(event: GestureResponderEvent, nativeLayout?: KeyboardFocusedInputLayout | null): boolean;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const FOCUSED_INPUT_TOUCH_SLOP = 16;
|
|
2
1
|
let focusedInput = null;
|
|
3
2
|
const listeners = new Set();
|
|
4
3
|
function emit() {
|
|
@@ -16,23 +15,22 @@ export function subscribeKeyboardFocus(listener) {
|
|
|
16
15
|
listeners.delete(listener);
|
|
17
16
|
};
|
|
18
17
|
}
|
|
19
|
-
export function
|
|
18
|
+
export function setKeyboardFocusedInput(token, blur) {
|
|
20
19
|
const wasPresent = focusedInput != null;
|
|
21
|
-
focusedInput = { token,
|
|
22
|
-
// Only notify subscribers on a presence transition;
|
|
23
|
-
//
|
|
20
|
+
focusedInput = { token, blur };
|
|
21
|
+
// Only notify subscribers on a presence transition; a focus handoff between
|
|
22
|
+
// two fields keeps presence true and stays silent.
|
|
24
23
|
if (!wasPresent)
|
|
25
24
|
emit();
|
|
26
25
|
}
|
|
27
|
-
export function
|
|
26
|
+
export function clearKeyboardFocusedInput(token) {
|
|
27
|
+
// Token-guarded: when focus hops A -> B, B's focus can land before A's blur,
|
|
28
|
+
// and A's late clear must not wipe B's registration.
|
|
28
29
|
if (focusedInput?.token === token) {
|
|
29
30
|
focusedInput = null;
|
|
30
31
|
emit();
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
export function getKeyboardFocusedInputLayout() {
|
|
34
|
-
return focusedInput?.layout;
|
|
35
|
-
}
|
|
36
34
|
/** Whether any native field currently holds focus. Stable boolean for snapshots. */
|
|
37
35
|
export function hasKeyboardFocusedInput() {
|
|
38
36
|
return focusedInput != null;
|
|
@@ -42,23 +40,9 @@ export function hasKeyboardFocusedInput() {
|
|
|
42
40
|
* `true` if a field was focused and a blur handle was available.
|
|
43
41
|
*/
|
|
44
42
|
export function dismissKeyboardFocusedInput() {
|
|
45
|
-
if (focusedInput
|
|
43
|
+
if (focusedInput) {
|
|
46
44
|
focusedInput.blur();
|
|
47
45
|
return true;
|
|
48
46
|
}
|
|
49
47
|
return false;
|
|
50
48
|
}
|
|
51
|
-
function isTouchInsideLayout(event, layout) {
|
|
52
|
-
if (!layout)
|
|
53
|
-
return false;
|
|
54
|
-
const { pageX, pageY } = event.nativeEvent;
|
|
55
|
-
const left = layout.absoluteX - FOCUSED_INPUT_TOUCH_SLOP;
|
|
56
|
-
const right = layout.absoluteX + layout.width + FOCUSED_INPUT_TOUCH_SLOP;
|
|
57
|
-
const top = layout.absoluteY - FOCUSED_INPUT_TOUCH_SLOP;
|
|
58
|
-
const bottom = layout.absoluteY + layout.height + FOCUSED_INPUT_TOUCH_SLOP;
|
|
59
|
-
return pageX >= left && pageX <= right && pageY >= top && pageY <= bottom;
|
|
60
|
-
}
|
|
61
|
-
export function isTouchInsideKeyboardFocusedInput(event, nativeLayout) {
|
|
62
|
-
return (isTouchInsideLayout(event, getKeyboardFocusedInputLayout()) ||
|
|
63
|
-
isTouchInsideLayout(event, nativeLayout));
|
|
64
|
-
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spacing Constants
|
|
3
3
|
*
|
|
4
|
-
* A consistent spacing scale based on an 8px base unit
|
|
5
|
-
*
|
|
4
|
+
* A consistent spacing scale based on an 8px base unit, plus a small set of
|
|
5
|
+
* density tokens that components reach for when composing rows, surfaces, and
|
|
6
|
+
* screens. Tune the density tokens to shift the whole library's feel without
|
|
7
|
+
* touching the base scale.
|
|
6
8
|
*/
|
|
7
9
|
export declare const spacing: {
|
|
8
10
|
readonly base: 8;
|
|
9
11
|
readonly xxs: 2;
|
|
10
12
|
readonly xs: 4;
|
|
11
13
|
readonly sm: 8;
|
|
14
|
+
readonly smd: 12;
|
|
12
15
|
readonly md: 16;
|
|
16
|
+
readonly mdl: 20;
|
|
13
17
|
readonly lg: 24;
|
|
14
18
|
readonly xl: 32;
|
|
15
19
|
readonly xxl: 48;
|
|
@@ -17,12 +21,18 @@ export declare const spacing: {
|
|
|
17
21
|
readonly gutter: 16;
|
|
18
22
|
readonly gutterVertical: 24;
|
|
19
23
|
readonly screenPadding: 16;
|
|
24
|
+
readonly sectionSpacing: 24;
|
|
25
|
+
readonly rowPaddingY: 10;
|
|
26
|
+
readonly rowPaddingX: 16;
|
|
27
|
+
readonly rowGap: 12;
|
|
28
|
+
readonly rowMinHeight: 40;
|
|
29
|
+
readonly formRowMinHeight: 32;
|
|
30
|
+
readonly cardPadding: 16;
|
|
31
|
+
readonly dialogPadding: 20;
|
|
32
|
+
readonly touchTarget: 44;
|
|
20
33
|
readonly buttonPadding: 10;
|
|
21
34
|
readonly inputPadding: 10;
|
|
22
|
-
readonly cardPadding: 16;
|
|
23
|
-
readonly sectionSpacing: 32;
|
|
24
35
|
readonly listItemSpacing: 8;
|
|
25
|
-
readonly touchTarget: 44;
|
|
26
36
|
readonly radiusNone: 0;
|
|
27
37
|
readonly radiusXs: 4;
|
|
28
38
|
readonly radiusSm: 8;
|
|
@@ -37,5 +47,5 @@ export declare const spacing: {
|
|
|
37
47
|
readonly iconLg: 32;
|
|
38
48
|
readonly iconXl: 48;
|
|
39
49
|
};
|
|
40
|
-
export declare const base: 8, xxs: 2, xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16,
|
|
50
|
+
export declare const base: 8, xxs: 2, xs: 4, sm: 8, smd: 12, md: 16, mdl: 20, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, sectionSpacing: 24, rowPaddingY: 10, rowPaddingX: 16, rowGap: 12, rowMinHeight: 40, formRowMinHeight: 32, cardPadding: 16, dialogPadding: 20, touchTarget: 44, buttonPadding: 10, inputPadding: 10, listItemSpacing: 8, radiusNone: 0, radiusXs: 4, radiusSm: 8, radiusMd: 12, radiusLg: 14, radiusXl: 18, radius2xl: 24, radiusFull: 9999, iconXs: 12, iconSm: 16, iconMd: 24, iconLg: 32, iconXl: 48;
|
|
41
51
|
export declare const space: (multiplier: number) => number;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spacing Constants
|
|
3
3
|
*
|
|
4
|
-
* A consistent spacing scale based on an 8px base unit
|
|
5
|
-
*
|
|
4
|
+
* A consistent spacing scale based on an 8px base unit, plus a small set of
|
|
5
|
+
* density tokens that components reach for when composing rows, surfaces, and
|
|
6
|
+
* screens. Tune the density tokens to shift the whole library's feel without
|
|
7
|
+
* touching the base scale.
|
|
6
8
|
*/
|
|
7
9
|
export const spacing = {
|
|
8
10
|
// Base unit: 8px
|
|
@@ -11,7 +13,9 @@ export const spacing = {
|
|
|
11
13
|
xxs: 2, // 2px - Minimal spacing
|
|
12
14
|
xs: 4, // 4px - Extra small
|
|
13
15
|
sm: 8, // 8px - Small (1 unit)
|
|
16
|
+
smd: 12, // 12px - Between small and medium (1.5 units); compact gaps and row padding
|
|
14
17
|
md: 16, // 16px - Medium (2 units)
|
|
18
|
+
mdl: 20, // 20px - Between medium and large (2.5 units); dialog and sheet padding
|
|
15
19
|
lg: 24, // 24px - Large (3 units)
|
|
16
20
|
xl: 32, // 32px - Extra large (4 units)
|
|
17
21
|
xxl: 48, // 48px - 2x extra large (6 units)
|
|
@@ -20,13 +24,20 @@ export const spacing = {
|
|
|
20
24
|
gutter: 16, // Standard horizontal gutter
|
|
21
25
|
gutterVertical: 24, // Standard vertical spacing
|
|
22
26
|
screenPadding: 16, // Default screen edge padding
|
|
27
|
+
sectionSpacing: 24, // Space between major sections / grouped lists
|
|
28
|
+
// Density tokens — rows, surfaces, overlays
|
|
29
|
+
rowPaddingY: 10, // Vertical padding for list / settings rows
|
|
30
|
+
rowPaddingX: 16, // Horizontal padding for list / settings rows
|
|
31
|
+
rowGap: 12, // Gap between media, content, and actions in a row
|
|
32
|
+
rowMinHeight: 40, // Visual min height for rows on pointer surfaces (web)
|
|
33
|
+
formRowMinHeight: 32, // Visual min height for checkbox / radio rows on web
|
|
34
|
+
cardPadding: 16, // Default card / stat card padding
|
|
35
|
+
dialogPadding: 20, // Default dialog / alert dialog padding
|
|
36
|
+
touchTarget: 44, // Minimum comfortable native touch target
|
|
23
37
|
// Component-specific
|
|
24
38
|
buttonPadding: 10, // Default button padding
|
|
25
39
|
inputPadding: 10, // Default input padding
|
|
26
|
-
cardPadding: 16, // Default card padding
|
|
27
|
-
sectionSpacing: 32, // Space between major sections
|
|
28
40
|
listItemSpacing: 8, // Space between list items
|
|
29
|
-
touchTarget: 44, // Minimum comfortable native touch target
|
|
30
41
|
// Border radius — shadcn-inspired scale (radiusMd = 12px default)
|
|
31
42
|
radiusNone: 0,
|
|
32
43
|
radiusXs: 4,
|
|
@@ -44,6 +55,6 @@ export const spacing = {
|
|
|
44
55
|
iconXl: 48,
|
|
45
56
|
};
|
|
46
57
|
// Export individual constants for convenience
|
|
47
|
-
export const { base, xxs, xs, sm, md, lg, xl, xxl, xxxl, gutter, gutterVertical, screenPadding,
|
|
58
|
+
export const { base, xxs, xs, sm, smd, md, mdl, lg, xl, xxl, xxxl, gutter, gutterVertical, screenPadding, sectionSpacing, rowPaddingY, rowPaddingX, rowGap, rowMinHeight, formRowMinHeight, cardPadding, dialogPadding, touchTarget, buttonPadding, inputPadding, listItemSpacing, radiusNone, radiusXs, radiusSm, radiusMd, radiusLg, radiusXl, radius2xl, radiusFull, iconXs, iconSm, iconMd, iconLg, iconXl, } = spacing;
|
|
48
59
|
// Helper function to multiply base spacing
|
|
49
60
|
export const space = (multiplier) => spacing.base * multiplier;
|