@aberhamm/liquid-glass-react 0.2.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.
@@ -0,0 +1 @@
1
+ .lg-content{display:inline-flex;align-items:center;justify-content:center;gap:.5em;color:var(--lg-fg, rgba(17, 17, 19, .92));text-shadow:0 1px 1px rgba(255,255,255,.45);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}@media(prefers-color-scheme:dark){.lg-content{color:var(--lg-fg, rgba(245, 245, 247, .94));text-shadow:0 1px 2px rgba(0,0,0,.55)}}.lg-button{position:relative;-moz-appearance:none;appearance:none;-webkit-appearance:none;border:0;margin:0;background:transparent;font:inherit;font-weight:600;line-height:1.2;letter-spacing:.01em;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:var(--lg-fg, rgba(17, 17, 19, .92));text-decoration:none;border-radius:inherit;transition:filter .16s ease,opacity .16s ease}.lg-button:hover:not(:disabled){filter:brightness(1.04)}.lg-button:focus-visible{outline:2px solid var(--lg-focus-ring, rgba(60, 130, 255, .9));outline-offset:2px;box-shadow:0 0 0 4px #3c82ff40}.lg-button:disabled{cursor:not-allowed;opacity:.5;filter:saturate(.7)}@media(prefers-contrast:more){.lg-content{color:var(--lg-fg, rgba(0, 0, 0, 1));text-shadow:none}.lg-button:focus-visible,.lg-segmented-option:has(.lg-segmented-input:focus-visible){outline:3px solid var(--lg-focus-ring, rgba(0, 90, 255, 1));outline-offset:2px;box-shadow:0 0 0 5px #ffffffe6}}@media(prefers-contrast:more)and (prefers-color-scheme:dark){.lg-content{color:var(--lg-fg, rgba(255, 255, 255, 1));text-shadow:none}.lg-button:focus-visible,.lg-segmented-option:has(.lg-segmented-input:focus-visible){box-shadow:0 0 0 5px #000000e6}}.lg-button[data-shine=true]:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;border-radius:inherit;pointer-events:none;background:linear-gradient(105deg,#fff0 30%,#ffffff8c,#fff0 70%);background-size:250% 100%;background-position:150% 0;opacity:0}.lg-button[data-shine=true]:active:not(:disabled):after{animation:lg-shine .48s ease-out}@keyframes lg-shine{0%{background-position:150% 0;opacity:0}20%{opacity:1}to{background-position:-50% 0;opacity:0}}@media(prefers-reduced-motion:reduce){.lg-button[data-shine=true]:active:not(:disabled):after{animation:none}}.lg-button--primary{--lg-fg: rgba(255, 255, 255, .98);text-shadow:0 1px 2px rgba(0,0,0,.35)}.lg-button--secondary{--lg-fg: rgba(20, 22, 28, .92)}.lg-button--subtle{--lg-fg: rgba(40, 42, 50, .82);font-weight:500}@media(prefers-color-scheme:dark){.lg-button--secondary{--lg-fg: rgba(240, 242, 248, .94)}.lg-button--subtle{--lg-fg: rgba(225, 227, 235, .85)}}.lg-button--sm{font-size:.8125rem}.lg-button--md{font-size:.9375rem}.lg-button--lg{font-size:1.0625rem}.lg-button--icon{font-size:1rem}.lg-button--icon .lg-content{width:1.25em;height:1.25em;gap:0}.lg-card{position:relative;display:block;border-radius:inherit}.lg-card .lg-content{display:block;text-align:left;text-shadow:none;color:var(--lg-fg, rgba(20, 22, 28, .9))}@media(prefers-color-scheme:dark){.lg-card .lg-content{color:var(--lg-fg, rgba(238, 240, 246, .92))}}.lg-card--flat{--lg-card-lift: none}.lg-card--raised{--lg-card-lift: 0 10px 30px rgba(0, 0, 0, .16)}.lg-card--floating{--lg-card-lift: 0 22px 60px rgba(0, 0, 0, .24)}.lg-card[data-elevation]:not(.lg-card--flat){filter:drop-shadow(var(--lg-card-lift, none))}.lg-segmented{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;margin:0;padding:0;min-inline-size:0;display:inline-flex;flex-direction:column;gap:.5rem;--lg-seg-pad: .25rem;--lg-seg-radius: 999px;font:inherit}.lg-segmented-legend{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.lg-segmented-legend--visible{position:static;width:auto;height:auto;margin:0;clip:auto;overflow:visible;white-space:normal;font-size:.8125rem;font-weight:600;letter-spacing:.02em;color:var(--lg-fg, rgba(20, 22, 28, .82))}@media(prefers-color-scheme:dark){.lg-segmented-legend--visible{color:var(--lg-fg, rgba(232, 234, 240, .86))}}.lg-segmented-track{position:relative;display:inline-flex;align-items:stretch;padding:var(--lg-seg-pad);border-radius:var(--lg-seg-radius);background:#ffffff1f;box-shadow:var(--lg-edge-shadow);isolation:isolate;-webkit-tap-highlight-color:transparent}@media(prefers-color-scheme:dark){.lg-segmented-track{background:#14161c38}}.lg-segmented-indicator{position:absolute;top:var(--lg-seg-pad);left:var(--lg-seg-pad);bottom:var(--lg-seg-pad);border-radius:var(--lg-seg-radius);z-index:0;pointer-events:none;will-change:transform,width}.lg-segmented-glass{border-radius:var(--lg-seg-radius)}.lg-segmented-glass-fill{display:block;width:100%;height:100%}.lg-segmented-option{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;gap:.4em;flex:1 1 auto;border-radius:var(--lg-seg-radius);cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap;font-weight:600;letter-spacing:.01em;color:var(--lg-fg, rgba(40, 42, 50, .7));transition:color .2s ease}.lg-segmented-option[data-active=true]{color:var(--lg-fg-active, rgba(17, 17, 19, .96))}@media(prefers-color-scheme:dark){.lg-segmented-option{color:var(--lg-fg, rgba(220, 222, 230, .72))}.lg-segmented-option[data-active=true]{color:var(--lg-fg-active, rgba(248, 249, 252, .98))}}.lg-segmented-input{position:absolute;width:1px;height:1px;padding:0;margin:0;opacity:0;pointer-events:none;clip:rect(0,0,0,0)}.lg-segmented-input:focus-visible+.lg-segmented-icon,.lg-segmented-input:focus-visible~.lg-segmented-label,.lg-segmented-option:focus-within{outline:none}.lg-segmented-option:has(.lg-segmented-input:focus-visible){outline:2px solid var(--lg-focus-ring, rgba(60, 130, 255, .9));outline-offset:2px;box-shadow:0 0 0 4px #3c82ff40}.lg-segmented-option:has(.lg-segmented-input:disabled){cursor:not-allowed;opacity:.45}.lg-segmented-icon{display:inline-flex;align-items:center;justify-content:center;line-height:0}.lg-segmented-option[data-icon-only=true]{aspect-ratio:1 / 1}.lg-segmented--sm{font-size:.8125rem}.lg-segmented--sm .lg-segmented-option{padding:.3rem .7rem}.lg-segmented--md{font-size:.9375rem}.lg-segmented--md .lg-segmented-option{padding:.45rem 1rem}.lg-segmented--lg{font-size:1.0625rem}.lg-segmented--lg .lg-segmented-option{padding:.6rem 1.35rem}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * React Native entry point for @aberhamm/liquid-glass-react (plan 025).
3
+ *
4
+ * Metro resolves this via the `react-native` export condition (see
5
+ * `package.json`). It barrels ONLY the platform-neutral surface: the native
6
+ * `<LiquidGlass>` primitive, the shared design tokens (plan 022), and the
7
+ * platform-neutral public types.
8
+ *
9
+ * It deliberately does NOT re-export the DOM-only prebuilt components
10
+ * (`GlassButton` / `GlassCard` / `GlassSegmentedControl`) or the DOM-only hooks
11
+ * (capability detection, backdrop-luminance sampling, mouse tracking), so Metro
12
+ * never pulls web-only code (`document`, `window`, `backdrop-filter`, SVG) into a
13
+ * native bundle. Native ports of the prebuilt components are out of scope for
14
+ * this round (primitive only).
15
+ */
16
+ export declare const VERSION = "0.0.0";
17
+ export { LiquidGlass, type LiquidGlassNativeProps } from './liquid-glass.native';
18
+ export type { DisplacementMode, GlassVariant, LiquidGlassCoreProps, MousePos, } from './types';
19
+ export * from './tokens';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * React Native entry point for @aberhamm/liquid-glass-react (plan 025).
3
+ *
4
+ * Metro resolves this via the `react-native` export condition (see
5
+ * `package.json`). It barrels ONLY the platform-neutral surface: the native
6
+ * `<LiquidGlass>` primitive, the shared design tokens (plan 022), and the
7
+ * platform-neutral public types.
8
+ *
9
+ * It deliberately does NOT re-export the DOM-only prebuilt components
10
+ * (`GlassButton` / `GlassCard` / `GlassSegmentedControl`) or the DOM-only hooks
11
+ * (capability detection, backdrop-luminance sampling, mouse tracking), so Metro
12
+ * never pulls web-only code (`document`, `window`, `backdrop-filter`, SVG) into a
13
+ * native bundle. Native ports of the prebuilt components are out of scope for
14
+ * this round (primitive only).
15
+ */
16
+ export const VERSION = '0.0.0';
17
+ export { LiquidGlass } from './liquid-glass.native';
18
+ // Shared design tokens (plan 022) — the same source of truth the web build uses.
19
+ export * from './tokens';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `<LiquidGlass>` — React Native surface (plan 025).
3
+ *
4
+ * The web `<LiquidGlass>` (`liquid-glass.tsx`) renders a tiered
5
+ * `backdrop-filter` recipe with an OPT-IN, Chromium-only SVG refraction pass.
6
+ * React Native has no DOM, no `backdrop-filter`, and no SVG-over-backdrop
7
+ * compositing, so this surface renders ONLY the portable frosted recipe — the
8
+ * same look the web file falls back to on every non-Chromium engine — built from
9
+ * real native primitives:
10
+ *
11
+ * 1. an `expo-blur` `BlurView` for the frosted backdrop (a real iOS
12
+ * `UIVisualEffectView`; on Android `experimentalBlurMethod` approximates it
13
+ * and degrades gracefully);
14
+ * 2. a white TINT overlay (`FROSTED_DEFAULT_FILL`) that gives the surface its
15
+ * milky body — alpha is raised slightly on Android where the blur is weaker
16
+ * so the surface never reads as a bare transparent box;
17
+ * 3. an `expo-linear-gradient` SHEEN highlight (top-biased specular, from the
18
+ * `SPECULAR_*` tokens);
19
+ * 4. a ~1px hairline ring (`FROSTED_HAIRLINE_RING`);
20
+ * 5. a soft drop shadow (iOS `shadow*` / Android `elevation`) from
21
+ * `SHADOW_LAYERS`.
22
+ *
23
+ * Every geometry/color value comes from `src/tokens.ts`, the one source of truth
24
+ * shared with the web file — this module only SERIALIZES those values into RN
25
+ * style objects (the RN analogue of the web file owning the CSS strings).
26
+ *
27
+ * ## Prop contract
28
+ *
29
+ * Typed on the platform-neutral {@link LiquidGlassCoreProps} (plan 022) plus
30
+ * native-appropriate `style?: ViewStyle` and `onPress?` — it NEVER references the
31
+ * web `LiquidGlassProps`'s DOM-only style/event types. The event handler is
32
+ * platform-idiomatic: native exposes `onPress` (web keeps `onClick`).
33
+ *
34
+ * The web-only refraction/motion props are accepted for a single cross-platform
35
+ * API contract but are DOCUMENTED NO-OPs here — native always renders frosted:
36
+ * `displacement`, `displacementScale`, `aberrationIntensity`, `elasticity`,
37
+ * `saturation` (no native backdrop-saturation control), `overLight`,
38
+ * `adaptiveTint`, `scrollAwareShadow`, `variant`, `mode`, `className`, and the
39
+ * plan-030 cross-browser copy-refraction props `refract`, `size`, `center`,
40
+ * `behind` (RN has no SVG element filters, so there is no copy-refraction path).
41
+ */
42
+ import type { ReactNode } from 'react';
43
+ import { type ViewStyle } from 'react-native';
44
+ import type { LiquidGlassCoreProps } from './types';
45
+ /**
46
+ * Props for the React Native `<LiquidGlass>` surface: the platform-neutral core
47
+ * (plan 022) plus native `style` and an `onPress` handler. Distinct from the web
48
+ * `LiquidGlassProps`, which carries DOM-only style/event types.
49
+ */
50
+ export type LiquidGlassNativeProps = LiquidGlassCoreProps & {
51
+ /** Style merged onto the outermost native `View`. */
52
+ style?: ViewStyle;
53
+ /** Press handler (native-idiomatic; the web surface uses `onClick`). */
54
+ onPress?: () => void;
55
+ };
56
+ /**
57
+ * The React Native liquid-glass primitive. See the file header for the layer
58
+ * stack, the token mapping, and the web-only prop no-ops.
59
+ *
60
+ * The web-only props (`displacement`, `saturation`, `variant`, `mode`, `refract`,
61
+ * `size`, `center`, `behind`, etc.) are intentionally NOT read — they're accepted
62
+ * so the cross-platform API is uniform, but native always renders the frosted
63
+ * recipe.
64
+ */
65
+ export declare function LiquidGlass({ children, blurAmount, cornerRadius, padding, style, onPress, }: LiquidGlassNativeProps): ReactNode;
@@ -0,0 +1,200 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * `<LiquidGlass>` — React Native surface (plan 025).
4
+ *
5
+ * The web `<LiquidGlass>` (`liquid-glass.tsx`) renders a tiered
6
+ * `backdrop-filter` recipe with an OPT-IN, Chromium-only SVG refraction pass.
7
+ * React Native has no DOM, no `backdrop-filter`, and no SVG-over-backdrop
8
+ * compositing, so this surface renders ONLY the portable frosted recipe — the
9
+ * same look the web file falls back to on every non-Chromium engine — built from
10
+ * real native primitives:
11
+ *
12
+ * 1. an `expo-blur` `BlurView` for the frosted backdrop (a real iOS
13
+ * `UIVisualEffectView`; on Android `experimentalBlurMethod` approximates it
14
+ * and degrades gracefully);
15
+ * 2. a white TINT overlay (`FROSTED_DEFAULT_FILL`) that gives the surface its
16
+ * milky body — alpha is raised slightly on Android where the blur is weaker
17
+ * so the surface never reads as a bare transparent box;
18
+ * 3. an `expo-linear-gradient` SHEEN highlight (top-biased specular, from the
19
+ * `SPECULAR_*` tokens);
20
+ * 4. a ~1px hairline ring (`FROSTED_HAIRLINE_RING`);
21
+ * 5. a soft drop shadow (iOS `shadow*` / Android `elevation`) from
22
+ * `SHADOW_LAYERS`.
23
+ *
24
+ * Every geometry/color value comes from `src/tokens.ts`, the one source of truth
25
+ * shared with the web file — this module only SERIALIZES those values into RN
26
+ * style objects (the RN analogue of the web file owning the CSS strings).
27
+ *
28
+ * ## Prop contract
29
+ *
30
+ * Typed on the platform-neutral {@link LiquidGlassCoreProps} (plan 022) plus
31
+ * native-appropriate `style?: ViewStyle` and `onPress?` — it NEVER references the
32
+ * web `LiquidGlassProps`'s DOM-only style/event types. The event handler is
33
+ * platform-idiomatic: native exposes `onPress` (web keeps `onClick`).
34
+ *
35
+ * The web-only refraction/motion props are accepted for a single cross-platform
36
+ * API contract but are DOCUMENTED NO-OPs here — native always renders frosted:
37
+ * `displacement`, `displacementScale`, `aberrationIntensity`, `elasticity`,
38
+ * `saturation` (no native backdrop-saturation control), `overLight`,
39
+ * `adaptiveTint`, `scrollAwareShadow`, `variant`, `mode`, `className`, and the
40
+ * plan-030 cross-browser copy-refraction props `refract`, `size`, `center`,
41
+ * `behind` (RN has no SVG element filters, so there is no copy-refraction path).
42
+ */
43
+ import { BlurView } from 'expo-blur';
44
+ import { LinearGradient } from 'expo-linear-gradient';
45
+ import { Platform, Pressable, StyleSheet, View } from 'react-native';
46
+ import { BLUR_PX_PER_UNIT, DEFAULTS, FROSTED_DEFAULT_FILL, FROSTED_HAIRLINE_RING, SHADOW_LAYERS, SPECULAR_CORE_OPACITY, } from './tokens';
47
+ /**
48
+ * Maps the small public `blurAmount` (a rem-ish factor, default `0.0625`) onto an
49
+ * `expo-blur` `intensity` (0–100). The web file turns `blurAmount` into a px blur
50
+ * via {@link BLUR_PX_PER_UNIT}; native has no px blur radius, so we reuse that
51
+ * same px figure and scale it into the intensity scale. Default `blurAmount`
52
+ * lands at ~`intensity: 25`, a legible frosted default; larger values ramp up and
53
+ * saturate at 100.
54
+ */
55
+ const NATIVE_BLUR_INTENSITY_SCALE = 25;
56
+ /**
57
+ * Extra tint alpha added on Android, where `expo-blur` (even with
58
+ * `experimentalBlurMethod`) blurs less convincingly than the iOS
59
+ * `UIVisualEffectView`. Raising the fill keeps the surface legible instead of a
60
+ * near-transparent box — the documented Android degradation boundary.
61
+ */
62
+ const ANDROID_TINT_ALPHA_BOOST = 0.12;
63
+ function clamp(value, min, max) {
64
+ return Math.min(Math.max(value, min), max);
65
+ }
66
+ /** Coerce the public `cornerRadius` (number | CSS length string) to an RN radius. */
67
+ function toRadius(value) {
68
+ if (typeof value === 'number')
69
+ return value;
70
+ const parsed = Number.parseFloat(value);
71
+ return Number.isFinite(parsed) ? parsed : Number(DEFAULTS.cornerRadius) || 0;
72
+ }
73
+ /**
74
+ * Coerce the public `padding` (number | CSS shorthand string like `'24px 32px'`)
75
+ * to an RN padding style. Percentage/keyword tokens fall back to a numeric
76
+ * default — RN paddings must be numbers.
77
+ */
78
+ function toPadding(value) {
79
+ if (typeof value === 'number')
80
+ return { padding: value };
81
+ const parts = value
82
+ .trim()
83
+ .split(/\s+/)
84
+ .map((token) => Number.parseFloat(token))
85
+ .filter((n) => Number.isFinite(n));
86
+ if (parts.length === 1)
87
+ return { padding: parts[0] ?? 0 };
88
+ if (parts.length === 2) {
89
+ return { paddingVertical: parts[0] ?? 0, paddingHorizontal: parts[1] ?? 0 };
90
+ }
91
+ if (parts.length === 3) {
92
+ return {
93
+ paddingTop: parts[0] ?? 0,
94
+ paddingHorizontal: parts[1] ?? 0,
95
+ paddingBottom: parts[2] ?? 0,
96
+ };
97
+ }
98
+ if (parts.length >= 4) {
99
+ return {
100
+ paddingTop: parts[0] ?? 0,
101
+ paddingRight: parts[1] ?? 0,
102
+ paddingBottom: parts[2] ?? 0,
103
+ paddingLeft: parts[3] ?? 0,
104
+ };
105
+ }
106
+ return { padding: 24 };
107
+ }
108
+ /** Parse an `rgb()/rgba()` string into channels, or `null` if it isn't one. */
109
+ function parseRgba(color) {
110
+ const match = color.match(/rgba?\(([^)]+)\)/i);
111
+ if (!match || !match[1])
112
+ return null;
113
+ const parts = match[1].split(',').map((token) => Number.parseFloat(token.trim()));
114
+ if (parts.length < 3)
115
+ return null;
116
+ return { r: parts[0] ?? 0, g: parts[1] ?? 0, b: parts[2] ?? 0, a: parts[3] ?? 1 };
117
+ }
118
+ /**
119
+ * The frosted tint fill. iOS uses the shared {@link FROSTED_DEFAULT_FILL}; Android
120
+ * raises the alpha by {@link ANDROID_TINT_ALPHA_BOOST} for legibility under its
121
+ * weaker blur. Falls back to the raw token if the color can't be parsed.
122
+ */
123
+ function resolveTintFill() {
124
+ if (Platform.OS !== 'android')
125
+ return FROSTED_DEFAULT_FILL;
126
+ const parsed = parseRgba(FROSTED_DEFAULT_FILL);
127
+ if (!parsed)
128
+ return FROSTED_DEFAULT_FILL;
129
+ const alpha = clamp(parsed.a + ANDROID_TINT_ALPHA_BOOST, 0, 1);
130
+ return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${alpha})`;
131
+ }
132
+ const styles = StyleSheet.create({
133
+ // Inline-block-ish container: shrinks to content, carries padding + shadow. It
134
+ // does NOT clip (no overflow:hidden) so the iOS shadow / Android elevation is
135
+ // not cut off; the blur is clipped by the inner `clip` layer instead.
136
+ container: {
137
+ position: 'relative',
138
+ alignSelf: 'flex-start',
139
+ shadowColor: '#000',
140
+ },
141
+ // Absolutely-filled clip layer: rounds + clips the BlurView/tint/sheen to the
142
+ // corner radius and carries the hairline ring. A sibling BEHIND the content.
143
+ clip: {
144
+ ...StyleSheet.absoluteFillObject,
145
+ overflow: 'hidden',
146
+ borderWidth: FROSTED_HAIRLINE_RING.width,
147
+ borderColor: FROSTED_HAIRLINE_RING.color,
148
+ },
149
+ fill: StyleSheet.absoluteFillObject,
150
+ // Content sits above the surface layers, unaffected by the blur/tint/sheen.
151
+ content: {
152
+ position: 'relative',
153
+ zIndex: 1,
154
+ },
155
+ });
156
+ /**
157
+ * The React Native liquid-glass primitive. See the file header for the layer
158
+ * stack, the token mapping, and the web-only prop no-ops.
159
+ *
160
+ * The web-only props (`displacement`, `saturation`, `variant`, `mode`, `refract`,
161
+ * `size`, `center`, `behind`, etc.) are intentionally NOT read — they're accepted
162
+ * so the cross-platform API is uniform, but native always renders the frosted
163
+ * recipe.
164
+ */
165
+ export function LiquidGlass({ children, blurAmount = DEFAULTS.blurAmount, cornerRadius = DEFAULTS.cornerRadius, padding = DEFAULTS.padding, style, onPress, }) {
166
+ const radius = toRadius(cornerRadius);
167
+ // Reuse the web px-blur mapping (blurAmount * BLUR_PX_PER_UNIT) then project it
168
+ // onto expo-blur's 0–100 intensity scale.
169
+ const blurPx = blurAmount * BLUR_PX_PER_UNIT;
170
+ const intensity = clamp(Math.round(blurPx * NATIVE_BLUR_INTENSITY_SCALE), 0, 100);
171
+ const tintFill = resolveTintFill();
172
+ // Top-biased specular sheen mirroring the web highlight's rest stops
173
+ // (SPECULAR_REST at top-center): bright core → mid → transparent.
174
+ const core = SPECULAR_CORE_OPACITY.rest;
175
+ const sheenColors = [
176
+ `rgba(255, 255, 255, ${core})`,
177
+ `rgba(255, 255, 255, ${(core * 0.4).toFixed(3)})`,
178
+ 'rgba(255, 255, 255, 0)',
179
+ ];
180
+ // Primary drop-shadow layer (RN supports a single shadow; take the base layer).
181
+ // `SHADOW_LAYERS.rest` is a fixed `as const` tuple, so index 0 is always present.
182
+ const { y: shadowY, blur: shadowBlur, alpha: shadowAlpha } = SHADOW_LAYERS.rest[0];
183
+ const containerStyle = {
184
+ borderRadius: radius,
185
+ // iOS shadow; Android elevation needs a backgroundColor to render — it is
186
+ // fully covered by the absolutely-filled clip layer, so it is never visible.
187
+ shadowOffset: { width: 0, height: shadowY },
188
+ shadowOpacity: shadowAlpha,
189
+ shadowRadius: shadowBlur,
190
+ elevation: Math.round(shadowY),
191
+ ...(Platform.OS === 'android' ? { backgroundColor: tintFill } : null),
192
+ ...toPadding(padding),
193
+ };
194
+ const clipStyle = { borderRadius: radius };
195
+ const surface = (_jsxs(_Fragment, { children: [_jsxs(View, { pointerEvents: "none", style: [styles.clip, clipStyle], children: [_jsx(BlurView, { intensity: intensity, tint: "light", experimentalBlurMethod: Platform.OS === 'android' ? 'dimezisBlurView' : 'none', style: styles.fill }), _jsx(View, { style: [styles.fill, { backgroundColor: tintFill }] }), _jsx(LinearGradient, { colors: sheenColors, locations: [0, 0.3, 0.65], start: { x: 0.5, y: 0 }, end: { x: 0.5, y: 1 }, style: styles.fill })] }), _jsx(View, { style: styles.content, children: children })] }));
196
+ if (onPress) {
197
+ return (_jsx(Pressable, { onPress: onPress, style: [styles.container, containerStyle, style], children: surface }));
198
+ }
199
+ return _jsx(View, { style: [styles.container, containerStyle, style], children: surface });
200
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Platform-neutral design tokens (plan 022).
3
+ *
4
+ * The single source of truth for the library's plain-data design constants —
5
+ * tint/saturation defaults, the blur-per-unit mapping, the drop-shadow layers
6
+ * and scroll-aware depth factors, the specular/press-glow geometry, the
7
+ * material-variant parameters, and the accessibility (reduced-transparency /
8
+ * increased-contrast) fills. These were previously embedded as web-local
9
+ * `const`s inside `liquid-glass.tsx`; they live here so BOTH the web file
10
+ * (`liquid-glass.tsx`) and the future React Native file
11
+ * (`liquid-glass.native.tsx`, plan 025) can consume ONE source of truth.
12
+ *
13
+ * ## Hard constraints (what may live here)
14
+ *
15
+ * This module is a leaf with ZERO platform coupling:
16
+ *
17
+ * - It imports nothing from the React renderer packages.
18
+ * - It contains NO DOM style types and NO browser-global references.
19
+ * - It holds only the underlying NUMBERS / COLORS / structured data. Anything
20
+ * inherently CSS — `mixBlendMode`, `backdrop-filter` strings, the
21
+ * `box-shadow` serialization (`buildDropShadow`), the `radial-gradient`
22
+ * templates — STAYS in the web file. The native file will serialize these
23
+ * same values into RN style objects.
24
+ *
25
+ * The only import is the platform-neutral {@link DisplacementMode} string-union
26
+ * type (no runtime, no DOM), so {@link DEFAULTS.mode} keeps its exact type.
27
+ *
28
+ * @internal Not part of the public package API pre-1.0 (eng-review decision):
29
+ * not re-exported from `index.ts`. The native barrel (plan 025) may import from
30
+ * `./tokens` directly. Exposing publicly later is additive / non-breaking.
31
+ */
32
+ import type { DisplacementMode } from './types';
33
+ /** Default prop values for `<LiquidGlass>` (the parity spec). */
34
+ export declare const DEFAULTS: {
35
+ readonly displacementScale: 70;
36
+ readonly blurAmount: 0.0625;
37
+ readonly saturation: 140;
38
+ readonly aberrationIntensity: 2;
39
+ readonly cornerRadius: number | string;
40
+ readonly mode: DisplacementMode;
41
+ readonly padding: number | string;
42
+ readonly elasticity: 0.15;
43
+ };
44
+ /**
45
+ * Maps the small public `blurAmount` (default `0.0625`) onto a px backdrop blur.
46
+ * The upstream convention treats `blurAmount` as a rem-ish factor; we scale by
47
+ * 16 (1rem) so the default lands at ~1px and larger values scale linearly.
48
+ */
49
+ export declare const BLUR_PX_PER_UNIT = 16;
50
+ /**
51
+ * Scheme-aware translucent fill layered ON TOP of the blurred backdrop when
52
+ * `prefers-reduced-transparency: reduce` is active and the backdrop tiers (1/2)
53
+ * still carry a `backdrop-filter`. It frosts the surface toward opaque so the
54
+ * material reads as static frosted glass rather than live lensing. Higher alpha
55
+ * than the tier-3 solid fill because here it composites over a blurred backdrop,
56
+ * not bare content. (a11y plan 013.)
57
+ */
58
+ export declare const REDUCED_TRANSPARENCY_FILL: {
59
+ readonly light: "rgba(255, 255, 255, 0.75)";
60
+ readonly dark: "rgba(30, 30, 35, 0.75)";
61
+ };
62
+ /**
63
+ * Scheme-aware solid fill for the no-`backdrop-filter` tier (3) under reduced
64
+ * transparency: a higher alpha than the default `0.55` solid fallback so the
65
+ * surface is more opaque / less transparent as the setting requests.
66
+ */
67
+ export declare const REDUCED_TRANSPARENCY_SOLID_FILL: {
68
+ readonly light: "rgba(255, 255, 255, 0.92)";
69
+ readonly dark: "rgba(30, 30, 35, 0.92)";
70
+ };
71
+ /**
72
+ * Solid border color drawn as an INSET `box-shadow` ring on the surface so the
73
+ * box geometry is unchanged (no layout shift) — a real `border` would grow the
74
+ * box or eat into padding. Scheme-aware near-opaque ink/light for a clearly
75
+ * delineated edge that the soft bevel alone cannot provide.
76
+ */
77
+ export declare const CONTRAST_BORDER_COLOR: {
78
+ readonly light: "rgba(0, 0, 0, 0.92)";
79
+ readonly dark: "rgba(255, 255, 255, 0.92)";
80
+ };
81
+ /** Inset solid-border ring width (px), prepended to the surface box-shadow. */
82
+ export declare const CONTRAST_BORDER_WIDTH = 2;
83
+ /**
84
+ * More-opaque scheme-aware surface fill under increased contrast: it reduces
85
+ * the backdrop tint/show-through so foreground content reads at a higher
86
+ * contrast ratio. Layered over any retained blur (tiers 1/2) or used as the
87
+ * solid fill (tier 3). At 0.92 alpha it is at least as opaque as the
88
+ * reduced-transparency fills, so when BOTH a11y axes are active the contrast
89
+ * fill never undercuts reduced-transparency's opacity intent — contrast wants
90
+ * MORE opacity, not less, on every tier.
91
+ */
92
+ export declare const CONTRAST_FILL: {
93
+ readonly light: "rgba(255, 255, 255, 0.92)";
94
+ readonly dark: "rgba(22, 22, 26, 0.92)";
95
+ };
96
+ /**
97
+ * Under increased contrast, saturation is pinned to 100% (no decorative
98
+ * vibrancy boost) so the backdrop doesn't push color that competes with
99
+ * foreground text. The default `saturation` (140) is decorative and harms
100
+ * contrast, so it is overridden — never raised.
101
+ */
102
+ export declare const CONTRAST_SATURATION = 100;
103
+ /**
104
+ * Content-adaptive foreground ink (plan 018). When `adaptiveTint` is on, the
105
+ * content layer's `color` flips with the verdict so small content (button
106
+ * labels, captions) stays legible: DARK ink over a bright/light backdrop, LIGHT
107
+ * ink over a dark backdrop. `color` is inheritable, so children that don't set
108
+ * their own color pick it up; children with an explicit color keep it. Best-
109
+ * effort legibility — see the `adaptiveTint` TSDoc limitation note.
110
+ */
111
+ export declare const CONTENT_INK: {
112
+ readonly light: "rgba(255, 255, 255, 0.96)";
113
+ readonly dark: "rgba(17, 17, 20, 0.96)";
114
+ };
115
+ /**
116
+ * Resting specular-hotspot center as a percentage of the surface box. With no
117
+ * pointer over the glass (or under reduced-motion) the highlight falls back to a
118
+ * neutral, top-biased position so the material reads like overhead light on
119
+ * glass rather than a flat sweep. Top-center, slightly above the box.
120
+ */
121
+ export declare const SPECULAR_REST_X = "50%";
122
+ export declare const SPECULAR_REST_Y = "0%";
123
+ /**
124
+ * Peak opacity of the specular hotspot's bright core (the inner stop). Hover
125
+ * brightens it slightly (see `LiquidGlass`); these are the base values.
126
+ */
127
+ export declare const SPECULAR_CORE_OPACITY: {
128
+ readonly rest: 0.55;
129
+ readonly hover: 0.85;
130
+ };
131
+ /**
132
+ * Radius of the specular hotspot gradient as a percentage of the surface's
133
+ * larger dimension. Big enough to read as a soft glassy hotspot, small enough
134
+ * that it tracks visibly rather than washing the whole surface.
135
+ */
136
+ export declare const SPECULAR_RADIUS = "70%";
137
+ /**
138
+ * Peak opacity of the press-glow bloom's core when active. The glow originates
139
+ * at the contact point and spreads, then fades via the layer's opacity
140
+ * transition (gated on reduced-motion).
141
+ */
142
+ export declare const PRESS_GLOW_OPACITY = 0.5;
143
+ /** Radius of the press-glow bloom as a percentage of the surface box. */
144
+ export declare const PRESS_GLOW_RADIUS = "60%";
145
+ /**
146
+ * The static drop-shadow (the `static` factor of 1 reproduces today's exact
147
+ * values) is scaled by a single multiplier per backdrop scheme. The shadow
148
+ * DEEPENS/DARKENS over a dark backdrop (factor > 1 → larger offset/blur + darker
149
+ * alpha, lifting the glass above dense scrolling content) and EASES/LIGHTENS over
150
+ * a light/solid one (factor < 1). Factors are deliberately subtle so the polish
151
+ * reads as a gentle depth shift, not a jump.
152
+ *
153
+ * Keys map directly to `BackdropScheme`; `static` is the SSR / first-paint /
154
+ * unsampled fallback and equals the committed baseline shadow exactly.
155
+ */
156
+ export declare const SCROLL_SHADOW_DEPTH: {
157
+ /** Conservative fallback: byte-for-byte the existing static shadow. */
158
+ readonly static: 1;
159
+ /** Dark/dense backdrop ⇒ deeper, darker shadow. */
160
+ readonly dark: 1.35;
161
+ /** Light/solid backdrop ⇒ shallower, lighter shadow. */
162
+ readonly light: 0.7;
163
+ };
164
+ /** Base outer drop-shadow layers (offsetY, blur, alpha) at rest and on hover. */
165
+ export declare const SHADOW_LAYERS: {
166
+ readonly rest: readonly [{
167
+ readonly y: 8;
168
+ readonly blur: 24;
169
+ readonly alpha: 0.22;
170
+ }, {
171
+ readonly y: 1;
172
+ readonly blur: 4;
173
+ readonly alpha: 0.14;
174
+ }];
175
+ readonly hover: readonly [{
176
+ readonly y: 12;
177
+ readonly blur: 32;
178
+ readonly alpha: 0.28;
179
+ }, {
180
+ readonly y: 2;
181
+ readonly blur: 8;
182
+ readonly alpha: 0.18;
183
+ }];
184
+ };
185
+ /**
186
+ * Per-variant surface parameters (plan 019). A small parameter LOOKUP — NOT a
187
+ * theming system — that feeds the existing surface/content styles.
188
+ *
189
+ * - `'regular'` is a pure pass-through: `saturationFactor = 1`, no scrim, and
190
+ * `forceNonAdaptive = false`, so the default render is byte-for-byte unchanged
191
+ * and `adaptiveTint` keeps working exactly as in plan 018.
192
+ * - `'clear'` is permanently MORE transparent: the decorative backdrop saturation
193
+ * is pulled back (`saturationFactor < 1` reduces the tint/vibrancy show-through)
194
+ * and `forceNonAdaptive` hard-disables the adaptive sampling path. A subtle
195
+ * dimming scrim is rendered as a CONTENT SIBLING (see the layer stack) so labels
196
+ * stay legible over busy media without re-introducing adaptivity.
197
+ */
198
+ export declare const VARIANT_PARAMS: {
199
+ readonly regular: {
200
+ /** Multiplier on the requested `saturation` (1 = unchanged). */
201
+ readonly saturationFactor: 1;
202
+ /** Force the adaptive-tint path off regardless of `adaptiveTint`. */
203
+ readonly forceNonAdaptive: false;
204
+ /** Scheme-aware dimming-scrim color behind the content, or `null` for none. */
205
+ readonly scrim: {
206
+ light: string;
207
+ dark: string;
208
+ } | null;
209
+ };
210
+ readonly clear: {
211
+ readonly saturationFactor: 0.7;
212
+ readonly forceNonAdaptive: true;
213
+ readonly scrim: {
214
+ light: string;
215
+ dark: string;
216
+ } | null;
217
+ };
218
+ };
219
+ /**
220
+ * The frosted-glass DEFAULT fill: a faint white tint that gives the surface a
221
+ * subtle milky body even before any blur. ≈ `rgba(255, 255, 255, 0.10)`, the
222
+ * `oklch(1 0 0 / 0.1)` equivalent used by the Video.js liquid-glass reference.
223
+ *
224
+ * Defined HERE as the CONTRACT only — plan 022 does NOT apply it to any surface
225
+ * (that would change the committed pixel baseline). Plan 023 wires it onto the
226
+ * frosted DEFAULT look. Distinct in intent from the a11y
227
+ * {@link REDUCED_TRANSPARENCY_FILL} / {@link CONTRAST_FILL} fills, which exist
228
+ * to force opacity under specific OS settings.
229
+ */
230
+ export declare const FROSTED_DEFAULT_FILL = "rgba(255, 255, 255, 0.1)";
231
+ /**
232
+ * The frosted-glass DEFAULT hairline ring: a crisp ~1px inset edge that defines
233
+ * the frosted surface's perimeter. `color` is a translucent white drawn as an
234
+ * inset ring; `width` is in px.
235
+ *
236
+ * Defined HERE as the CONTRACT only — plan 022 does NOT apply it (no surface
237
+ * change). Plan 023 serializes it onto the frosted DEFAULT surface (e.g. as an
238
+ * inset `box-shadow` ring, geometry-neutral like the existing bevel).
239
+ */
240
+ export declare const FROSTED_HAIRLINE_RING: {
241
+ /** Inset ring color (translucent white). */
242
+ readonly color: "rgba(255, 255, 255, 0.5)";
243
+ /** Ring width in px (hairline). */
244
+ readonly width: 1;
245
+ };