@hzblj/kara-components 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +5 -5
- package/lib/module/atoms/activity-indicator/activity-indicator.android.js +42 -0
- package/lib/module/atoms/activity-indicator/activity-indicator.android.js.map +1 -0
- package/lib/module/atoms/activity-indicator/activity-indicator.ios.js +19 -0
- package/lib/module/atoms/activity-indicator/activity-indicator.ios.js.map +1 -0
- package/lib/module/atoms/activity-indicator/activity-indicator.types.js +4 -0
- package/lib/module/atoms/activity-indicator/activity-indicator.types.js.map +1 -0
- package/lib/module/atoms/activity-indicator/index.js +2 -1
- package/lib/module/atoms/activity-indicator/index.js.map +1 -1
- package/lib/module/atoms/avatar/avatar.js +2 -4
- package/lib/module/atoms/avatar/avatar.js.map +1 -1
- package/lib/module/atoms/blur-view/blur-view.android.js +100 -0
- package/lib/module/atoms/blur-view/blur-view.android.js.map +1 -0
- package/lib/module/atoms/blur-view/blur-view.ios.js +43 -0
- package/lib/module/atoms/blur-view/blur-view.ios.js.map +1 -0
- package/lib/module/atoms/blur-view/blur-view.types.js +4 -0
- package/lib/module/atoms/blur-view/blur-view.types.js.map +1 -0
- package/lib/module/atoms/blur-view/index.js +5 -0
- package/lib/module/atoms/blur-view/index.js.map +1 -0
- package/lib/module/atoms/glass-view/glass-view.android.js +36 -5
- package/lib/module/atoms/glass-view/glass-view.android.js.map +1 -1
- package/lib/module/atoms/image/image.js +13 -6
- package/lib/module/atoms/image/image.js.map +1 -1
- package/lib/module/atoms/image/image.types.js +4 -0
- package/lib/module/atoms/image/image.types.js.map +1 -0
- package/lib/module/atoms/image/index.js +1 -0
- package/lib/module/atoms/image/index.js.map +1 -1
- package/lib/module/atoms/index.js +1 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/atoms/island/island.android.js +6 -2
- package/lib/module/atoms/island/island.android.js.map +1 -1
- package/lib/module/atoms/island/island.ios.js +5 -2
- package/lib/module/atoms/island/island.ios.js.map +1 -1
- package/lib/module/atoms/pressable/pressable.js +38 -6
- package/lib/module/atoms/pressable/pressable.js.map +1 -1
- package/lib/module/atoms/progress/progress.ios.js +10 -4
- package/lib/module/atoms/progress/progress.ios.js.map +1 -1
- package/lib/module/atoms/rn-text/rn-text.js +5 -2
- package/lib/module/atoms/rn-text/rn-text.js.map +1 -1
- package/lib/module/atoms/skeleton/skeleton.js +5 -5
- package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
- package/lib/module/atoms/surface/surface.ios.js +3 -0
- package/lib/module/atoms/surface/surface.ios.js.map +1 -1
- package/lib/module/atoms/text/text.android.js +5 -2
- package/lib/module/atoms/text/text.android.js.map +1 -1
- package/lib/module/atoms/text/text.ios.js +14 -3
- package/lib/module/atoms/text/text.ios.js.map +1 -1
- package/lib/module/atoms/text-field/text-field.android.js +19 -4
- package/lib/module/atoms/text-field/text-field.android.js.map +1 -1
- package/lib/module/atoms/text-field/text-field.ios.js +7 -2
- package/lib/module/atoms/text-field/text-field.ios.js.map +1 -1
- package/lib/module/molecules/collapsible/collapsible.android.js +37 -7
- package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
- package/lib/module/molecules/navigation-bar/navigation-bar.android.js +2 -8
- package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
- package/lib/module/molecules/pager/pager.android.js +5 -4
- package/lib/module/molecules/pager/pager.android.js.map +1 -1
- package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +114 -11
- package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
- package/lib/module/molecules/stepper/stepper.ios.js +45 -8
- package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
- package/lib/module/ripple-clip.js +9 -1
- package/lib/module/ripple-clip.js.map +1 -1
- package/lib/typescript/atoms/activity-indicator/activity-indicator.d.ts +1 -3
- package/lib/typescript/atoms/activity-indicator/activity-indicator.ios.d.ts +3 -0
- package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +17 -0
- package/lib/typescript/atoms/activity-indicator/index.d.ts +1 -0
- package/lib/typescript/atoms/blur-view/blur-view.d.ts +1 -0
- package/lib/typescript/atoms/blur-view/blur-view.ios.d.ts +3 -0
- package/lib/typescript/atoms/blur-view/blur-view.types.d.ts +31 -0
- package/lib/typescript/atoms/blur-view/index.d.ts +2 -0
- package/lib/typescript/atoms/box/a-box.d.ts +1 -1
- package/lib/typescript/atoms/glass-view/glass-view.types.d.ts +12 -5
- package/lib/typescript/atoms/image/image.d.ts +12 -4
- package/lib/typescript/atoms/image/image.types.d.ts +4 -0
- package/lib/typescript/atoms/image/index.d.ts +1 -0
- package/lib/typescript/atoms/index.d.ts +1 -0
- package/lib/typescript/atoms/island/island.types.d.ts +8 -4
- package/lib/typescript/atoms/pressable/pressable.types.d.ts +5 -3
- package/lib/typescript/atoms/progress/progress.types.d.ts +2 -2
- package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +7 -2
- package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +8 -4
- package/lib/typescript/atoms/surface/surface.types.d.ts +2 -2
- package/lib/typescript/atoms/text/text.types.d.ts +7 -2
- package/lib/typescript/atoms/text-field/text-field.types.d.ts +6 -2
- package/lib/typescript/molecules/scroll-edge-container/scroll-edge-container.types.d.ts +6 -4
- package/lib/typescript/molecules/stepper/stepper.ios.d.ts +5 -0
- package/lib/typescript/molecules/stepper/stepper.types.d.ts +1 -4
- package/package.json +5 -5
- package/src/atoms/activity-indicator/activity-indicator.android.tsx +32 -0
- package/src/atoms/activity-indicator/activity-indicator.ios.tsx +15 -0
- package/src/atoms/activity-indicator/activity-indicator.types.ts +20 -0
- package/src/atoms/activity-indicator/index.ts +1 -0
- package/src/atoms/avatar/avatar.tsx +1 -1
- package/src/atoms/blur-view/blur-view.android.tsx +68 -0
- package/src/atoms/blur-view/blur-view.ios.tsx +37 -0
- package/src/atoms/blur-view/blur-view.types.ts +34 -0
- package/src/atoms/blur-view/index.ts +2 -0
- package/src/atoms/glass-view/glass-view.android.tsx +41 -3
- package/src/atoms/glass-view/glass-view.types.ts +12 -5
- package/src/atoms/image/image.tsx +11 -12
- package/src/atoms/image/image.types.ts +6 -0
- package/src/atoms/image/index.ts +1 -0
- package/src/atoms/index.ts +1 -0
- package/src/atoms/island/island.android.tsx +10 -2
- package/src/atoms/island/island.ios.tsx +4 -2
- package/src/atoms/island/island.types.ts +8 -4
- package/src/atoms/pressable/pressable.tsx +46 -6
- package/src/atoms/pressable/pressable.types.ts +5 -3
- package/src/atoms/progress/progress.ios.tsx +14 -5
- package/src/atoms/progress/progress.types.ts +2 -2
- package/src/atoms/rn-text/rn-text.tsx +6 -3
- package/src/atoms/rn-text/rn-text.types.ts +15 -9
- package/src/atoms/skeleton/skeleton.tsx +4 -4
- package/src/atoms/skeleton/skeleton.types.ts +8 -4
- package/src/atoms/surface/surface.ios.tsx +2 -1
- package/src/atoms/surface/surface.types.ts +2 -2
- package/src/atoms/text/text.android.tsx +7 -1
- package/src/atoms/text/text.ios.tsx +14 -3
- package/src/atoms/text/text.types.ts +7 -2
- package/src/atoms/text-field/text-field.android.tsx +19 -3
- package/src/atoms/text-field/text-field.ios.tsx +7 -1
- package/src/atoms/text-field/text-field.types.ts +6 -2
- package/src/molecules/collapsible/collapsible.android.tsx +47 -7
- package/src/molecules/navigation-bar/navigation-bar.android.tsx +1 -6
- package/src/molecules/pager/pager.android.tsx +4 -1
- package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +103 -10
- package/src/molecules/scroll-edge-container/scroll-edge-container.types.ts +6 -4
- package/src/molecules/stepper/stepper.ios.tsx +37 -1
- package/src/molecules/stepper/stepper.types.ts +1 -4
- package/src/ripple-clip.ts +10 -1
- package/lib/module/atoms/activity-indicator/activity-indicator.js +0 -14
- package/lib/module/atoms/activity-indicator/activity-indicator.js.map +0 -1
- package/src/atoms/activity-indicator/activity-indicator.tsx +0 -9
|
@@ -8,8 +8,12 @@ export type TextFieldProps = {
|
|
|
8
8
|
defaultValue?: string;
|
|
9
9
|
/** Stops editing and dims the field. On iOS it outranks `error`. */
|
|
10
10
|
disabled?: boolean;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Marks the field invalid: a destructive outline and label. A message is shown under the field as well — on Android
|
|
13
|
+
* as Material's supporting text, beside its error icon — so the error never rests on colour alone; Material's error
|
|
14
|
+
* colour is a pale red in dark mode. `true` marks it without a message.
|
|
15
|
+
*/
|
|
16
|
+
error?: boolean | string;
|
|
13
17
|
/** Shown above the field, in `labelMedium`. */
|
|
14
18
|
label?: string;
|
|
15
19
|
/** Wraps the text and grows the field to fit it. iOS ignores it when `secure`. */
|
|
@@ -3,8 +3,10 @@ import type { ResponsiveViewSx } from '@hzblj/kara-sx';
|
|
|
3
3
|
import type { ReactElement, ReactNode } from 'react';
|
|
4
4
|
export type { ScrollEdgeEffect, ScrollEdgeScrollableProps } from '@hzblj/kara-native/swift-ui';
|
|
5
5
|
/**
|
|
6
|
-
* Bars over one React Native scroll view, with
|
|
7
|
-
* blur where content scrolls past a bar
|
|
6
|
+
* Bars over one React Native scroll view, with each platform's scroll edge under them — on iOS 26 the system's
|
|
7
|
+
* progressive blur where content scrolls past a bar; on Android a Material top app bar's behaviour, the bars stacked
|
|
8
|
+
* around the scroll view and filling with `surface.secondary` once content scrolls under them. The React Native tier:
|
|
9
|
+
* it needs no host.
|
|
8
10
|
*
|
|
9
11
|
* A screen-level container: wrap the screen's scroll view with it. Mounted inside a cell of a virtualized list on iOS
|
|
10
12
|
* it draws only its bottom bar — the hosted scroll view and the top bar stay blank.
|
|
@@ -15,7 +17,7 @@ export type ScrollEdgeContainerProps = ResponsiveViewSx & {
|
|
|
15
17
|
* content begins below the top bar and scrolls under it. On by default.
|
|
16
18
|
*/
|
|
17
19
|
automaticInsets?: boolean;
|
|
18
|
-
/** Laid over the scroll view's bottom edge on iOS; stacked below it on Android,
|
|
20
|
+
/** Laid over the scroll view's bottom edge on iOS; stacked below it on Android, filled while content continues below. */
|
|
19
21
|
bottomBar?: ReactNode;
|
|
20
22
|
/** Exactly one React Native scroll view — `RnScrollView`, `RnFlatList`, `RnSectionList`. */
|
|
21
23
|
children: ReactElement<ScrollEdgeScrollableProps>;
|
|
@@ -23,7 +25,7 @@ export type ScrollEdgeContainerProps = ResponsiveViewSx & {
|
|
|
23
25
|
effect?: ScrollEdgeEffect;
|
|
24
26
|
/** Test identifier, forwarded to the container view. */
|
|
25
27
|
testID?: string;
|
|
26
|
-
/** Laid over the scroll view's top edge on iOS; stacked above it on Android,
|
|
28
|
+
/** Laid over the scroll view's top edge on iOS; stacked above it on Android, filled once content scrolls under it. */
|
|
27
29
|
topBar?: ReactNode;
|
|
28
30
|
};
|
|
29
31
|
export type ScrollEdgeContainerComponent = (props: ScrollEdgeContainerProps) => ReactNode;
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { StepperComponent } from './stepper.types';
|
|
2
|
+
/**
|
|
3
|
+
* SwiftUI's `Stepper` at the end of a row, the label leading and the value just before the control — the way iOS's own
|
|
4
|
+
* settings show a stepped value. The value rolls to the next one (`numericText`), up or down with the change, in
|
|
5
|
+
* monospaced digits so the row does not jump.
|
|
6
|
+
*/
|
|
2
7
|
export declare const Stepper: StepperComponent;
|
|
@@ -4,10 +4,7 @@ import type { ReactNode } from 'react';
|
|
|
4
4
|
* two icon buttons in a row. A native atom — it renders inside `<Screen>` or `<Island>`.
|
|
5
5
|
*/
|
|
6
6
|
export type StepperProps = {
|
|
7
|
-
/**
|
|
8
|
-
* The leading text. iOS shows only the label, not the value — put the value in it if it should be seen. Android
|
|
9
|
-
* shows the value between its buttons.
|
|
10
|
-
*/
|
|
7
|
+
/** The leading text. The value is shown too — before the control on iOS, between the buttons on Android. */
|
|
11
8
|
label: string;
|
|
12
9
|
/** The highest value. When unset, Android has no upper limit and iOS stops at `100`. */
|
|
13
10
|
max?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzblj/kara-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The atoms, molecules, organisms and templates of @hzblj/kara-ui — real SwiftUI and Jetpack Compose views through @expo/ui.",
|
|
5
5
|
"author": "Jan Blazej",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/hzblj/kara-ui/issues"
|
|
14
14
|
},
|
|
15
|
-
"homepage": "https://kara.janblazej.dev/docs/components",
|
|
15
|
+
"homepage": "https://www.kara.janblazej.dev/docs/components",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"react-native",
|
|
18
18
|
"expo",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
]
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@hzblj/kara-native": "^0.
|
|
63
|
-
"@hzblj/kara-sx": "^0.
|
|
64
|
-
"@hzblj/kara-theme": "^0.
|
|
62
|
+
"@hzblj/kara-native": "^0.9.0",
|
|
63
|
+
"@hzblj/kara-sx": "^0.9.0",
|
|
64
|
+
"@hzblj/kara-theme": "^0.9.0",
|
|
65
65
|
"sf-symbols-typescript": "^2.2.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {Host, LoadingIndicator} from '@expo/ui/jetpack-compose'
|
|
2
|
+
import {fillMaxSize, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
|
|
3
|
+
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
+
import {useMemo} from 'react'
|
|
5
|
+
import {StyleSheet} from 'react-native'
|
|
6
|
+
|
|
7
|
+
import type {ActivityIndicatorComponent, ActivityIndicatorProps} from './activity-indicator.types'
|
|
8
|
+
|
|
9
|
+
/** Material's 48dp for a screen; half of it in a row, about where React Native's small spinner sat. */
|
|
10
|
+
const styles = StyleSheet.create({
|
|
11
|
+
large: {height: 48, width: 48},
|
|
12
|
+
small: {height: 24, width: 24},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Material 3's expressive `LoadingIndicator` in a host of its own, sized here rather than measured, so it renders
|
|
17
|
+
* anywhere and takes its place in the first frame.
|
|
18
|
+
*/
|
|
19
|
+
export const ActivityIndicator: ActivityIndicatorComponent = ({
|
|
20
|
+
color = 'fill.accent.primary',
|
|
21
|
+
size = 'small',
|
|
22
|
+
testID,
|
|
23
|
+
}: ActivityIndicatorProps) => {
|
|
24
|
+
const theme = useAppTheme()
|
|
25
|
+
const modifiers = useMemo(() => (testID ? [fillMaxSize(), testIdModifier(testID)] : [fillMaxSize()]), [testID])
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Host colorScheme={theme.dark ? 'dark' : 'light'} seedColor={theme.materialSeed} style={styles[size]}>
|
|
29
|
+
<LoadingIndicator color={theme.color(color)} modifiers={modifiers} />
|
|
30
|
+
</Host>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {useAppTheme} from '@hzblj/kara-theme'
|
|
2
|
+
import {ActivityIndicator as RNActivityIndicator} from 'react-native'
|
|
3
|
+
|
|
4
|
+
import type {ActivityIndicatorComponent, ActivityIndicatorProps} from './activity-indicator.types'
|
|
5
|
+
|
|
6
|
+
/** React Native's spinner, which on iOS is UIKit's own `UIActivityIndicatorView`. */
|
|
7
|
+
export const ActivityIndicator: ActivityIndicatorComponent = ({
|
|
8
|
+
color = 'fill.accent.primary',
|
|
9
|
+
size = 'small',
|
|
10
|
+
testID,
|
|
11
|
+
}: ActivityIndicatorProps) => {
|
|
12
|
+
const theme = useAppTheme()
|
|
13
|
+
|
|
14
|
+
return <RNActivityIndicator color={theme.color(color)} size={size} testID={testID} />
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {ColorToken} from '@hzblj/kara-theme'
|
|
2
|
+
import type {ReactNode} from 'react'
|
|
3
|
+
|
|
4
|
+
export type ActivityIndicatorSize = 'large' | 'small'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* That something is loading, for as long as it takes — UIKit's spinner on iOS, Material 3's expressive
|
|
8
|
+
* `LoadingIndicator` on Android, its shapes morphing as it turns. A React Native view on iOS and a host of its own on
|
|
9
|
+
* Android, so it renders anywhere. For how far along something is, use `Progress`.
|
|
10
|
+
*/
|
|
11
|
+
export type ActivityIndicatorProps = {
|
|
12
|
+
/** Its colour. `fill.accent.primary` by default. */
|
|
13
|
+
color?: ColorToken
|
|
14
|
+
/** `small` sits in a row or a button, `large` holds a screen that is loading. `small` by default. */
|
|
15
|
+
size?: ActivityIndicatorSize
|
|
16
|
+
/** Test identifier, forwarded to the view. */
|
|
17
|
+
testID?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ActivityIndicatorComponent = (props: ActivityIndicatorProps) => ReactNode
|
|
@@ -32,7 +32,7 @@ export const Avatar: AvatarComponent = ({initials, size = 'md', source, testID}:
|
|
|
32
32
|
w={diameter}
|
|
33
33
|
>
|
|
34
34
|
{source ? (
|
|
35
|
-
<Image source={source}
|
|
35
|
+
<Image h={diameter} source={source} w={diameter} />
|
|
36
36
|
) : (
|
|
37
37
|
<RnText color="content.secondary" variant={VARIANT[size]}>
|
|
38
38
|
{initials ?? ''}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {BackdropBlurView} from '@hzblj/kara-native/jetpack-compose'
|
|
2
|
+
import {useSx} from '@hzblj/kara-sx'
|
|
3
|
+
import {type ColorToken, type OpacitySize, useAppTheme, withOpacity} from '@hzblj/kara-theme'
|
|
4
|
+
import {useMemo} from 'react'
|
|
5
|
+
import {StyleSheet} from 'react-native'
|
|
6
|
+
|
|
7
|
+
import type {BlurMaterial, BlurViewComponent, BlurViewProps} from './blur-view.types'
|
|
8
|
+
|
|
9
|
+
type MaterialSpec = {
|
|
10
|
+
/** dp. */
|
|
11
|
+
blur: number
|
|
12
|
+
/** A Material tonal surface — the thicker the material, the higher the container. */
|
|
13
|
+
fill: ColorToken
|
|
14
|
+
/**
|
|
15
|
+
* How much of the surface covers the blur, light and dark. A dark material covers more, as iOS's do: a dark surface
|
|
16
|
+
* over a bright backdrop otherwise leaves light content on light colour.
|
|
17
|
+
*/
|
|
18
|
+
opacity: {dark: OpacitySize; light: OpacitySize}
|
|
19
|
+
/** iOS's materials lift the colour of what they blur; a plain blur greys it into mud. */
|
|
20
|
+
saturation: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MATERIALS: Record<BlurMaterial, MaterialSpec> = {
|
|
24
|
+
regular: {blur: 28, fill: 'surface.secondary', opacity: {dark: 'o75', light: 'o60'}, saturation: 1.6},
|
|
25
|
+
thick: {blur: 30, fill: 'surface.secondary', opacity: {dark: 'o85', light: 'o80'}, saturation: 1.4},
|
|
26
|
+
thin: {blur: 24, fill: 'surface.primary', opacity: {dark: 'o60', light: 'o40'}, saturation: 1.7},
|
|
27
|
+
ultraThick: {blur: 32, fill: 'fill.quaternary', opacity: {dark: 'o90', light: 'o90'}, saturation: 1.2},
|
|
28
|
+
ultraThin: {blur: 20, fill: 'surface.base', opacity: {dark: 'o50', light: 'o25'}, saturation: 1.8},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The materials in Android's terms: kara's `BackdropBlurView` — what lies behind, blurred and saturated — under a
|
|
33
|
+
* Material tonal surface that gets higher and more opaque as the material thickens. Pinned to the appearance the app
|
|
34
|
+
* is not in, the surface is Material's inverse one. `vibrancy` has no Android counterpart and is left out.
|
|
35
|
+
*/
|
|
36
|
+
export const BlurView: BlurViewComponent = ({
|
|
37
|
+
appearance,
|
|
38
|
+
children,
|
|
39
|
+
material = 'regular',
|
|
40
|
+
testID,
|
|
41
|
+
vibrancy: _vibrancy,
|
|
42
|
+
...sx
|
|
43
|
+
}: BlurViewProps) => {
|
|
44
|
+
const theme = useAppTheme()
|
|
45
|
+
const resolved = useSx(sx)
|
|
46
|
+
const spec = MATERIALS[material]
|
|
47
|
+
const inverted = appearance !== undefined && (appearance === 'dark') !== theme.dark
|
|
48
|
+
const dark = appearance ? appearance === 'dark' : theme.dark
|
|
49
|
+
const base = String(theme.color(inverted ? 'surface.high.contrast' : spec.fill))
|
|
50
|
+
const {cornerRadius, style} = useMemo(() => {
|
|
51
|
+
const {borderRadius, ...rest} = StyleSheet.flatten(resolved) ?? {}
|
|
52
|
+
return {cornerRadius: typeof borderRadius === 'number' ? borderRadius : 0, style: rest}
|
|
53
|
+
}, [resolved])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<BackdropBlurView
|
|
57
|
+
blurRadius={spec.blur}
|
|
58
|
+
cornerRadius={cornerRadius}
|
|
59
|
+
fallbackTint={withOpacity(base, 'o95')}
|
|
60
|
+
glassTint={withOpacity(base, spec.opacity[dark ? 'dark' : 'light'])}
|
|
61
|
+
saturation={spec.saturation}
|
|
62
|
+
style={style}
|
|
63
|
+
testID={testID}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</BackdropBlurView>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {BlurView as NativeBlurView} from '@hzblj/kara-native/swift-ui'
|
|
2
|
+
import {useSx} from '@hzblj/kara-sx'
|
|
3
|
+
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
+
import {useMemo} from 'react'
|
|
5
|
+
import {StyleSheet} from 'react-native'
|
|
6
|
+
|
|
7
|
+
import type {BlurViewComponent, BlurViewProps} from './blur-view.types'
|
|
8
|
+
|
|
9
|
+
/** `borderRadius` is the material's shape, so it goes to the effect view — the one that clips — not the layout box. */
|
|
10
|
+
export const BlurView: BlurViewComponent = ({
|
|
11
|
+
appearance,
|
|
12
|
+
children,
|
|
13
|
+
material = 'regular',
|
|
14
|
+
testID,
|
|
15
|
+
vibrancy,
|
|
16
|
+
...sx
|
|
17
|
+
}: BlurViewProps) => {
|
|
18
|
+
const theme = useAppTheme()
|
|
19
|
+
const resolved = useSx(sx)
|
|
20
|
+
const {cornerRadius, style} = useMemo(() => {
|
|
21
|
+
const {borderRadius, ...rest} = StyleSheet.flatten(resolved) ?? {}
|
|
22
|
+
return {cornerRadius: typeof borderRadius === 'number' ? borderRadius : 0, style: rest}
|
|
23
|
+
}, [resolved])
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<NativeBlurView
|
|
27
|
+
colorScheme={appearance ?? (theme.dark ? 'dark' : 'light')}
|
|
28
|
+
cornerRadius={cornerRadius}
|
|
29
|
+
material={material}
|
|
30
|
+
style={style}
|
|
31
|
+
testID={testID}
|
|
32
|
+
vibrancy={vibrancy}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
</NativeBlurView>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {BlurMaterial, BlurVibrancy} from '@hzblj/kara-native/swift-ui'
|
|
2
|
+
import type {ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
3
|
+
import type {ReactNode} from 'react'
|
|
4
|
+
|
|
5
|
+
export type {BlurMaterial, BlurVibrancy} from '@hzblj/kara-native/swift-ui'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One of the system's materials behind React Native content — what sheets, bars, menus and HUDs are made of, in
|
|
9
|
+
* SwiftUI's five thicknesses. iOS: UIKit's `system*Material` blur, with optional vibrancy for the content. Android:
|
|
10
|
+
* what lies behind, blurred and saturated (Android 12+), under a Material tonal surface as thick as the material —
|
|
11
|
+
* `surface` at `ultraThin` up to `surfaceContainerHighest` at `ultraThick`; below Android 12 the surface alone. The
|
|
12
|
+
* React Native tier: it needs no host, so it suits a list cell. Shape it with `borderRadius`; it clips its children.
|
|
13
|
+
*/
|
|
14
|
+
export type BlurViewProps = ResponsiveViewSx & {
|
|
15
|
+
/**
|
|
16
|
+
* Pins the material to light or dark whatever the app's appearance — a dark HUD over a light screen. Unset, it follows
|
|
17
|
+
* the app theme. Android draws the opposite appearance with Material's inverse surface.
|
|
18
|
+
*/
|
|
19
|
+
appearance?: 'dark' | 'light'
|
|
20
|
+
/** React Native content, over the material. */
|
|
21
|
+
children?: ReactNode
|
|
22
|
+
/** How much of what lies behind shows through, from `ultraThin` (most) to `ultraThick` (least). `regular` by default. */
|
|
23
|
+
material?: BlurMaterial
|
|
24
|
+
/** Test identifier, forwarded to the view. */
|
|
25
|
+
testID?: string
|
|
26
|
+
/**
|
|
27
|
+
* iOS: draws the children vibrant at this level, the way iOS draws its own labels and fills on a material — their
|
|
28
|
+
* colour becomes the material's vibrant one and their alpha the mask. Android has no vibrancy: the children keep
|
|
29
|
+
* their colours there.
|
|
30
|
+
*/
|
|
31
|
+
vibrancy?: BlurVibrancy
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type BlurViewComponent = (props: BlurViewProps) => ReactNode
|
|
@@ -1,5 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {BackdropBlurView} from '@hzblj/kara-native/jetpack-compose'
|
|
2
|
+
import {useSx} from '@hzblj/kara-sx'
|
|
3
|
+
import {radii, useAppTheme, withOpacity} from '@hzblj/kara-theme'
|
|
4
|
+
|
|
2
5
|
import type {GlassViewComponent, GlassViewProps} from './glass-view.types'
|
|
3
6
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
7
|
+
/** dp. Strong enough that a photo behind reads as colour and light, not as detail. */
|
|
8
|
+
const BLUR_RADIUS = 16
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Frosted glass, Android's way: what lies behind the capsule, blurred (Android 12+), under a tonal Material surface
|
|
12
|
+
* and a light hairline — the blurred surfaces Android 16 gives its own system UI, not a copy of iOS's lens. Without a
|
|
13
|
+
* tint it is the surface colour, translucent, so the content keeps its usual colours on it; a tint fills it with that
|
|
14
|
+
* colour instead. Below Android 12, with nothing to blur, the same colours are drawn near-opaque. Android has no swell,
|
|
15
|
+
* so `interactive` changes nothing here — the `Pressable` around it gives the press its feedback.
|
|
16
|
+
*/
|
|
17
|
+
export const GlassView: GlassViewComponent = ({
|
|
18
|
+
children,
|
|
19
|
+
interactive: _interactive,
|
|
20
|
+
testID,
|
|
21
|
+
tint,
|
|
22
|
+
...sx
|
|
23
|
+
}: GlassViewProps) => {
|
|
24
|
+
const theme = useAppTheme()
|
|
25
|
+
const style = useSx(sx)
|
|
26
|
+
const base = String(theme.color(tint ?? 'surface.primary'))
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<BackdropBlurView
|
|
30
|
+
blurRadius={BLUR_RADIUS}
|
|
31
|
+
cornerRadius={radii.full}
|
|
32
|
+
fallbackTint={withOpacity(base, 'o95')}
|
|
33
|
+
glassTint={withOpacity(base, tint ? 'o85' : 'o50')}
|
|
34
|
+
hairlineColor={withOpacity(String(theme.color('content.on.bg')), theme.dark ? 'o20' : 'o60')}
|
|
35
|
+
hairlineWidth={1}
|
|
36
|
+
saturation={1.6}
|
|
37
|
+
style={style}
|
|
38
|
+
testID={testID}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</BackdropBlurView>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -3,18 +3,25 @@ import type {ColorToken} from '@hzblj/kara-theme'
|
|
|
3
3
|
import type {ReactNode} from 'react'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Glass around a React Native subtree, shaped as a capsule — the React Native tier, so it needs no host. iOS 26:
|
|
7
|
+
* Liquid Glass. Android: frosted glass — what lies behind, blurred on Android 12+, under a translucent tonal surface
|
|
8
|
+
* (or `tint`) and a hairline. iOS before 26 has no Liquid Glass and draws a capsule of `tint` instead, or of the glass
|
|
9
|
+
* buttons' translucent fill (`btn.bg.glass.base`) when there is none.
|
|
10
|
+
*
|
|
11
|
+
* On Android each glass re-records what lies behind it before every frame: a few buttons cost nothing, but keep it out
|
|
12
|
+
* of the cells of a long list — a plain `tint` reads as well there.
|
|
9
13
|
*/
|
|
10
14
|
export type GlassViewProps = ResponsiveViewSx & {
|
|
11
15
|
/** React Native views. Give them `pointerEvents="none"` when `interactive`, so the press reaches the `Pressable`. */
|
|
12
16
|
children?: ReactNode
|
|
13
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* iOS 26: swells toward the finger on touch, carrying the children with it. Android has no swell: wrap the view in a
|
|
19
|
+
* `Pressable` with `borderRadius="full"`, whose ripple then keeps to the capsule. The press is always the `Pressable`'s.
|
|
20
|
+
*/
|
|
14
21
|
interactive?: boolean
|
|
15
22
|
/** Test identifier, forwarded to the view. */
|
|
16
23
|
testID?: string
|
|
17
|
-
/** Tints the material. Unset leaves it clear. */
|
|
24
|
+
/** Tints the material. Unset leaves it clear on iOS, and the translucent surface colour on Android. */
|
|
18
25
|
tint?: ColorToken
|
|
19
26
|
}
|
|
20
27
|
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
+
import {sx} from '@hzblj/kara-sx'
|
|
1
2
|
import {duration} from '@hzblj/kara-theme'
|
|
2
|
-
import {Image as ExpoImage, ImageProps} from 'expo-image'
|
|
3
|
-
import {
|
|
3
|
+
import {Image as ExpoImage, type ImageProps as ExpoImageProps} from 'expo-image'
|
|
4
|
+
import type {Ref} from 'react'
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
<ExpoImage
|
|
7
|
-
transition={{
|
|
8
|
-
duration: duration.d100,
|
|
9
|
-
timing: 'linear',
|
|
10
|
-
}}
|
|
11
|
-
{...props}
|
|
12
|
-
/>
|
|
13
|
-
)
|
|
6
|
+
const FADE = {duration: duration.d100, timing: 'linear'} as const
|
|
14
7
|
|
|
15
|
-
|
|
8
|
+
const FadingImage = (props: ExpoImageProps & {ref?: Ref<ExpoImage>}) => <ExpoImage transition={FADE} {...props} />
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `expo-image` with the `sx` props, the way `Box` is a `View` with them, and the design system's 100 ms linear fade
|
|
12
|
+
* already applied, so a loaded image never pops in. An explicit `transition` wins, and so does an explicit `style`.
|
|
13
|
+
*/
|
|
14
|
+
export const Image = sx(FadingImage)
|
package/src/atoms/image/index.ts
CHANGED
package/src/atoms/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {Host} from '@expo/ui/jetpack-compose'
|
|
2
|
+
import {useSx} from '@hzblj/kara-sx'
|
|
2
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
3
4
|
|
|
4
5
|
import {HostBoundary} from '../../host-context'
|
|
@@ -8,15 +9,22 @@ const MATCH_HEIGHT_ONLY = {horizontal: false, vertical: true}
|
|
|
8
9
|
|
|
9
10
|
const MATCH_NEITHER = {horizontal: false, vertical: false}
|
|
10
11
|
|
|
11
|
-
export const Island: IslandComponent = ({
|
|
12
|
+
export const Island: IslandComponent = ({
|
|
13
|
+
children,
|
|
14
|
+
matchContents = true,
|
|
15
|
+
style,
|
|
16
|
+
testID: _testID,
|
|
17
|
+
...sx
|
|
18
|
+
}: IslandProps) => {
|
|
12
19
|
const theme = useAppTheme()
|
|
20
|
+
const resolved = useSx(sx)
|
|
13
21
|
|
|
14
22
|
return (
|
|
15
23
|
<Host
|
|
16
24
|
colorScheme={theme.dark ? 'dark' : 'light'}
|
|
17
25
|
matchContents={matchContents ? MATCH_HEIGHT_ONLY : MATCH_NEITHER}
|
|
18
26
|
seedColor={theme.materialSeed}
|
|
19
|
-
style={style}
|
|
27
|
+
style={style ? [resolved, style] : resolved}
|
|
20
28
|
>
|
|
21
29
|
<HostBoundary>{children}</HostBoundary>
|
|
22
30
|
</Host>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {Host} from '@expo/ui/swift-ui'
|
|
2
2
|
import {dynamicTypeSize} from '@expo/ui/swift-ui/modifiers'
|
|
3
|
+
import {useSx} from '@hzblj/kara-sx'
|
|
3
4
|
import {MAX_DYNAMIC_TYPE_SIZE, useAppTheme} from '@hzblj/kara-theme'
|
|
4
5
|
|
|
5
6
|
import {HostBoundary} from '../../host-context'
|
|
@@ -11,8 +12,9 @@ const MATCH_HEIGHT_ONLY = {horizontal: false, vertical: true}
|
|
|
11
12
|
|
|
12
13
|
const MATCH_NEITHER = {horizontal: false, vertical: false}
|
|
13
14
|
|
|
14
|
-
export const Island: IslandComponent = ({children, matchContents = true, style, testID}: IslandProps) => {
|
|
15
|
+
export const Island: IslandComponent = ({children, matchContents = true, style, testID, ...sx}: IslandProps) => {
|
|
15
16
|
const theme = useAppTheme()
|
|
17
|
+
const resolved = useSx(sx)
|
|
16
18
|
|
|
17
19
|
return (
|
|
18
20
|
<Host
|
|
@@ -20,7 +22,7 @@ export const Island: IslandComponent = ({children, matchContents = true, style,
|
|
|
20
22
|
matchContents={matchContents ? MATCH_HEIGHT_ONLY : MATCH_NEITHER}
|
|
21
23
|
modifiers={DYNAMIC_TYPE_CAP}
|
|
22
24
|
seedColor={theme.materialSeed}
|
|
23
|
-
style={style}
|
|
25
|
+
style={style ? [resolved, style] : resolved}
|
|
24
26
|
testID={testID}
|
|
25
27
|
>
|
|
26
28
|
<HostBoundary>{children}</HostBoundary>
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type {ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
1
2
|
import type {ReactNode} from 'react'
|
|
2
3
|
import type {StyleProp, ViewStyle} from 'react-native'
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A host for native atoms in a React Native layout — the SwiftUI / Compose `Host` an `@expo/ui` tree mounts into. It
|
|
7
|
+
* sits in that layout like a `Box`, and takes `Box`'s `sx` props for it: `h`, `flex`, margins.
|
|
8
|
+
*/
|
|
9
|
+
export type IslandProps = ResponsiveViewSx & {
|
|
6
10
|
/** The native views it hosts. */
|
|
7
11
|
children?: ReactNode
|
|
8
12
|
/**
|
|
@@ -11,13 +15,13 @@ export type IslandProps = {
|
|
|
11
15
|
* width maps to `fixedSize(horizontal: true)`, so the native side ignores the proposed width and
|
|
12
16
|
* resolves a `wide` stack's ideal width as unbounded.
|
|
13
17
|
*
|
|
14
|
-
* Turn it off — and give the island an explicit
|
|
18
|
+
* Turn it off — and give the island an explicit height, `h` — when the subtree contains an
|
|
15
19
|
* `RnView`: that host drives its SwiftUI frame from the child's Yoga-laid-out bounds while this one
|
|
16
20
|
* writes its measured size back into the Yoga style, and the two together spin a layout loop that
|
|
17
21
|
* hangs the main thread as soon as the views lay out on screen.
|
|
18
22
|
*/
|
|
19
23
|
matchContents?: boolean
|
|
20
|
-
/** A React Native style for the host view
|
|
24
|
+
/** A React Native style for the host view, merged over the `sx` props — for what they cannot say. */
|
|
21
25
|
style?: StyleProp<ViewStyle>
|
|
22
26
|
/** Test identifier, forwarded to the host view on iOS — Android ignores it. */
|
|
23
27
|
testID?: string
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {useSplitSx} from '@hzblj/kara-sx'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {useAppTheme, withOpacity} from '@hzblj/kara-theme'
|
|
3
|
+
import {useCallback, useMemo} from 'react'
|
|
4
|
+
import {Platform, Pressable as RNPressable, View, type ViewStyle} from 'react-native'
|
|
4
5
|
import Animated, {useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated'
|
|
5
6
|
|
|
6
7
|
import {haptics} from '../../haptics'
|
|
8
|
+
import {splitForRipple} from '../../ripple-clip'
|
|
7
9
|
import type {PressableComponent, PressableProps} from './pressable.types'
|
|
8
10
|
|
|
9
11
|
const SPRING = {damping: 20, stiffness: 400}
|
|
@@ -22,20 +24,39 @@ export const Pressable: PressableComponent = ({
|
|
|
22
24
|
const scale = useSharedValue(1)
|
|
23
25
|
const feedback = haptic === true ? 'light' : haptic || undefined
|
|
24
26
|
|
|
27
|
+
const theme = useAppTheme()
|
|
28
|
+
const isAndroid = Platform.OS === 'android'
|
|
29
|
+
|
|
25
30
|
const rStyle = useAnimatedStyle(() => ({transform: [{scale: scale.value}]}), [])
|
|
26
31
|
|
|
32
|
+
// Android's press is Material's: a ripple over the content — the pressed state layer, `onSurface` at 10 % — clipped to
|
|
33
|
+
// the shape by an outer view (`splitForRipple`), and nothing moves. iOS springs down.
|
|
34
|
+
const ripple = useMemo(
|
|
35
|
+
() =>
|
|
36
|
+
isAndroid ? {color: withOpacity(String(theme.color('content.primary')), 'o10'), foreground: true} : undefined,
|
|
37
|
+
[isAndroid, theme]
|
|
38
|
+
)
|
|
39
|
+
const androidStyle = useMemo(
|
|
40
|
+
() => (isAndroid ? splitForRipple(resolved as ViewStyle) : undefined),
|
|
41
|
+
[isAndroid, resolved]
|
|
42
|
+
)
|
|
43
|
+
|
|
27
44
|
// The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
|
|
28
45
|
const onPressIn = useCallback(() => {
|
|
29
|
-
|
|
46
|
+
if (!isAndroid) {
|
|
47
|
+
scale.value = withSpring(0.96, SPRING)
|
|
48
|
+
}
|
|
30
49
|
|
|
31
50
|
if (feedback) {
|
|
32
51
|
haptics.prepare(feedback)
|
|
33
52
|
}
|
|
34
|
-
}, [feedback, scale])
|
|
53
|
+
}, [feedback, isAndroid, scale])
|
|
35
54
|
|
|
36
55
|
const onPressOut = useCallback(() => {
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
if (!isAndroid) {
|
|
57
|
+
scale.value = withSpring(1, SPRING)
|
|
58
|
+
}
|
|
59
|
+
}, [isAndroid, scale])
|
|
39
60
|
|
|
40
61
|
const onPressWithHaptic = useCallback(() => {
|
|
41
62
|
if (feedback) {
|
|
@@ -45,6 +66,25 @@ export const Pressable: PressableComponent = ({
|
|
|
45
66
|
onPress?.()
|
|
46
67
|
}, [feedback, onPress])
|
|
47
68
|
|
|
69
|
+
if (androidStyle) {
|
|
70
|
+
return (
|
|
71
|
+
<View style={androidStyle.outer}>
|
|
72
|
+
<RNPressable
|
|
73
|
+
android_ripple={ripple}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
hitSlop={8}
|
|
76
|
+
onPress={onPressWithHaptic}
|
|
77
|
+
onPressIn={onPressIn}
|
|
78
|
+
style={androidStyle.inner}
|
|
79
|
+
testID={testID}
|
|
80
|
+
{...rest}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</RNPressable>
|
|
84
|
+
</View>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
48
88
|
return (
|
|
49
89
|
<AnimatedPressable
|
|
50
90
|
disabled={disabled}
|