@likith99/react-native-adaptive-liquid-glass 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/AdaptiveLiquidGlass.podspec +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +127 -0
  4. package/android/build.gradle +21 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/adaptiveglass/ALGMenuManager.kt +31 -0
  7. package/android/src/main/java/com/adaptiveglass/ALGMenuView.kt +184 -0
  8. package/android/src/main/java/com/adaptiveglass/ALGSliderManager.kt +32 -0
  9. package/android/src/main/java/com/adaptiveglass/ALGSliderView.kt +132 -0
  10. package/android/src/main/java/com/adaptiveglass/ALGTabsManager.kt +24 -0
  11. package/android/src/main/java/com/adaptiveglass/ALGTabsView.kt +161 -0
  12. package/android/src/main/java/com/adaptiveglass/AdaptiveLiquidGlassPackage.kt +11 -0
  13. package/android/src/main/res/drawable/alg_tab_circle.xml +3 -0
  14. package/android/src/main/res/drawable/alg_tab_favorites.xml +3 -0
  15. package/android/src/main/res/drawable/alg_tab_home.xml +3 -0
  16. package/android/src/main/res/drawable/alg_tab_inbox.xml +3 -0
  17. package/android/src/main/res/drawable/alg_tab_library.xml +3 -0
  18. package/android/src/main/res/drawable/alg_tab_search.xml +3 -0
  19. package/android/src/main/res/drawable/alg_tab_settings.xml +3 -0
  20. package/ios/ALGActionClusterComponentView.h +3 -0
  21. package/ios/ALGActionClusterComponentView.mm +47 -0
  22. package/ios/ALGActionClusterView.swift +252 -0
  23. package/ios/ALGButtonComponentView.h +3 -0
  24. package/ios/ALGButtonComponentView.mm +37 -0
  25. package/ios/ALGButtonView.swift +113 -0
  26. package/ios/ALGMenuComponentView.h +3 -0
  27. package/ios/ALGMenuComponentView.mm +34 -0
  28. package/ios/ALGMenuView.swift +221 -0
  29. package/ios/ALGSegmentedComponentView.h +3 -0
  30. package/ios/ALGSegmentedComponentView.mm +36 -0
  31. package/ios/ALGSegmentedView.swift +58 -0
  32. package/ios/ALGSliderComponentView.h +3 -0
  33. package/ios/ALGSliderComponentView.mm +47 -0
  34. package/ios/ALGSliderView.swift +102 -0
  35. package/ios/ALGSurfaceComponentView.h +3 -0
  36. package/ios/ALGSurfaceComponentView.mm +45 -0
  37. package/ios/ALGSurfaceView.swift +108 -0
  38. package/ios/ALGTabsComponentView.h +3 -0
  39. package/ios/ALGTabsComponentView.mm +33 -0
  40. package/ios/ALGTabsView.swift +194 -0
  41. package/package.json +73 -0
  42. package/react-native.config.js +3 -0
  43. package/src/GlassActionCluster.ios.tsx +18 -0
  44. package/src/GlassActionCluster.tsx +1 -0
  45. package/src/GlassButton.ios.tsx +18 -0
  46. package/src/GlassButton.tsx +7 -0
  47. package/src/GlassContainer.ios.tsx +9 -0
  48. package/src/GlassContainer.tsx +6 -0
  49. package/src/GlassMenuButton.tsx +26 -0
  50. package/src/GlassPressable.tsx +17 -0
  51. package/src/GlassSegmentedControl.ios.tsx +21 -0
  52. package/src/GlassSegmentedControl.tsx +1 -0
  53. package/src/GlassSlider.tsx +24 -0
  54. package/src/GlassTabBar.tsx +25 -0
  55. package/src/GlassToolbar.tsx +25 -0
  56. package/src/GlassView.ios.tsx +13 -0
  57. package/src/GlassView.tsx +1 -0
  58. package/src/adaptiveHeight.ts +15 -0
  59. package/src/fallback/GlassActionCluster.tsx +25 -0
  60. package/src/fallback/GlassSegmentedControl.tsx +26 -0
  61. package/src/fallback/GlassView.tsx +12 -0
  62. package/src/fallback/NativeGlassButton.tsx +21 -0
  63. package/src/index.ts +17 -0
  64. package/src/menuTree.ts +35 -0
  65. package/src/sliderMath.ts +12 -0
  66. package/src/specs/ALGActionClusterNativeComponent.ts +15 -0
  67. package/src/specs/ALGButtonNativeComponent.ts +15 -0
  68. package/src/specs/ALGMenuNativeComponent.ts +17 -0
  69. package/src/specs/ALGSegmentedNativeComponent.ts +12 -0
  70. package/src/specs/ALGSliderNativeComponent.ts +19 -0
  71. package/src/specs/ALGSurfaceNativeComponent.ts +13 -0
  72. package/src/specs/ALGTabsNativeComponent.ts +11 -0
  73. package/src/support.ts +5 -0
  74. package/src/types.ts +181 -0
  75. package/src/validateActions.ts +10 -0
  76. package/src/validateSegments.ts +11 -0
  77. package/src/validateTabs.ts +17 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Default height for a native control host at the current system font scale.
