@djangocfg/ui-core 2.1.584 → 2.1.586
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/package.json +7 -7
- package/src/components/index.ts +6 -0
- package/src/components/primitives/TouchSafeInput.tsx +160 -0
- package/src/components/specialized/token-icon/index.tsx +6 -1
- package/src/hooks/device/index.ts +2 -0
- package/src/hooks/device/useTouchSafeStyle.md +341 -0
- package/src/hooks/device/useTouchSafeStyle.ts +112 -0
- package/src/styles/css/utilities/touch-safe.css +166 -0
- package/src/styles/css/utilities.css +1 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/touch-safe.ts +116 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.586",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -130,8 +130,8 @@
|
|
|
130
130
|
"check:contrast": "node scripts/check-preset-contrast.mjs"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
|
-
"@djangocfg/api": "^2.1.
|
|
134
|
-
"@djangocfg/i18n": "^2.1.
|
|
133
|
+
"@djangocfg/api": "^2.1.586",
|
|
134
|
+
"@djangocfg/i18n": "^2.1.586",
|
|
135
135
|
"consola": "^3.4.2",
|
|
136
136
|
"lucide-react": "^0.545.0",
|
|
137
137
|
"moment": "^2.30.1",
|
|
@@ -207,10 +207,10 @@
|
|
|
207
207
|
"vaul": "1.1.2"
|
|
208
208
|
},
|
|
209
209
|
"devDependencies": {
|
|
210
|
-
"@djangocfg/api": "^2.1.
|
|
211
|
-
"@djangocfg/eslint-config": "^2.1.
|
|
212
|
-
"@djangocfg/i18n": "^2.1.
|
|
213
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
210
|
+
"@djangocfg/api": "^2.1.586",
|
|
211
|
+
"@djangocfg/eslint-config": "^2.1.586",
|
|
212
|
+
"@djangocfg/i18n": "^2.1.586",
|
|
213
|
+
"@djangocfg/typescript-config": "^2.1.586",
|
|
214
214
|
"@storybook/react-vite": "^10.5.0",
|
|
215
215
|
"@types/node": "^24.13.3",
|
|
216
216
|
"@types/react": "19.2.15",
|
package/src/components/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Primitives
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
export { TouchSafeInput, TouchSafeTextarea, TouchSafeSelect } from './primitives/TouchSafeInput';
|
|
7
|
+
export type { TouchSafeInputProps, TouchSafeTextareaProps, TouchSafeSelectProps } from './primitives/TouchSafeInput';
|
|
8
|
+
|
|
3
9
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
10
|
// Forms
|
|
5
11
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from 'react';
|
|
2
|
+
import { touchSafeStyle } from '../../utils/touch-safe';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Touch-safe input wrapper that automatically prevents iOS WebKit auto-zoom.
|
|
6
|
+
*
|
|
7
|
+
* Drop-in replacement for <input> with automatic iOS protection.
|
|
8
|
+
* No device detection needed, no hooks, no manual styling.
|
|
9
|
+
*
|
|
10
|
+
* **How it works**:
|
|
11
|
+
* - Uses CSS custom property `--touch-safe-font-size`
|
|
12
|
+
* - iOS: Automatically gets max(16px, 1em) via @supports
|
|
13
|
+
* - Android/Desktop: Uses provided fontSize or 1rem default
|
|
14
|
+
*
|
|
15
|
+
* **WCAG Compliant**: No viewport restrictions, text zoom works.
|
|
16
|
+
*
|
|
17
|
+
* @example Basic usage
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { TouchSafeInput } from '@djangocfg/ui-core/components';
|
|
20
|
+
*
|
|
21
|
+
* <TouchSafeInput type="email" placeholder="Email" />
|
|
22
|
+
* // iOS: font-size max(16px, 1em) → no zoom
|
|
23
|
+
* // Android/Desktop: font-size 1rem → design's default
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example Custom design font-size
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <TouchSafeInput
|
|
29
|
+
* type="search"
|
|
30
|
+
* designFontSize="0.875rem" // 14px on Android/Desktop
|
|
31
|
+
* />
|
|
32
|
+
* // iOS: max(16px, 1em)
|
|
33
|
+
* // Android/Desktop: 0.875rem (14px)
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example With existing styles
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <TouchSafeInput
|
|
39
|
+
* style={{ padding: '8px', border: '1px solid gray' }}
|
|
40
|
+
* designFontSize="14px"
|
|
41
|
+
* />
|
|
42
|
+
* // fontSize automatically added, other styles preserved
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @see {@link ../../utils/touch-safe.ts} Utility functions
|
|
46
|
+
* @see {@link ../../hooks/device/useTouchSafeStyle.ts} Hook alternative
|
|
47
|
+
*/
|
|
48
|
+
export interface TouchSafeInputProps extends ComponentPropsWithoutRef<'input'> {
|
|
49
|
+
/**
|
|
50
|
+
* Design's intended font-size for Android/Desktop.
|
|
51
|
+
* iOS will use max(16px, 1em) regardless.
|
|
52
|
+
*
|
|
53
|
+
* @default '1rem'
|
|
54
|
+
*/
|
|
55
|
+
designFontSize?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const TouchSafeInput = forwardRef<
|
|
59
|
+
ElementRef<'input'>,
|
|
60
|
+
TouchSafeInputProps
|
|
61
|
+
>(({ designFontSize = '1rem', style, ...props }, ref) => {
|
|
62
|
+
return (
|
|
63
|
+
<input
|
|
64
|
+
ref={ref}
|
|
65
|
+
{...props}
|
|
66
|
+
style={{
|
|
67
|
+
...style,
|
|
68
|
+
...touchSafeStyle(designFontSize),
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
TouchSafeInput.displayName = 'TouchSafeInput';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Touch-safe textarea wrapper that automatically prevents iOS WebKit auto-zoom.
|
|
78
|
+
*
|
|
79
|
+
* Drop-in replacement for <textarea> with automatic iOS protection.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx
|
|
83
|
+
* import { TouchSafeTextarea } from '@djangocfg/ui-core/components';
|
|
84
|
+
*
|
|
85
|
+
* <TouchSafeTextarea
|
|
86
|
+
* placeholder="Type a message..."
|
|
87
|
+
* designFontSize="14px"
|
|
88
|
+
* />
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export interface TouchSafeTextareaProps extends ComponentPropsWithoutRef<'textarea'> {
|
|
92
|
+
/**
|
|
93
|
+
* Design's intended font-size for Android/Desktop.
|
|
94
|
+
* iOS will use max(16px, 1em) regardless.
|
|
95
|
+
*
|
|
96
|
+
* @default '1rem'
|
|
97
|
+
*/
|
|
98
|
+
designFontSize?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const TouchSafeTextarea = forwardRef<
|
|
102
|
+
ElementRef<'textarea'>,
|
|
103
|
+
TouchSafeTextareaProps
|
|
104
|
+
>(({ designFontSize = '1rem', style, ...props }, ref) => {
|
|
105
|
+
return (
|
|
106
|
+
<textarea
|
|
107
|
+
ref={ref}
|
|
108
|
+
{...props}
|
|
109
|
+
style={{
|
|
110
|
+
...style,
|
|
111
|
+
...touchSafeStyle(designFontSize),
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
TouchSafeTextarea.displayName = 'TouchSafeTextarea';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Touch-safe select wrapper that automatically prevents iOS WebKit auto-zoom.
|
|
121
|
+
*
|
|
122
|
+
* Drop-in replacement for <select> with automatic iOS protection.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```tsx
|
|
126
|
+
* import { TouchSafeSelect } from '@djangocfg/ui-core/components';
|
|
127
|
+
*
|
|
128
|
+
* <TouchSafeSelect designFontSize="14px">
|
|
129
|
+
* <option value="1">Option 1</option>
|
|
130
|
+
* <option value="2">Option 2</option>
|
|
131
|
+
* </TouchSafeSelect>
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export interface TouchSafeSelectProps extends ComponentPropsWithoutRef<'select'> {
|
|
135
|
+
/**
|
|
136
|
+
* Design's intended font-size for Android/Desktop.
|
|
137
|
+
* iOS will use max(16px, 1em) regardless.
|
|
138
|
+
*
|
|
139
|
+
* @default '1rem'
|
|
140
|
+
*/
|
|
141
|
+
designFontSize?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const TouchSafeSelect = forwardRef<
|
|
145
|
+
ElementRef<'select'>,
|
|
146
|
+
TouchSafeSelectProps
|
|
147
|
+
>(({ designFontSize = '1rem', style, ...props }, ref) => {
|
|
148
|
+
return (
|
|
149
|
+
<select
|
|
150
|
+
ref={ref}
|
|
151
|
+
{...props}
|
|
152
|
+
style={{
|
|
153
|
+
...style,
|
|
154
|
+
...touchSafeStyle(designFontSize),
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
TouchSafeSelect.displayName = 'TouchSafeSelect';
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
|
|
5
5
|
import { cn } from '../../../lib/utils';
|
|
6
|
+
import { touchSafeFontSize } from '../../../utils/touch-safe';
|
|
6
7
|
|
|
7
8
|
export interface TokenIconProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
9
|
symbol: string
|
|
@@ -149,6 +150,10 @@ export function TokenIcon({
|
|
|
149
150
|
const fallbackFontSize = Math.max(8, size * 0.4)
|
|
150
151
|
const truncatedSymbol = symbol?.slice(0, 3) || '?'
|
|
151
152
|
|
|
153
|
+
// Touch-safe font size for iOS auto-zoom prevention
|
|
154
|
+
// iOS: max(16px, fallbackFontSize), Android/Desktop: fallbackFontSize
|
|
155
|
+
const touchSafeFallbackFontSize = touchSafeFontSize(`${fallbackFontSize}px`)
|
|
156
|
+
|
|
152
157
|
return (
|
|
153
158
|
<div
|
|
154
159
|
className={cn("inline-flex items-center justify-center flex-shrink-0", className)}
|
|
@@ -164,7 +169,7 @@ export function TokenIcon({
|
|
|
164
169
|
// Fallback: show symbol as text
|
|
165
170
|
<span
|
|
166
171
|
className="text-muted-foreground font-medium uppercase"
|
|
167
|
-
style={{ fontSize:
|
|
172
|
+
style={{ fontSize: touchSafeFallbackFontSize }}
|
|
168
173
|
>
|
|
169
174
|
{truncatedSymbol}
|
|
170
175
|
</span>
|
|
@@ -5,3 +5,5 @@ export type { BrowserInfo } from './useBrowserDetect';
|
|
|
5
5
|
export { useDeviceDetect } from './useDeviceDetect';
|
|
6
6
|
export type { DeviceDetectResult } from './useDeviceDetect';
|
|
7
7
|
export { useShortcutModLabel } from './useShortcutModLabel';
|
|
8
|
+
export { useTouchSafeStyle } from './useTouchSafeStyle';
|
|
9
|
+
export type { TouchSafeStyleOptions } from './useTouchSafeStyle';
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# useTouchSafeStyle
|
|
2
|
+
|
|
3
|
+
**iOS WebKit auto-zoom prevention hook**
|
|
4
|
+
|
|
5
|
+
Returns touch-safe font-size for input/textarea elements to prevent iOS Safari/Chrome from zooming viewport on focus.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Problem
|
|
10
|
+
|
|
11
|
+
iOS WebKit (Safari, Chrome, Edge) automatically zooms the viewport when an `<input>` or `<textarea>` with `font-size < 16px` receives focus. This causes:
|
|
12
|
+
|
|
13
|
+
- **UI "flying away"** — the entire page zooms in, scrolling content out of view
|
|
14
|
+
- **Critical UX failure** — especially in chat/conversation interfaces where context is essential
|
|
15
|
+
- **Disorientation** — users must manually zoom out to regain context
|
|
16
|
+
|
|
17
|
+
**Root Cause**: iOS design decision to help users read small text, but problematic for modern responsive designs.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Solution
|
|
22
|
+
|
|
23
|
+
Use `max(16px, 1em)` to maintain relative sizing while preventing zoom:
|
|
24
|
+
|
|
25
|
+
- **16px**: iOS zoom threshold (anything ≥16px prevents auto-zoom)
|
|
26
|
+
- **1em**: Maintains relative sizing for accessibility (respects user's base font-size preference)
|
|
27
|
+
- **max()**: Takes the larger value, ensuring both goals are met
|
|
28
|
+
|
|
29
|
+
**Default Behavior**: iOS-only targeting. Android and desktop remain unaffected.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
function useTouchSafeStyle(options?: TouchSafeStyleOptions): string | undefined
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Options
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
interface TouchSafeStyleOptions {
|
|
43
|
+
/** Target iOS specifically (default: true) */
|
|
44
|
+
ios?: boolean;
|
|
45
|
+
|
|
46
|
+
/** Target all mobile devices (default: false) */
|
|
47
|
+
mobile?: boolean;
|
|
48
|
+
|
|
49
|
+
/** Minimum font size to prevent zoom (default: '16px') */
|
|
50
|
+
minSize?: string;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Returns
|
|
55
|
+
|
|
56
|
+
- **iOS** (when `ios: true`, default): `"max(16px, 1em)"`
|
|
57
|
+
- **Android** (when `ios: true`): `undefined` (keeps optimal font-size, no zoom issue)
|
|
58
|
+
- **Desktop**: `undefined` (no mobile zoom behavior)
|
|
59
|
+
- **All mobile** (when `mobile: true`): `"max(16px, 1em)"` for iOS + Android
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Basic (iOS-only, recommended)
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { useTouchSafeStyle } from '@djangocfg/ui-core/hooks/device';
|
|
69
|
+
|
|
70
|
+
function ChatInput() {
|
|
71
|
+
const fontSize = useTouchSafeStyle();
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<textarea
|
|
75
|
+
style={{ fontSize }}
|
|
76
|
+
placeholder="Type a message..."
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Result**:
|
|
83
|
+
- **iOS**: `font-size: max(16px, 1em)` — no zoom ✅
|
|
84
|
+
- **Android**: no inline style — keeps design's 14px ✅
|
|
85
|
+
- **Desktop**: no inline style — normal sizing ✅
|
|
86
|
+
|
|
87
|
+
### All Mobile Devices
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
const fontSize = useTouchSafeStyle({ mobile: true, ios: false });
|
|
91
|
+
|
|
92
|
+
// iOS + Android both get 16px minimum
|
|
93
|
+
// Useful when: consistency across mobile platforms > optimal Android UX
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Custom Minimum Size
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
const fontSize = useTouchSafeStyle({ minSize: '18px' });
|
|
100
|
+
|
|
101
|
+
// Ensures font-size ≥18px on iOS
|
|
102
|
+
// Useful for: accessibility requirements, larger touch targets
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Conditional Fallback
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
function SearchInput() {
|
|
109
|
+
const touchSafeFontSize = useTouchSafeStyle();
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<input
|
|
113
|
+
type="search"
|
|
114
|
+
style={{
|
|
115
|
+
fontSize: touchSafeFontSize || '14px', // Desktop gets 14px
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Form with Multiple Inputs
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
function LoginForm() {
|
|
126
|
+
const fontSize = useTouchSafeStyle();
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<form>
|
|
130
|
+
<input type="email" style={{ fontSize }} />
|
|
131
|
+
<input type="password" style={{ fontSize }} />
|
|
132
|
+
<button type="submit">Sign In</button>
|
|
133
|
+
</form>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## CSS Alternative
|
|
141
|
+
|
|
142
|
+
For inputs that don't need programmatic control, use CSS utilities:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import '@djangocfg/ui-core/styles';
|
|
146
|
+
|
|
147
|
+
// Option 1: Utility class (opt-in)
|
|
148
|
+
<div className="touch-safe-input">
|
|
149
|
+
<input type="text" />
|
|
150
|
+
<textarea />
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
// Option 2: Automatic via @supports (iOS detection at build time)
|
|
154
|
+
// Already applied globally — inputs get max(16px, 1em) on iOS
|
|
155
|
+
// No class needed, works without JS
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**When to use CSS vs Hook**:
|
|
159
|
+
- **CSS**: Static inputs, no conditional logic, works without JS
|
|
160
|
+
- **Hook**: Dynamic styling, iOS-only precision, React-controlled components
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Why iOS-Only by Default?
|
|
165
|
+
|
|
166
|
+
**Android doesn't have auto-zoom behavior** — enforcing 16px there is unnecessary:
|
|
167
|
+
|
|
168
|
+
- ✅ **Better UX**: Android keeps design's optimal font-size (e.g., 14px for compact UI)
|
|
169
|
+
- ✅ **Precise targeting**: Only fixes the platform with the problem
|
|
170
|
+
- ✅ **Design flexibility**: Developers can choose smaller fonts on Android
|
|
171
|
+
|
|
172
|
+
**When to use `mobile: true`**:
|
|
173
|
+
- Consistent font-size across all mobile platforms is a requirement
|
|
174
|
+
- Simplicity over optimization
|
|
175
|
+
- Accessibility guidelines mandate 16px minimum everywhere
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## WCAG Compliance
|
|
180
|
+
|
|
181
|
+
✅ **WCAG 2.1 Level AA Compliant**
|
|
182
|
+
|
|
183
|
+
- **SC 1.4.4 Resize Text**: Text can be resized up to 200% without loss of content or functionality
|
|
184
|
+
- ✅ No viewport restrictions (no `user-scalable=no`)
|
|
185
|
+
- ✅ `max(16px, 1em)` respects user's base font-size settings
|
|
186
|
+
- ✅ Browser's text zoom still works
|
|
187
|
+
|
|
188
|
+
- **SC 1.4.10 Reflow**: Content reflows at 400% zoom without horizontal scrolling
|
|
189
|
+
- ✅ Relative `em` units maintain reflow behavior
|
|
190
|
+
- ✅ No fixed pixel-only sizing
|
|
191
|
+
|
|
192
|
+
**Rejected Alternative**: `viewport meta` with `user-scalable=no` ❌
|
|
193
|
+
```html
|
|
194
|
+
<!-- ❌ WCAG violation — prevents text zoom -->
|
|
195
|
+
<meta name="viewport" content="user-scalable=no" />
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Examples
|
|
201
|
+
|
|
202
|
+
### Chat Composer (Critical Context)
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
function ChatComposer() {
|
|
206
|
+
const fontSize = useTouchSafeStyle(); // iOS-only
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<textarea
|
|
210
|
+
style={{ fontSize }}
|
|
211
|
+
placeholder="Type your message..."
|
|
212
|
+
aria-label="Message input"
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Rationale**: Chat conversations are context-critical. iOS zoom breaks UX entirely. Android keeps optimal 14px.
|
|
219
|
+
|
|
220
|
+
### Search Box (Optional Enhancement)
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
function SearchBar() {
|
|
224
|
+
const fontSize = useTouchSafeStyle();
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<input
|
|
228
|
+
type="search"
|
|
229
|
+
style={{ fontSize: fontSize || '15px' }} // Desktop: 15px
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Rationale**: Search is less context-critical, but preventing zoom improves UX. Desktop gets design's preferred size.
|
|
236
|
+
|
|
237
|
+
### Login Form (Security-Critical)
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
function PasswordField() {
|
|
241
|
+
const fontSize = useTouchSafeStyle({ mobile: true }); // All mobile
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<input
|
|
245
|
+
type="password"
|
|
246
|
+
style={{ fontSize }}
|
|
247
|
+
autoComplete="current-password"
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Rationale**: Password fields benefit from larger text on all mobile devices for security/visibility.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Implementation Details
|
|
258
|
+
|
|
259
|
+
### How It Works
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
export function useTouchSafeStyle(options = {}) {
|
|
263
|
+
const { isIOS, isMobile } = useDeviceDetect();
|
|
264
|
+
const minSize = options.minSize || '16px';
|
|
265
|
+
|
|
266
|
+
// All mobile (iOS + Android)
|
|
267
|
+
if (options.mobile && isMobile) {
|
|
268
|
+
return `max(${minSize}, 1em)`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// iOS-only (default)
|
|
272
|
+
if (options.ios !== false && isIOS) {
|
|
273
|
+
return `max(${minSize}, 1em)`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Desktop or non-targeted mobile
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Detection Method
|
|
282
|
+
|
|
283
|
+
Uses `useDeviceDetect()` hook (from `react-device-detect`):
|
|
284
|
+
- **iOS**: Detects iOS Safari, Chrome, Edge, Firefox
|
|
285
|
+
- **Android**: Detects Android browsers
|
|
286
|
+
- **Desktop**: Excludes non-mobile devices
|
|
287
|
+
- **SSR-safe**: Returns default values server-side, updates client-side
|
|
288
|
+
|
|
289
|
+
### Performance
|
|
290
|
+
|
|
291
|
+
- **Zero runtime cost on desktop** — returns `undefined`, no style applied
|
|
292
|
+
- **Minimal on mobile** — one device detect call (cached), simple string return
|
|
293
|
+
- **No layout shift** — applied on mount, before first paint
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Related
|
|
298
|
+
|
|
299
|
+
- **CSS Utilities**: `@djangocfg/ui-core/styles/utilities/touch-safe.css`
|
|
300
|
+
- **Research**: `@researches/2026-10-25__ios-webkit-auto-zoom/`
|
|
301
|
+
- **Widget Implementation**: `@djangocfg/widget-chat` (auto-applied)
|
|
302
|
+
- **Device Detection**: `useDeviceDetect()` hook
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## References
|
|
307
|
+
|
|
308
|
+
- [Apple: Configuring the Viewport](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/AdjustingtheTextSize/AdjustingtheTextSize.html)
|
|
309
|
+
- [MDN: viewport meta tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag)
|
|
310
|
+
- [WCAG 2.1: Success Criterion 1.4.4](https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html)
|
|
311
|
+
- [WCAG 2.1: Success Criterion 1.4.10](https://www.w3.org/WAI/WCAG21/Understanding/reflow.html)
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Migration from Viewport Meta
|
|
316
|
+
|
|
317
|
+
**Before** (❌ WCAG violation):
|
|
318
|
+
```tsx
|
|
319
|
+
// layout.tsx
|
|
320
|
+
export const viewport = {
|
|
321
|
+
maximumScale: 1,
|
|
322
|
+
userScalable: false, // ❌ Prevents text zoom
|
|
323
|
+
};
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**After** (✅ WCAG compliant):
|
|
327
|
+
```tsx
|
|
328
|
+
// Remove viewport restrictions entirely
|
|
329
|
+
// Use useTouchSafeStyle() in components instead
|
|
330
|
+
|
|
331
|
+
function MyInput() {
|
|
332
|
+
const fontSize = useTouchSafeStyle();
|
|
333
|
+
return <input style={{ fontSize }} />;
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Benefits**:
|
|
338
|
+
- ✅ WCAG 2.1 AA compliant
|
|
339
|
+
- ✅ Text zoom works
|
|
340
|
+
- ✅ No global viewport restrictions
|
|
341
|
+
- ✅ iOS-only targeting (better Android UX)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useDeviceDetect } from './useDeviceDetect';
|
|
4
|
+
|
|
5
|
+
export interface TouchSafeStyleOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Target iOS specifically (default: true)
|
|
8
|
+
*
|
|
9
|
+
* When enabled, returns font-size only for iOS devices.
|
|
10
|
+
* Android and other platforms return undefined.
|
|
11
|
+
*/
|
|
12
|
+
ios?: boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Target all mobile devices (default: false)
|
|
16
|
+
*
|
|
17
|
+
* When enabled, returns font-size for all mobile devices,
|
|
18
|
+
* not just iOS. Overrides `ios` option.
|
|
19
|
+
*/
|
|
20
|
+
mobile?: boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Minimum font size to prevent zoom (default: '16px')
|
|
24
|
+
*
|
|
25
|
+
* iOS WebKit zooms when font-size < 16px, but you can set
|
|
26
|
+
* a higher threshold if needed (e.g., '18px').
|
|
27
|
+
*/
|
|
28
|
+
minSize?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns touch-safe font-size style for iOS WebKit auto-zoom prevention.
|
|
33
|
+
*
|
|
34
|
+
* **Problem**: iOS Safari/Chrome zooms viewport when font-size < 16px on input focus,
|
|
35
|
+
* causing UI elements to "fly away" (especially critical in chat/conversation interfaces).
|
|
36
|
+
*
|
|
37
|
+
* **Solution**: Use max(16px, 1em) to maintain relative sizing while preventing zoom.
|
|
38
|
+
*
|
|
39
|
+
* **WCAG Compliance**: ✅ WCAG 2.1 AA compliant (SC 1.4.4, 1.4.10)
|
|
40
|
+
* - Text zoom still works (no viewport user-scalable=no)
|
|
41
|
+
* - No reflow issues
|
|
42
|
+
* - Maintains 200% text zoom capability
|
|
43
|
+
*
|
|
44
|
+
* **Default Behavior**: iOS-only targeting (Android keeps optimal font-size)
|
|
45
|
+
*
|
|
46
|
+
* @param options - Configuration options
|
|
47
|
+
* @returns Font-size CSS value string or undefined if not applicable
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* // iOS-only (default) — Android keeps 14px, iOS gets 16px
|
|
52
|
+
* function ChatInput() {
|
|
53
|
+
* const fontSize = useTouchSafeStyle();
|
|
54
|
+
* return <textarea style={{ fontSize }} placeholder="Type a message..." />;
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* // All mobile devices
|
|
61
|
+
* const fontSize = useTouchSafeStyle({ mobile: true, ios: false });
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* // Custom minimum size
|
|
67
|
+
* const fontSize = useTouchSafeStyle({ minSize: '18px' });
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* // Conditional application
|
|
73
|
+
* function SearchInput() {
|
|
74
|
+
* const fontSize = useTouchSafeStyle();
|
|
75
|
+
*
|
|
76
|
+
* return (
|
|
77
|
+
* <input
|
|
78
|
+
* type="search"
|
|
79
|
+
* style={{
|
|
80
|
+
* fontSize: fontSize || '14px', // Fallback for desktop
|
|
81
|
+
* }}
|
|
82
|
+
* />
|
|
83
|
+
* );
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @see {@link https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/AdjustingtheTextSize/AdjustingtheTextSize.html}
|
|
88
|
+
* @see {@link @researches/2026-10-25__ios-webkit-auto-zoom} Research documentation
|
|
89
|
+
* @see {@link ../../styles/css/utilities/touch-safe.css} CSS utilities
|
|
90
|
+
*/
|
|
91
|
+
export function useTouchSafeStyle(options: TouchSafeStyleOptions = {}): string | undefined {
|
|
92
|
+
const { isIOS, isMobile } = useDeviceDetect();
|
|
93
|
+
|
|
94
|
+
const minSize = options.minSize || '16px';
|
|
95
|
+
|
|
96
|
+
// Default: iOS-only targeting
|
|
97
|
+
const targetIOS = options.ios !== false;
|
|
98
|
+
const targetMobile = options.mobile === true;
|
|
99
|
+
|
|
100
|
+
// Mobile override (all mobile devices)
|
|
101
|
+
if (targetMobile && isMobile) {
|
|
102
|
+
return `max(${minSize}, 1em)`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// iOS-specific targeting (default)
|
|
106
|
+
if (targetIOS && isIOS) {
|
|
107
|
+
return `max(${minSize}, 1em)`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Desktop or non-targeted mobile (Android when ios: true)
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Touch-Safe Input Utilities
|
|
3
|
+
*
|
|
4
|
+
* Prevents iOS WebKit auto-zoom on input/textarea focus by enforcing 16px minimum font-size.
|
|
5
|
+
*
|
|
6
|
+
* **Problem**: iOS Safari/Chrome zooms viewport when font-size < 16px on focus,
|
|
7
|
+
* causing UI elements to "fly away" (especially critical in chat/conversation interfaces).
|
|
8
|
+
*
|
|
9
|
+
* **Solution**: Use max(16px, 1em) to maintain relative sizing while preventing zoom.
|
|
10
|
+
*
|
|
11
|
+
* **Universal Application**: CSS custom property `--touch-safe-font-size` is set on :root
|
|
12
|
+
* for iOS devices. Components can reference this variable to get automatic iOS fix without
|
|
13
|
+
* explicit hook calls.
|
|
14
|
+
*
|
|
15
|
+
* **WCAG Compliance**: ✅ WCAG 2.1 AA compliant (SC 1.4.4, 1.4.10)
|
|
16
|
+
* - Text zoom still works (no viewport user-scalable=no)
|
|
17
|
+
* - No reflow issues
|
|
18
|
+
* - Maintains 200% text zoom capability
|
|
19
|
+
*
|
|
20
|
+
* @see {@link ../../hooks/input/useTouchSafeStyle.ts} React hook for programmatic control
|
|
21
|
+
* @see {@link @researches/2026-10-25__ios-webkit-auto-zoom} Research documentation
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/* ============================================================================
|
|
25
|
+
CSS Custom Property (Universal iOS Fix)
|
|
26
|
+
========================================================================= */
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Global CSS variable for touch-safe font sizing.
|
|
30
|
+
*
|
|
31
|
+
* Set on :root for iOS devices only via @supports detection.
|
|
32
|
+
* Components can reference var(--touch-safe-font-size, fallback) to get
|
|
33
|
+
* automatic iOS protection without explicit hook usage.
|
|
34
|
+
*
|
|
35
|
+
* iOS: 16px minimum (prevents auto-zoom)
|
|
36
|
+
* Android/Desktop: undefined (use component's design font-size)
|
|
37
|
+
*
|
|
38
|
+
* @example Component usage:
|
|
39
|
+
* .my-input {
|
|
40
|
+
* font-size: var(--touch-safe-font-size, 0.875rem);
|
|
41
|
+
* }
|
|
42
|
+
* // iOS: max(16px, 1em)
|
|
43
|
+
* // Android/Desktop: 0.875rem (14px)
|
|
44
|
+
*/
|
|
45
|
+
:root {
|
|
46
|
+
--touch-safe-font-size: initial; /* Desktop/Android default */
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@supports (-webkit-touch-callout: none) {
|
|
50
|
+
:root {
|
|
51
|
+
--touch-safe-font-size: max(16px, 1em);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ============================================================================
|
|
56
|
+
Utility Class (Opt-In)
|
|
57
|
+
========================================================================= */
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Apply to any container with inputs/textareas that need touch-safe sizing.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* <form className="touch-safe-input">
|
|
64
|
+
* <input type="text" />
|
|
65
|
+
* <textarea />
|
|
66
|
+
* </form>
|
|
67
|
+
*/
|
|
68
|
+
.touch-safe-input input,
|
|
69
|
+
.touch-safe-input textarea,
|
|
70
|
+
.touch-safe-input select {
|
|
71
|
+
font-size: max(16px, 1em);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* ============================================================================
|
|
75
|
+
iOS-Specific Auto-Fix (@supports detection)
|
|
76
|
+
========================================================================= */
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Targets iOS WebKit devices via CSS feature detection.
|
|
80
|
+
* More reliable than user-agent sniffing, works at build time.
|
|
81
|
+
*
|
|
82
|
+
* Note: Also matches desktop Safari on macOS, but 16px is acceptable there
|
|
83
|
+
* (no zoom behavior). For precise iOS-only targeting, use useTouchSafeStyle hook.
|
|
84
|
+
*/
|
|
85
|
+
@supports (-webkit-touch-callout: none) {
|
|
86
|
+
input[type="text"],
|
|
87
|
+
input[type="email"],
|
|
88
|
+
input[type="search"],
|
|
89
|
+
input[type="tel"],
|
|
90
|
+
input[type="url"],
|
|
91
|
+
input[type="password"],
|
|
92
|
+
input[type="number"],
|
|
93
|
+
textarea,
|
|
94
|
+
select {
|
|
95
|
+
font-size: max(16px, 1em);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Prevent iOS text size adjustment during orientation changes */
|
|
99
|
+
html {
|
|
100
|
+
-webkit-text-size-adjust: 100%;
|
|
101
|
+
text-size-adjust: 100%;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ============================================================================
|
|
106
|
+
Mobile Baseline (Opt-In via class)
|
|
107
|
+
========================================================================= */
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Apply to containers for mobile-wide touch-safe inputs (iOS + Android).
|
|
111
|
+
*
|
|
112
|
+
* Note: Android doesn't have the same zoom behavior, so this is only needed
|
|
113
|
+
* if you want consistent 16px across all mobile devices. For iOS-only,
|
|
114
|
+
* prefer @supports above or useTouchSafeStyle({ ios: true }) hook.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* <div className="touch-safe-mobile">
|
|
118
|
+
* <input type="text" />
|
|
119
|
+
* </div>
|
|
120
|
+
*/
|
|
121
|
+
@media (max-width: 768px) {
|
|
122
|
+
.touch-safe-mobile input,
|
|
123
|
+
.touch-safe-mobile textarea,
|
|
124
|
+
.touch-safe-mobile select {
|
|
125
|
+
font-size: max(16px, 1em);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ============================================================================
|
|
130
|
+
Touch Target Size (44px minimum for WCAG)
|
|
131
|
+
========================================================================= */
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Ensures minimum 44×44px touch targets for mobile accessibility.
|
|
135
|
+
*
|
|
136
|
+
* WCAG 2.1 Success Criterion 2.5.5 (Level AAA): Target Size
|
|
137
|
+
* iOS Human Interface Guidelines: 44pt minimum
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* <button className="touch-target">Click Me</button>
|
|
141
|
+
*/
|
|
142
|
+
.touch-target {
|
|
143
|
+
min-height: 44px;
|
|
144
|
+
min-width: 44px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@media (max-width: 768px) {
|
|
148
|
+
/* Enforce on mobile for buttons, links with onClick, form controls */
|
|
149
|
+
button,
|
|
150
|
+
[role="button"],
|
|
151
|
+
a[onClick],
|
|
152
|
+
input[type="submit"],
|
|
153
|
+
input[type="button"],
|
|
154
|
+
input[type="reset"],
|
|
155
|
+
select {
|
|
156
|
+
min-height: 44px;
|
|
157
|
+
min-width: 44px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Special case: Icon-only buttons */
|
|
161
|
+
button[aria-label]:not(:has(*)),
|
|
162
|
+
[role="button"][aria-label]:not(:has(*)) {
|
|
163
|
+
min-height: 44px;
|
|
164
|
+
min-width: 44px;
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Touch-safe font-size utilities for universal iOS auto-zoom prevention.
|
|
3
|
+
*
|
|
4
|
+
* Provides CSS and JavaScript helpers that automatically protect against
|
|
5
|
+
* iOS WebKit viewport zoom on input focus, without requiring explicit
|
|
6
|
+
* device detection in every component.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link ../styles/css/utilities/touch-safe.css} CSS implementation
|
|
9
|
+
* @see {@link ../hooks/device/useTouchSafeStyle.ts} React hook alternative
|
|
10
|
+
* @see {@link @researches/2026-10-25__ios-webkit-auto-zoom} Research
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns touch-safe font-size value that works universally.
|
|
15
|
+
*
|
|
16
|
+
* Uses CSS custom property `--touch-safe-font-size` which is automatically
|
|
17
|
+
* set to `max(16px, 1em)` on iOS via @supports detection.
|
|
18
|
+
*
|
|
19
|
+
* **Automatic behavior**:
|
|
20
|
+
* - iOS: `max(16px, 1em)` (prevents auto-zoom)
|
|
21
|
+
* - Android/Desktop: fallback value (design's intended size)
|
|
22
|
+
*
|
|
23
|
+
* @param fallback - Fallback font-size for Android/Desktop (default: '1rem')
|
|
24
|
+
* @returns CSS font-size value string
|
|
25
|
+
*
|
|
26
|
+
* @example Inline style
|
|
27
|
+
* ```tsx
|
|
28
|
+
* import { touchSafeFontSize } from '@djangocfg/ui-core/utils';
|
|
29
|
+
*
|
|
30
|
+
* <input style={{ fontSize: touchSafeFontSize('0.875rem') }} />
|
|
31
|
+
* // iOS: max(16px, 1em)
|
|
32
|
+
* // Android/Desktop: 0.875rem (14px)
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @example CSS-in-JS
|
|
36
|
+
* ```tsx
|
|
37
|
+
* const inputStyles = {
|
|
38
|
+
* fontSize: touchSafeFontSize('14px'),
|
|
39
|
+
* };
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example Tailwind arbitrary value
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <input className={`text-[${touchSafeFontSize('14px')}]`} />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function touchSafeFontSize(fallback: string = '1rem'): string {
|
|
48
|
+
return `var(--touch-safe-font-size, ${fallback})`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns touch-safe style object for inline styles.
|
|
53
|
+
*
|
|
54
|
+
* Convenience wrapper around touchSafeFontSize() that returns
|
|
55
|
+
* a complete style object ready for React inline styles.
|
|
56
|
+
*
|
|
57
|
+
* @param fallback - Fallback font-size for Android/Desktop
|
|
58
|
+
* @returns Style object with fontSize property
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* import { touchSafeStyle } from '@djangocfg/ui-core/utils';
|
|
63
|
+
*
|
|
64
|
+
* <textarea style={touchSafeStyle('14px')} />
|
|
65
|
+
* // iOS: { fontSize: 'max(16px, 1em)' }
|
|
66
|
+
* // Android/Desktop: { fontSize: '14px' }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @example Merge with existing styles
|
|
70
|
+
* ```tsx
|
|
71
|
+
* <input
|
|
72
|
+
* style={{
|
|
73
|
+
* ...myStyles,
|
|
74
|
+
* ...touchSafeStyle('0.875rem'),
|
|
75
|
+
* }}
|
|
76
|
+
* />
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function touchSafeStyle(fallback: string = '1rem'): { fontSize: string } {
|
|
80
|
+
return {
|
|
81
|
+
fontSize: touchSafeFontSize(fallback),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* CSS class name for touch-safe inputs (opt-in).
|
|
87
|
+
*
|
|
88
|
+
* Apply to a container to make all child inputs/textareas touch-safe
|
|
89
|
+
* on mobile devices.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* import { TOUCH_SAFE_INPUT_CLASS } from '@djangocfg/ui-core/utils';
|
|
93
|
+
* // className={TOUCH_SAFE_INPUT_CLASS}
|
|
94
|
+
*/
|
|
95
|
+
export const TOUCH_SAFE_INPUT_CLASS = 'touch-safe-input';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* CSS class name for mobile touch-safe inputs (iOS + Android).
|
|
99
|
+
*
|
|
100
|
+
* Apply when you want consistent 16px minimum across all mobile
|
|
101
|
+
* platforms, not just iOS.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* import { TOUCH_SAFE_MOBILE_CLASS } from '@djangocfg/ui-core/utils';
|
|
105
|
+
* // className={TOUCH_SAFE_MOBILE_CLASS}
|
|
106
|
+
*/
|
|
107
|
+
export const TOUCH_SAFE_MOBILE_CLASS = 'touch-safe-mobile';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* CSS class name for minimum 44px touch targets (WCAG AAA).
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* import { TOUCH_TARGET_CLASS } from '@djangocfg/ui-core/utils';
|
|
114
|
+
* // className={TOUCH_TARGET_CLASS}
|
|
115
|
+
*/
|
|
116
|
+
export const TOUCH_TARGET_CLASS = 'touch-target';
|