3
+ *
4
+ * Native controls do not size Yoga from their intrinsic content, so each one needs an
5
+ * explicit React height. Only the text inside a control grows with the font scale; its
6
+ * icons, insets and touch padding do not. Scaling the whole default height therefore
7
+ * leaves a large empty box around the label at accessibility text sizes, so this grows
8
+ * only the share of the default height that the text occupies.
9
+ *
10
+ * `base` is the default height at font scale 1 and `text` is the line height the control
11
+ * gives its label there. A caller's explicit `style.height` still wins over this.
12
+ */
13
+ export default function adaptiveHeight(base: number, text: number, fontScale: number): number {
14
+ return Math.round(base + text * (Math.max(1, fontScale) - 1));
15
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import {Text, View, useColorScheme} from 'react-native';
3
+ import GlassButton from '../GlassButton';
4
+ import {validateActions} from '../validateActions';
5
+ import type {GlassActionClusterProps} from '../types';
6
+ export default function GlassActionCluster({actions, expanded, onExpandedChange, onAction,
7
+ toggleLabel = 'Actions', style, spacing: _spacing, tintColor: _tint, material: _material,
8
+ interactive: _interactive, animationDuration: _duration, forceFallback: _force,
9
+ mergingEnabled: _merging, pressFeedback: _pressFeedback, ...props}: GlassActionClusterProps) {
10
+ validateActions(actions);
11
+ const dark = useColorScheme() === 'dark';
12
+ const textStyle = {color: dark ? '#F5F5FA' : '#242630', fontWeight: '600' as const};
13
+ return <View {...props} accessible={false} style={[{minHeight: 80, flexDirection: 'row', flexWrap: 'wrap', gap: 12,
14
+ alignItems: 'center', justifyContent: 'flex-end', padding: 12}, style]}>
15
+ {expanded && actions.map(action => <GlassButton key={action.id} forceFallback
16
+ testID={`glass-action-${action.id}`} accessibilityLabel={action.title} disabled={action.disabled}
17
+ onPress={() => onAction(action.id)} contentStyle={{paddingHorizontal: 16}}>
18
+ <Text style={textStyle}>{action.title}</Text>
19
+ </GlassButton>)}
20
+ <GlassButton forceFallback testID="glass-cluster-toggle" accessibilityLabel={toggleLabel}
21
+ accessibilityState={{expanded}} onPress={() => onExpandedChange(!expanded)}>
22
+ <Text style={textStyle}>{expanded ? 'Close' : toggleLabel}</Text>
23
+ </GlassButton>
24
+ </View>;
25
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import {Pressable, Text, View, useColorScheme} from 'react-native';
3
+ import type {GlassSegmentedControlProps} from '../types';
4
+ import {validateSegments} from '../validateSegments';
5
+ export default function GlassSegmentedControl({options, value, onValueChange, disabled, tintColor,
6
+ colorScheme = 'system', forceFallback: _fallback, style, testID, ...props}: GlassSegmentedControlProps) {
7
+ validateSegments(options, value);
8
+ const system = useColorScheme();
9
+ const dark = (colorScheme === 'system' ? system : colorScheme) === 'dark';
10
+ return <View {...props} testID={testID} accessible={false}
11
+ style={[{flexDirection: 'row', alignItems: 'stretch', backgroundColor: dark ? '#242630' : '#E7E7EE', borderRadius: 16, padding: 4}, style]}>
12
+ {options.map(option => {
13
+ const inactive = !!(disabled || option.disabled);
14
+ const selected = option.value === value;
15
+ return <Pressable key={option.value} testID={testID ? `${testID}-${option.value}` : undefined}
16
+ accessibilityRole="radio" accessibilityLabel={option.label}
17
+ accessibilityState={{selected, checked: selected, disabled: inactive}} disabled={inactive}
18
+ android_ripple={{color: '#80808040', foreground: true}}
19
+ onPress={() => {if (!inactive && !selected) onValueChange(option.value);}}
20
+ style={{flex: 1, minHeight: 48, padding: 8, alignItems: 'center', justifyContent: 'center', borderRadius: 12,
21
+ overflow: 'hidden', opacity: inactive ? 0.4 : 1, backgroundColor: selected ? tintColor ?? (dark ? '#4B4D5A' : '#FFFFFF') : 'transparent'}}>
22
+ <Text style={{fontSize: 14, fontWeight: selected ? '700' : '500', textAlign: 'center', color: dark ? '#F4F4FA' : '#242630'}}>{option.label}</Text>
23
+ </Pressable>;
24
+ })}
25
+ </View>;
26
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import {View, useColorScheme} from 'react-native';
3
+ import type {GlassViewProps} from '../types';
4
+
5
+ export default function GlassView({material = 'regular', interactive: _interactive,
6
+ tintColor: _tint, cornerRadius = 24, animationDuration: _duration, colorScheme = 'system',
7
+ fallbackStyle, forceFallback: _force, style, ...props}: GlassViewProps) {
8
+ const system = useColorScheme();
9
+ const dark = (colorScheme === 'system' ? system : colorScheme) === 'dark';
10
+ return <View {...props} style={[{borderRadius: cornerRadius,
11
+ backgroundColor: material === 'none' ? 'transparent' : dark ? '#25272D' : '#F0F1F5'}, style, fallbackStyle]} />;
12
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import {ActivityIndicator, Pressable, Text, useColorScheme} from 'react-native';
3
+ import type {NativeGlassButtonProps} from '../types';
4
+ export default function NativeGlassButton({title, systemImage: _symbol, variant = 'regular', disabled,
5
+ loading, onPress, tintColor, colorScheme = 'system', forceFallback: _fallback, style,
6
+ accessibilityLabel, accessibilityState, ...props}: NativeGlassButtonProps) {
7
+ const scheme = useColorScheme();
8
+ const dark = (colorScheme === 'system' ? scheme : colorScheme) === 'dark';
9
+ const inactive = !!(disabled || loading);
10
+ const foreground = variant === 'prominent' ? '#FFFFFF' : dark ? '#F4F4FA' : '#242630';
11
+ return <Pressable {...props} accessibilityRole="button" accessibilityLabel={accessibilityLabel ?? title}
12
+ accessibilityState={{...accessibilityState, disabled: inactive, busy: !!loading}} disabled={inactive}
13
+ onPress={() => {if (!inactive) onPress();}} android_ripple={{color: '#80808040', foreground: true}}
14
+ style={[{minHeight: 56, borderRadius: 28, paddingVertical: 14, paddingHorizontal: 20,
15
+ flexDirection: 'row', gap: 10, alignItems: 'center', justifyContent: 'center', overflow: 'hidden',
16
+ backgroundColor: variant === 'prominent' ? tintColor ?? '#6159B7' : dark ? '#25272D' : '#F0F1F5',
17
+ opacity: disabled ? 0.45 : 1}, style]}>
18
+ {loading && <ActivityIndicator color={foreground} />}
19
+ <Text style={{color: foreground, fontSize: 16, fontWeight: '600', flexShrink: 1}}>{title}</Text>
20
+ </Pressable>;
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export {default as GlassView} from './GlassView';
2
+ export {default as GlassContainer} from './GlassContainer';
3
+ export {default as GlassButton} from './GlassButton';
4
+ export {default as GlassPressable} from './GlassPressable';
5
+ export {default as GlassSegmentedControl} from './GlassSegmentedControl';
6
+ export {default as GlassSlider} from './GlassSlider';
7
+ export type {GlassSliderProps} from './types';
8
+ export {default as GlassActionCluster} from './GlassActionCluster';
9
+ export {isLiquidGlassSupported} from './support';
10
+ export type {GlassViewProps, GlassContainerProps, GlassButtonProps, GlassAction, GlassActionClusterProps} from './types';
11
+ export type {NativeGlassButtonProps, GlassPressableProps, GlassSegment, GlassSegmentedControlProps} from './types';
12
+ export {default as GlassMenuButton} from './GlassMenuButton';
13
+ export type {GlassMenuButtonProps, GlassMenuItem} from './types';
14
+ export {default as GlassToolbar} from './GlassToolbar';
15
+ export type {GlassMenuElement, GlassMenuSubmenu, GlassMenuSection, GlassToolbarItem, GlassToolbarProps} from './types';
16
+ export {default as GlassTabBar} from './GlassTabBar';
17
+ export type {GlassTabBarProps, GlassTabItem, GlassTabIcon} from './types';
@@ -0,0 +1,35 @@
1
+ import type {GlassMenuElement, GlassMenuItem} from './types';
2
+
3
+ /** Bounded recursive contract shared by menu buttons and toolbar menus. */
4
+ export function validateMenuItems(items: readonly GlassMenuElement[]) {
5
+ const ids = new Set<string>();
6
+ const visit = (elements: readonly GlassMenuElement[], depth: number, submenuDepth: number) => {
7
+ if (depth > 8) throw new Error('Menus support at most 8 levels.');
8
+ for (const item of elements) {
9
+ if (!item.id.trim() || (item.kind !== 'section' && !item.title.trim())) {
10
+ throw new Error('Menu IDs and action/submenu titles must be nonempty.');
11
+ }
12
+ if (ids.has(item.id)) throw new Error('Menu IDs must be unique throughout the tree.');
13
+ ids.add(item.id);
14
+ if (ids.size > 256) throw new Error('Menus support at most 256 elements.');
15
+ if (item.kind === 'section' || item.kind === 'submenu') {
16
+ if (!item.items.length) throw new Error('Menu groups must contain items.');
17
+ const nextSubmenuDepth = submenuDepth + (item.kind === 'submenu' ? 1 : 0);
18
+ if (nextSubmenuDepth > 1) throw new Error('Menus support one submenu level across platforms.');
19
+ visit(item.items, depth + 1, nextSubmenuDepth);
20
+ }
21
+ }
22
+ };
23
+ visit(items, 1, 0);
24
+ }
25
+
26
+ /** Only enabled leaves can emit actions; a disabled submenu blocks all descendants. */
27
+ export function enabledMenuAction(items: readonly GlassMenuElement[], id: string): GlassMenuItem | undefined {
28
+ for (const item of items) {
29
+ if (item.kind === 'section' || item.kind === 'submenu') {
30
+ if (item.kind === 'submenu' && item.disabled) continue;
31
+ const action = enabledMenuAction(item.items, id);
32
+ if (action) return action;
33
+ } else if (item.id === id && !item.disabled) return item;
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ export function validateSlider(value: number, minimum: number, maximum: number, step: number) {
2
+ if (![value, minimum, maximum, step, maximum - minimum].every(Number.isFinite)) {
3
+ throw new Error('GlassSlider values and range must be finite numbers.');
4
+ }
5
+ if (maximum <= minimum) throw new Error('GlassSlider maximumValue must exceed minimumValue.');
6
+ if (step < 0 || step > maximum - minimum) throw new Error('GlassSlider step must be between 0 and the range.');
7
+ }
8
+ export function normalizeSliderValue(value: number, minimum: number, maximum: number, step: number) {
9
+ const clamped = Math.max(minimum, Math.min(maximum, value));
10
+ if (clamped === maximum || step === 0) return clamped;
11
+ return Math.max(minimum, Math.min(maximum, minimum + Math.round((clamped - minimum) / step) * step));
12
+ }
@@ -0,0 +1,15 @@
1
+ import {codegenNativeComponent, type ViewProps, type ColorValue, type CodegenTypes} from 'react-native';
2
+ export interface NativeProps extends ViewProps {
3
+ actionsJSON?: string;
4
+ expanded?: CodegenTypes.WithDefault<boolean, false>;
5
+ mergingEnabled?: CodegenTypes.WithDefault<boolean, false>;
6
+ spacing?: CodegenTypes.WithDefault<CodegenTypes.Float, 20>;
7
+ glassTint?: ColorValue;
8
+ material?: CodegenTypes.WithDefault<'regular' | 'clear', 'regular'>;
9
+ interactive?: CodegenTypes.WithDefault<boolean, true>;
10
+ duration?: CodegenTypes.WithDefault<CodegenTypes.Double, 0.45>;
11
+ toggleLabel?: string;
12
+ onAction?: CodegenTypes.DirectEventHandler<Readonly<{id: string}>>;
13
+ onExpandedChange?: CodegenTypes.DirectEventHandler<Readonly<{expanded: boolean}>>;
14
+ }
15
+ export default codegenNativeComponent<NativeProps>('ALGActionCluster', {excludedPlatforms: ['android']});
@@ -0,0 +1,15 @@
1
+ import {codegenNativeComponent, type ViewProps, type ColorValue, type CodegenTypes} from 'react-native';
2
+ export interface NativeProps extends ViewProps {
3
+ title: string;
4
+ systemImage?: string;
5
+ variant?: CodegenTypes.WithDefault<'regular' | 'prominent', 'regular'>;
6
+ disabled?: CodegenTypes.WithDefault<boolean, false>;
7
+ loading?: CodegenTypes.WithDefault<boolean, false>;
8
+ glassTint?: ColorValue;
9
+ colorScheme?: CodegenTypes.WithDefault<'system' | 'light' | 'dark', 'system'>;
10
+ controlLabel?: string;
11
+ controlHint?: string;
12
+ controlTestID?: string;
13
+ onActivate?: CodegenTypes.DirectEventHandler<Readonly<{activated: boolean}>>;
14
+ }
15
+ export default codegenNativeComponent<NativeProps>('ALGButton', {excludedPlatforms: ['android']});
@@ -0,0 +1,17 @@
1
+ import {codegenNativeComponent, type ViewProps, type ColorValue, type CodegenTypes} from 'react-native';
2
+ export interface NativeProps extends ViewProps {
3
+ title: string;
4
+ itemsJSON: string;
5
+ toolbar?: CodegenTypes.WithDefault<boolean, false>;
6
+ maxVisibleItems?: CodegenTypes.WithDefault<CodegenTypes.Int32, 3>;
7
+ mergingEnabled?: CodegenTypes.WithDefault<boolean, false>;
8
+ systemImage?: string;
9
+ disabled?: CodegenTypes.WithDefault<boolean, false>;
10
+ forceFallback?: CodegenTypes.WithDefault<boolean, false>;
11
+ glassTint?: ColorValue;
12
+ controlLabel?: string;
13
+ controlHint?: string;
14
+ controlTestID?: string;
15
+ onMenuAction?: CodegenTypes.DirectEventHandler<Readonly<{id: string}>>;
16
+ }
17
+ export default codegenNativeComponent<NativeProps>('ALGMenu');
@@ -0,0 +1,12 @@
1
+ import {codegenNativeComponent, type ViewProps, type ColorValue, type CodegenTypes} from 'react-native';
2
+ export interface NativeProps extends ViewProps {
3
+ optionsJSON: string;
4
+ selectedValue: string;
5
+ disabled?: CodegenTypes.WithDefault<boolean, false>;
6
+ glassTint?: ColorValue;
7
+ colorScheme?: CodegenTypes.WithDefault<'system' | 'light' | 'dark', 'system'>;
8
+ controlLabel?: string;
9
+ controlTestID?: string;
10
+ onSelectionChange?: CodegenTypes.DirectEventHandler<Readonly<{value: string}>>;
11
+ }
12
+ export default codegenNativeComponent<NativeProps>('ALGSegmented', {excludedPlatforms: ['android']});
@@ -0,0 +1,19 @@
1
+ import {codegenNativeComponent, type ViewProps, type ColorValue, type CodegenTypes} from 'react-native';
2
+ type SliderEvent = Readonly<{value: CodegenTypes.Double}>;
3
+ export interface NativeProps extends ViewProps {
4
+ value: CodegenTypes.Double;
5
+ minimumValue?: CodegenTypes.WithDefault<CodegenTypes.Double, 0>;
6
+ maximumValue?: CodegenTypes.WithDefault<CodegenTypes.Double, 1>;
7
+ step?: CodegenTypes.WithDefault<CodegenTypes.Double, 0>;
8
+ disabled?: CodegenTypes.WithDefault<boolean, false>;
9
+ glassTint?: ColorValue;
10
+ revision?: CodegenTypes.WithDefault<CodegenTypes.Int32, 0>;
11
+ controlLabel?: string;
12
+ controlHint?: string;
13
+ controlTestID?: string;
14
+ onSliderChange?: CodegenTypes.DirectEventHandler<SliderEvent>;
15
+ onSliderStart?: CodegenTypes.DirectEventHandler<SliderEvent>;
16
+ onSliderComplete?: CodegenTypes.DirectEventHandler<SliderEvent>;
17
+ onSliderCancel?: CodegenTypes.DirectEventHandler<SliderEvent>;
18
+ }
19
+ export default codegenNativeComponent<NativeProps>('ALGSlider');
@@ -0,0 +1,13 @@
1
+ import {codegenNativeComponent, type ViewProps, type ColorValue, type CodegenTypes} from 'react-native';
2
+ export interface NativeProps extends ViewProps {
3
+ material?: CodegenTypes.WithDefault<'regular' | 'clear' | 'none', 'regular'>;
4
+ interactive?: CodegenTypes.WithDefault<boolean, false>;
5
+ glassTint?: ColorValue;
6
+ glassRadius?: CodegenTypes.WithDefault<CodegenTypes.Float, 24>;
7
+ container?: CodegenTypes.WithDefault<boolean, false>;
8
+ mergingEnabled?: CodegenTypes.WithDefault<boolean, false>;
9
+ spacing?: CodegenTypes.WithDefault<CodegenTypes.Float, 20>;
10
+ animationDuration?: CodegenTypes.WithDefault<CodegenTypes.Double, 0.35>;
11
+ colorScheme?: CodegenTypes.WithDefault<'system' | 'light' | 'dark', 'system'>;
12
+ }
13
+ export default codegenNativeComponent<NativeProps>('ALGSurface', {excludedPlatforms: ['android']});
@@ -0,0 +1,11 @@
1
+ import {codegenNativeComponent, type ViewProps, type ColorValue, type CodegenTypes} from 'react-native';
2
+ export interface NativeProps extends ViewProps {
3
+ itemsJSON: string;
4
+ selectedValue: string;
5
+ selectionRevision?: CodegenTypes.WithDefault<CodegenTypes.Int32, 0>;
6
+ disabled?: CodegenTypes.WithDefault<boolean, false>;
7
+ glassTint?: ColorValue;
8
+ controlTestID?: string;
9
+ onSelectionChange?: CodegenTypes.DirectEventHandler<Readonly<{id: string}>>;
10
+ }
11
+ export default codegenNativeComponent<NativeProps>('ALGTabs');
package/src/support.ts ADDED
@@ -0,0 +1,5 @@
1
+ import {Platform} from 'react-native';
2
+ /** The package requires a native build with Xcode 26+. This is an OS capability check. */
3
+ export function isLiquidGlassSupported(): boolean {
4
+ return Platform.OS === 'ios' && Number.parseInt(String(Platform.Version), 10) >= 26;
5
+ }
package/src/types.ts ADDED
@@ -0,0 +1,181 @@
1
+ import type {ColorValue, PressableProps, StyleProp, ViewProps, ViewStyle} from 'react-native';
2
+ export interface GlassViewProps extends ViewProps {
3
+ material?: 'regular' | 'clear' | 'none';
4
+ interactive?: boolean;
5
+ tintColor?: ColorValue;
6
+ /** Uniform native shape radius. Use this instead of style.borderRadius. */
7
+ cornerRadius?: number;
8
+ /** Seconds, 0 disables material transitions. Reduce Motion takes precedence. */
9
+ animationDuration?: number;
10
+ colorScheme?: 'system' | 'light' | 'dark';
11
+ /** Surface style applied on older iOS, Android, or in forced fallback mode. */
12
+ fallbackStyle?: StyleProp<ViewStyle>;
13
+ /** Use an opaque React surface instead of native glass or older-iOS blur. */
14
+ forceFallback?: boolean;
15
+ }
16
+ export interface GlassContainerProps extends ViewProps {
17
+ /** Allow nearby glass surfaces to join. Defaults to false. */
18
+ mergingEnabled?: boolean;
19
+ /** Native merging threshold in points when enabled, not the layout gap. */
20
+ spacing?: number;
21
+ forceFallback?: boolean;
22
+ }
23
+ export interface GlassPressableProps extends Omit<PressableProps, 'style' | 'children'> {
24
+ title?: never;
25
+ children: React.ReactNode;
26
+ style?: StyleProp<ViewStyle>;
27
+ contentStyle?: StyleProp<ViewStyle>;
28
+ material?: GlassViewProps['material'];
29
+ tintColor?: ColorValue;
30
+ cornerRadius?: number;
31
+ fallbackStyle?: StyleProp<ViewStyle>;
32
+ forceFallback?: boolean;
33
+ }
34
+ /** SwiftUI owns the title/icon button on iOS 26+. */
35
+ export interface NativeGlassButtonProps extends Omit<ViewProps, 'children'> {
36
+ children?: never;
37
+ title: string;
38
+ systemImage?: string;
39
+ variant?: 'regular' | 'prominent';
40
+ disabled?: boolean;
41
+ loading?: boolean;
42
+ onPress: () => void;
43
+ tintColor?: ColorValue;
44
+ colorScheme?: 'system' | 'light' | 'dark';
45
+ forceFallback?: boolean;
46
+ }
47
+ /** Custom children remain supported for compatibility; prefer GlassPressable for them. */
48
+ export type GlassButtonProps = NativeGlassButtonProps | GlassPressableProps;
49
+ export interface GlassSegment {
50
+ value: string;
51
+ label: string;
52
+ disabled?: boolean;
53
+ }
54
+ export interface GlassSegmentedControlProps extends Omit<ViewProps, 'children'> {
55
+ options: readonly GlassSegment[];
56
+ value: string | null;
57
+ onValueChange: (value: string) => void;
58
+ disabled?: boolean;
59
+ tintColor?: ColorValue;
60
+ colorScheme?: 'system' | 'light' | 'dark';
61
+ forceFallback?: boolean;
62
+ }
63
+ export interface GlassSliderProps extends Omit<ViewProps, 'children'> {
64
+ /** Controlled value; clamped to the range and snapped to step. */
65
+ value: number;
66
+ onValueChange: (value: number) => void;
67
+ minimumValue?: number;
68
+ maximumValue?: number;
69
+ /** 0 means continuous; positive steps are anchored at minimumValue. */
70
+ step?: number;
71
+ disabled?: boolean;
72
+ tintColor?: ColorValue;
73
+ onSlidingStart?: (value: number) => void;
74
+ onSlidingComplete?: (value: number) => void;
75
+ /** Cancellation restores the latest controlled value; it is not completion. */
76
+ onSlidingCancel?: (value: number) => void;
77
+ }
78
+ export interface GlassAction {
79
+ /** Stable unique identity used to retain the native action control. '__toggle' is reserved. */
80
+ id: string;
81
+ title: string;
82
+ /** SF Symbol name on iOS. Android uses title. */
83
+ systemImage?: string;
84
+ disabled?: boolean;
85
+ }
86
+ export interface GlassActionClusterProps extends Omit<ViewProps, 'children'> {
87
+ actions: readonly GlassAction[];
88
+ expanded: boolean;
89
+ onExpandedChange: (expanded: boolean) => void;
90
+ onAction: (id: string) => void;
91
+ /** Enable shared glass merging and morphing. Defaults to false. */
92
+ mergingEnabled?: boolean;
93
+ spacing?: number;
94
+ /** Native material tint. Default: neutral dark-mode shading; overrides replace it. */
95
+ tintColor?: ColorValue;
96
+ material?: 'regular' | 'clear';
97
+ interactive?: boolean;
98
+ /** @deprecated Ignored. Native glass feedback follows interactive; custom feedback was removed. */
99
+ pressFeedback?: 'subtle' | 'native';
100
+ animationDuration?: number;
101
+ toggleLabel?: string;
102
+ forceFallback?: boolean;
103
+ }
104
+
105
+ export interface GlassMenuItem {
106
+ kind?: 'action';
107
+ id: string;
108
+ title: string;
109
+ /** SF Symbol on iOS; Android uses the title. */
110
+ systemImage?: string;
111
+ disabled?: boolean;
112
+ destructive?: boolean;
113
+ /** Controlled checkmark. Update items in onAction to change selection. */
114
+ checked?: boolean;
115
+ }
116
+ export interface GlassMenuSubmenu {
117
+ kind: 'submenu';
118
+ id: string;
119
+ title: string;
120
+ systemImage?: string;
121
+ disabled?: boolean;
122
+ items: readonly GlassMenuElement[];
123
+ }
124
+ export interface GlassMenuSection {
125
+ kind: 'section';
126
+ id: string;
127
+ /** Empty title creates an untitled group. */
128
+ title: string;
129
+ items: readonly GlassMenuElement[];
130
+ }
131
+ export type GlassMenuElement = GlassMenuItem | GlassMenuSubmenu | GlassMenuSection;
132
+ export type GlassToolbarItem = (GlassMenuItem | GlassMenuSubmenu) & {
133
+ /** Automatic items may move into overflow when space is limited. */
134
+ placement?: 'automatic' | 'overflow';
135
+ };
136
+ export interface GlassToolbarProps extends Omit<ViewProps, 'children'> {
137
+ items: readonly GlassToolbarItem[];
138
+ onAction: (id: string) => void;
139
+ disabled?: boolean;
140
+ /** Upper limit, not a guarantee. 0 places all items in overflow. Default 3. */
141
+ maxVisibleItems?: number;
142
+ /** Allow adjacent iOS toolbar items to share a native background. Default false. */
143
+ mergingEnabled?: boolean;
144
+ tintColor?: ColorValue;
145
+ forceFallback?: boolean;
146
+ }
147
+ export interface GlassMenuButtonProps extends Omit<ViewProps, 'children'> {
148
+ title: string;
149
+ items: readonly GlassMenuElement[];
150
+ onAction: (id: string) => void;
151
+ systemImage?: string;
152
+ disabled?: boolean;
153
+ /** Button foreground accent, not menu material tint. */
154
+ tintColor?: ColorValue;
155
+ /** Preview the standard iOS button. Android always uses its standard button. */
156
+ forceFallback?: boolean;
157
+ }
158
+
159
+ export type GlassTabIcon = 'home' | 'search' | 'library' | 'favorites' | 'inbox' | 'settings';
160
+ export interface GlassTabItem {
161
+ id: string;
162
+ title: string;
163
+ /** Built-in platform icon preset; defaults to a circle when absent. */
164
+ icon?: GlassTabIcon;
165
+ systemImage?: string;
166
+ selectedSystemImage?: string;
167
+ /** Drawable resource name in the consuming Android application. */
168
+ androidIcon?: string;
169
+ badge?: number | 'dot';
170
+ disabled?: boolean;
171
+ accessibilityLabel?: string;
172
+ }
173
+ export interface GlassTabBarProps extends Omit<ViewProps, 'children'> {
174
+ items: readonly GlassTabItem[];
175
+ /** Required for nonempty items. Set null only when items are empty. */
176
+ value: string | null;
177
+ onValueChange: (id: string) => void;
178
+ onTabReselect?: (id: string) => void;
179
+ disabled?: boolean;
180
+ tintColor?: ColorValue;
181
+ }
@@ -0,0 +1,10 @@
1
+ import type {GlassAction} from './types';
2
+ export function validateActions(actions: readonly GlassAction[]): void {
3
+ const ids = new Set<string>();
4
+ for (const action of actions) {
5
+ if (!action.id || action.id === '__toggle' || ids.has(action.id)) {
6
+ throw new Error('GlassActionCluster requires unique, nonempty action IDs; __toggle is reserved.');
7
+ }
8
+ ids.add(action.id);
9
+ }
10
+ }
@@ -0,0 +1,11 @@
1
+ import type {GlassSegment} from './types';
2
+ export function validateSegments(options: readonly GlassSegment[], value: string | null) {
3
+ if (!options.length) throw new Error('GlassSegmentedControl requires at least one option.');
4
+ const values = new Set<string>();
5
+ for (const option of options) {
6
+ if (!option.value || values.has(option.value)) throw new Error('Segment values must be unique and nonempty.');
7
+ if (!option.label.trim()) throw new Error('Segments require a nonempty label.');
8
+ values.add(option.value);
9
+ }
10
+ if (value !== null && !values.has(value)) throw new Error('Selected value must match an option or be null.');
11
+ }
@@ -0,0 +1,17 @@
1
+ import type {GlassTabItem} from './types';
2
+ export function validateTabs(items: readonly GlassTabItem[], value: string | null) {
3
+ if (items.length > 5) throw new Error('GlassTabBar supports at most 5 tabs.');
4
+ const ids = new Set<string>();
5
+ for (const item of items) {
6
+ if (!item.id.trim() || !item.title.trim()) throw new Error('Tab IDs and titles must be nonempty.');
7
+ if (ids.has(item.id)) throw new Error('Tab IDs must be unique.');
8
+ ids.add(item.id);
9
+ if (item.badge !== undefined && item.badge !== 'dot' &&
10
+ (!Number.isInteger(item.badge) || item.badge < 0 || item.badge > 2147483647)) {
11
+ throw new Error('Tab badge must be dot or a nonnegative 32-bit integer.');
12
+ }
13
+ }
14
+ if (items.length === 0 ? value !== null : value === null || !ids.has(value)) {
15
+ throw new Error('Tab value must identify an existing tab, or be null for empty items.');
16
+ }
17
+ }