@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
|
@@ -4,13 +4,15 @@ import type {ReactNode} from 'react'
|
|
|
4
4
|
import type {Haptic} from '../../haptics'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* React Native's `Pressable`, styled through `sx` like a `Box
|
|
8
|
-
*
|
|
7
|
+
* React Native's `Pressable`, styled through `sx` like a `Box`, taking touches 8 points past its edges. The press is
|
|
8
|
+
* each platform's own: iOS springs it down to 96%; Android draws Material's ripple over the content, clipped to the
|
|
9
|
+
* pressable's shape — give it the `borderRadius` of what it wraps (`full` around a `GlassView`). The React Native
|
|
10
|
+
* tier — it needs no host.
|
|
9
11
|
*/
|
|
10
12
|
export type PressableProps = ResponsiveViewSx & {
|
|
11
13
|
/** The pressable content. */
|
|
12
14
|
children?: ReactNode
|
|
13
|
-
/** Ignores presses — no `onPress`, no haptic, no spring. */
|
|
15
|
+
/** Ignores presses — no `onPress`, no haptic, no spring or ripple. */
|
|
14
16
|
disabled?: boolean
|
|
15
17
|
/** Played on press. `true` is a `light` impact; a name picks the haptic, e.g. `selection` for a choice. */
|
|
16
18
|
haptic?: boolean | Haptic
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {ProgressView} from '@expo/ui/swift-ui'
|
|
2
|
-
import {progressViewStyle, tint} from '@expo/ui/swift-ui/modifiers'
|
|
1
|
+
import {Gauge, ProgressView} from '@expo/ui/swift-ui'
|
|
2
|
+
import {gaugeStyle, progressViewStyle, tint} from '@expo/ui/swift-ui/modifiers'
|
|
3
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
4
|
import {useMemo} from 'react'
|
|
5
5
|
|
|
@@ -10,10 +10,19 @@ export const Progress: ProgressComponent = ({circular = false, value}: ProgressP
|
|
|
10
10
|
useHostGuard('Progress')
|
|
11
11
|
const theme = useAppTheme()
|
|
12
12
|
|
|
13
|
+
const ring = circular && value !== undefined
|
|
13
14
|
const modifiers = useMemo(
|
|
14
|
-
() => [
|
|
15
|
-
|
|
15
|
+
() => [
|
|
16
|
+
ring ? gaugeStyle('circularCapacity') : progressViewStyle(circular ? 'circular' : 'linear'),
|
|
17
|
+
tint(theme.color('fill.accent.primary')),
|
|
18
|
+
],
|
|
19
|
+
[circular, ring, theme]
|
|
16
20
|
)
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
// SwiftUI's circular `ProgressView` ignores its value on iOS and spins; a ring that fills is a capacity `Gauge`.
|
|
23
|
+
return ring ? (
|
|
24
|
+
<Gauge modifiers={modifiers} value={value} />
|
|
25
|
+
) : (
|
|
26
|
+
<ProgressView modifiers={modifiers} value={value ?? null} />
|
|
27
|
+
)
|
|
19
28
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type {ReactNode} from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A progress bar or ring, tinted `fill.accent.primary` — SwiftUI `ProgressView`
|
|
5
|
-
* `LinearProgressIndicator` and `CircularProgressIndicator`.
|
|
4
|
+
* A progress bar or ring, tinted `fill.accent.primary` — SwiftUI `ProgressView` (a capacity `Gauge` for a ring with a
|
|
5
|
+
* value, which `ProgressView` cannot draw on iOS) / Compose's `LinearProgressIndicator` and `CircularProgressIndicator`.
|
|
6
6
|
*/
|
|
7
7
|
export type ProgressProps = {
|
|
8
8
|
/** A circular indicator instead of a bar. Off by default. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import {useSplitSx} from '@hzblj/kara-sx'
|
|
1
2
|
import {MAX_FONT_SCALE, useAppTheme} from '@hzblj/kara-theme'
|
|
2
|
-
import {Text as RNText, useWindowDimensions} from 'react-native'
|
|
3
|
+
import {Text as RNText, type TextStyle, useWindowDimensions} from 'react-native'
|
|
3
4
|
|
|
4
5
|
import type {RnTextComponent, RnTextProps} from './rn-text.types'
|
|
5
6
|
|
|
@@ -10,6 +11,8 @@ export const RnText: RnTextComponent = ({
|
|
|
10
11
|
...props
|
|
11
12
|
}: RnTextProps) => {
|
|
12
13
|
const theme = useAppTheme()
|
|
14
|
+
// A view style, which a text accepts whole — only React Native's types disagree, on `userSelect`'s width.
|
|
15
|
+
const [sx, rest] = useSplitSx(props) as [TextStyle, Record<string, unknown>]
|
|
13
16
|
const {fontScale} = useWindowDimensions()
|
|
14
17
|
const style = theme.typography[variant]
|
|
15
18
|
|
|
@@ -26,8 +29,8 @@ export const RnText: RnTextComponent = ({
|
|
|
26
29
|
return (
|
|
27
30
|
<RNText
|
|
28
31
|
maxFontSizeMultiplier={MAX_FONT_SCALE}
|
|
29
|
-
style={[style, {color: theme.color(color), lineHeight, textAlign: align}]}
|
|
30
|
-
{...
|
|
32
|
+
style={[style, sx, {color: theme.color(color), lineHeight, textAlign: align}]}
|
|
33
|
+
{...rest}
|
|
31
34
|
/>
|
|
32
35
|
)
|
|
33
36
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import type {ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
1
2
|
import type {ColorToken, TextVariant} from '@hzblj/kara-theme'
|
|
2
3
|
import type {ReactNode} from 'react'
|
|
3
4
|
import type {TextProps as RNTextProps} from 'react-native'
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
/**
|
|
7
|
+
* React Native's `Text` in a theme typography variant — the React Native tier, for text outside a native host. It takes
|
|
8
|
+
* `Box`'s `sx` surface for its own box — margins, flex, padding, a background — all but `align`, which here aligns the
|
|
9
|
+
* lines.
|
|
10
|
+
*/
|
|
11
|
+
export type RnTextProps = Omit<RNTextProps, 'style'> &
|
|
12
|
+
Omit<ResponsiveViewSx, 'align'> & {
|
|
13
|
+
/** Horizontal alignment, as React Native's `textAlign`. */
|
|
14
|
+
align?: 'center' | 'left' | 'right'
|
|
15
|
+
/** The text's colour. `content.primary` by default. */
|
|
16
|
+
color?: ColorToken
|
|
17
|
+
/** The typography variant. `body` by default. */
|
|
18
|
+
variant?: TextVariant
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
export type RnTextComponent = (props: RnTextProps) => ReactNode
|
|
@@ -5,7 +5,7 @@ import {useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-na
|
|
|
5
5
|
import {ABox} from '../box'
|
|
6
6
|
import type {SkeletonComponent, SkeletonProps} from './skeleton.types'
|
|
7
7
|
|
|
8
|
-
export const Skeleton: SkeletonComponent = ({borderRadius = 'small',
|
|
8
|
+
export const Skeleton: SkeletonComponent = ({borderRadius = 'small', h, testID, w}: SkeletonProps) => {
|
|
9
9
|
const pulse = useSharedValue<number>(opacity.o40)
|
|
10
10
|
|
|
11
11
|
useEffect(() => {
|
|
@@ -18,11 +18,11 @@ export const Skeleton: SkeletonComponent = ({borderRadius = 'small', height, tes
|
|
|
18
18
|
<ABox
|
|
19
19
|
bg="fill.quaternary"
|
|
20
20
|
borderRadius={borderRadius}
|
|
21
|
-
h={
|
|
21
|
+
h={h}
|
|
22
22
|
style={rStyle}
|
|
23
23
|
testID={testID}
|
|
24
|
-
w={
|
|
25
|
-
wide={
|
|
24
|
+
w={w}
|
|
25
|
+
wide={w === undefined}
|
|
26
26
|
/>
|
|
27
27
|
)
|
|
28
28
|
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
import type {SxDimension} from '@hzblj/kara-sx'
|
|
1
2
|
import type {RadiiSize} from '@hzblj/kara-theme'
|
|
2
3
|
import type {ReactNode} from 'react'
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* A loading placeholder that pulses in `fill.quaternary` — a React Native view, so it needs no host. Sized with the
|
|
7
|
+
* `sx` names: `w`, `h`, `borderRadius`.
|
|
8
|
+
*/
|
|
5
9
|
export type SkeletonProps = {
|
|
6
10
|
/** The corner radius. `small` by default. */
|
|
7
11
|
borderRadius?: RadiiSize
|
|
8
12
|
/** In points. */
|
|
9
|
-
|
|
13
|
+
h: number
|
|
10
14
|
/** Test identifier, forwarded to the view. */
|
|
11
15
|
testID?: string
|
|
12
|
-
/** In points
|
|
13
|
-
|
|
16
|
+
/** In points, or a percentage of the row (`60%` for a line that ends early). Unset, it fills the row. */
|
|
17
|
+
w?: SxDimension
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export type SkeletonComponent = (props: SkeletonProps) => ReactNode
|
|
@@ -6,7 +6,8 @@ import type {SurfaceComponent, SurfaceProps} from './surface.types'
|
|
|
6
6
|
export const Surface: SurfaceComponent = ({children, glass = false, testID, ...sx}: SurfaceProps) => {
|
|
7
7
|
useHostGuard('Surface')
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// `surface.primary` unless told otherwise, as Material's `Surface` is on Android — without it a plain one drew nothing.
|
|
10
|
+
const modifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', ...sx})
|
|
10
11
|
const content = (
|
|
11
12
|
<VStack alignment="leading" modifiers={modifiers} testID={testID}>
|
|
12
13
|
{children}
|
|
@@ -2,8 +2,8 @@ import type {ResponsiveSx} from '@hzblj/kara-sx'
|
|
|
2
2
|
import type {ReactNode} from 'react'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* A container padded `s16` and rounded `large` unless told otherwise — a SwiftUI `VStack` on
|
|
6
|
-
* `Surface` around a `Column` on Android.
|
|
5
|
+
* A container on `surface.primary`, padded `s16` and rounded `large` unless told otherwise — a SwiftUI `VStack` on
|
|
6
|
+
* iOS, Material's `Surface` around a `Column` on Android.
|
|
7
7
|
*/
|
|
8
8
|
export type SurfaceProps = ResponsiveSx & {
|
|
9
9
|
/** Stacked vertically, from the leading edge. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {Text as ComposeText} from '@expo/ui/jetpack-compose'
|
|
2
2
|
import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
|
|
3
|
+
import {useSxModifiers} from '@hzblj/kara-sx'
|
|
3
4
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
5
|
import {useMemo} from 'react'
|
|
5
6
|
|
|
@@ -32,11 +33,16 @@ export const Text: TextComponent = ({
|
|
|
32
33
|
testID,
|
|
33
34
|
transform,
|
|
34
35
|
variant = 'body',
|
|
36
|
+
...sx
|
|
35
37
|
}: TextProps) => {
|
|
36
38
|
useHostGuard('Text')
|
|
37
39
|
const theme = useAppTheme()
|
|
38
40
|
const style = theme.typography[variant]
|
|
39
|
-
const
|
|
41
|
+
const sxModifiers = useSxModifiers(sx)
|
|
42
|
+
const modifiers = useMemo(
|
|
43
|
+
() => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
|
|
44
|
+
[sxModifiers, testID]
|
|
45
|
+
)
|
|
40
46
|
|
|
41
47
|
return (
|
|
42
48
|
<ComposeText
|
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
underline,
|
|
11
11
|
} from '@expo/ui/swift-ui/modifiers'
|
|
12
12
|
import {scaledFont} from '@hzblj/kara-native/swift-ui'
|
|
13
|
+
import {type FrameAlignment, useSxModifiers} from '@hzblj/kara-sx'
|
|
13
14
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
14
15
|
import {useMemo} from 'react'
|
|
15
16
|
|
|
16
17
|
import {useHostGuard} from '../../host-context'
|
|
17
|
-
import type {TextComponent, TextProps} from './text.types'
|
|
18
|
+
import type {TextAlign, TextComponent, TextProps} from './text.types'
|
|
18
19
|
|
|
19
20
|
const ALIGNMENT = {
|
|
20
21
|
center: 'center',
|
|
@@ -22,6 +23,13 @@ const ALIGNMENT = {
|
|
|
22
23
|
right: 'trailing',
|
|
23
24
|
} as const
|
|
24
25
|
|
|
26
|
+
/** Where a frame larger than the text — `wide`, `w`, `h` — puts it: on the side its lines align to, centred vertically. */
|
|
27
|
+
const FRAME_ALIGNMENT = {
|
|
28
|
+
center: 'center',
|
|
29
|
+
left: 'leading',
|
|
30
|
+
right: 'trailing',
|
|
31
|
+
} as const satisfies Record<TextAlign, FrameAlignment>
|
|
32
|
+
|
|
25
33
|
export const Text: TextComponent = ({
|
|
26
34
|
align,
|
|
27
35
|
children,
|
|
@@ -31,10 +39,13 @@ export const Text: TextComponent = ({
|
|
|
31
39
|
testID,
|
|
32
40
|
transform,
|
|
33
41
|
variant = 'body',
|
|
42
|
+
...sx
|
|
34
43
|
}: TextProps) => {
|
|
35
44
|
useHostGuard('Text')
|
|
36
45
|
const theme = useAppTheme()
|
|
37
46
|
const style = theme.typography[variant]
|
|
47
|
+
// After the text's own modifiers: the font and colour belong to the glyphs, the padding and frame to the box around.
|
|
48
|
+
const sxModifiers = useSxModifiers(sx, FRAME_ALIGNMENT[align ?? 'left'])
|
|
38
49
|
|
|
39
50
|
const modifiers = useMemo(() => {
|
|
40
51
|
const list: ModifierConfig[] = [
|
|
@@ -65,8 +76,8 @@ export const Text: TextComponent = ({
|
|
|
65
76
|
list.push(lineLimit(numberOfLines))
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
return list
|
|
69
|
-
}, [align, color, decoration, numberOfLines, style, theme, transform])
|
|
79
|
+
return [...list, ...sxModifiers]
|
|
80
|
+
}, [align, color, decoration, numberOfLines, style, sxModifiers, theme, transform])
|
|
70
81
|
|
|
71
82
|
return (
|
|
72
83
|
<SwiftUIText modifiers={modifiers} testID={testID}>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {ResponsiveSx} from '@hzblj/kara-sx'
|
|
1
2
|
import type {ColorToken, TextVariant} from '@hzblj/kara-theme'
|
|
2
3
|
import type {ReactNode} from 'react'
|
|
3
4
|
|
|
@@ -7,8 +8,12 @@ export type TextDecoration = 'none' | 'strikethrough' | 'underline'
|
|
|
7
8
|
|
|
8
9
|
export type TextTransform = 'lowercase' | 'uppercase'
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* SwiftUI `Text` / Compose `Text` in a theme typography variant — a native atom; outside a host, use `RnText`. It takes
|
|
13
|
+
* the native `sx` surface too: padding, a frame (`wide` lines it up by `align`), a background, and the `ios` bucket's
|
|
14
|
+
* `contentTransition`, `animation` and `monospacedDigit` for a number that changes in place.
|
|
15
|
+
*/
|
|
16
|
+
export type TextProps = ResponsiveSx & {
|
|
12
17
|
/**
|
|
13
18
|
* How its lines align. iOS reads `left` and `right` as leading and trailing, so they flip in a right-to-left
|
|
14
19
|
* layout; Android keeps them as they are.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {Column, Text as ComposeText, OutlinedTextField, useNativeState} from '@expo/ui/jetpack-compose'
|
|
2
2
|
import {useSxModifiers} from '@hzblj/kara-sx'
|
|
3
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
+
import {Glyph} from '../../glyph'
|
|
4
5
|
import {useHostGuard} from '../../host-context'
|
|
5
6
|
import type {TextFieldComponent, TextFieldProps} from './text-field.types'
|
|
6
7
|
|
|
@@ -17,12 +18,14 @@ export const TextField: TextFieldComponent = ({
|
|
|
17
18
|
const theme = useAppTheme()
|
|
18
19
|
const value = useNativeState(defaultValue)
|
|
19
20
|
const modifiers = useSxModifiers({wide: true})
|
|
21
|
+
const invalid = Boolean(error)
|
|
22
|
+
const message = typeof error === 'string' && error ? error : undefined
|
|
20
23
|
|
|
21
24
|
return (
|
|
22
25
|
<Column verticalArrangement={{spacedBy: 4}}>
|
|
23
26
|
{label ? (
|
|
24
27
|
<ComposeText
|
|
25
|
-
color={theme.colorString(
|
|
28
|
+
color={theme.colorString(invalid ? 'content.destructive' : 'content.secondary')}
|
|
26
29
|
style={theme.typography.labelMedium}
|
|
27
30
|
>
|
|
28
31
|
{label}
|
|
@@ -30,7 +33,7 @@ export const TextField: TextFieldComponent = ({
|
|
|
30
33
|
) : null}
|
|
31
34
|
<OutlinedTextField
|
|
32
35
|
enabled={!disabled}
|
|
33
|
-
isError={
|
|
36
|
+
isError={invalid}
|
|
34
37
|
modifiers={modifiers}
|
|
35
38
|
onValueChange={onChangeText}
|
|
36
39
|
singleLine={!multiline}
|
|
@@ -41,7 +44,20 @@ export const TextField: TextFieldComponent = ({
|
|
|
41
44
|
}}
|
|
42
45
|
value={value}
|
|
43
46
|
visualTransformation={secure ? 'password' : 'none'}
|
|
44
|
-
|
|
47
|
+
>
|
|
48
|
+
{invalid ? (
|
|
49
|
+
<OutlinedTextField.TrailingIcon>
|
|
50
|
+
<Glyph color="content.destructive" name="error" />
|
|
51
|
+
</OutlinedTextField.TrailingIcon>
|
|
52
|
+
) : null}
|
|
53
|
+
{message ? (
|
|
54
|
+
<OutlinedTextField.SupportingText>
|
|
55
|
+
<ComposeText color={theme.colorString('content.destructive')} style={theme.typography.footnote}>
|
|
56
|
+
{message}
|
|
57
|
+
</ComposeText>
|
|
58
|
+
</OutlinedTextField.SupportingText>
|
|
59
|
+
) : null}
|
|
60
|
+
</OutlinedTextField>
|
|
45
61
|
</Column>
|
|
46
62
|
)
|
|
47
63
|
}
|
|
@@ -45,7 +45,8 @@ export const TextField: TextFieldComponent = ({
|
|
|
45
45
|
}: TextFieldProps) => {
|
|
46
46
|
useHostGuard('TextField')
|
|
47
47
|
const text = useNativeState(defaultValue)
|
|
48
|
-
const tone = TONE[toneOf(disabled, error)]
|
|
48
|
+
const tone = TONE[toneOf(disabled, Boolean(error))]
|
|
49
|
+
const message = tone === TONE.error && typeof error === 'string' && error ? error : undefined
|
|
49
50
|
|
|
50
51
|
const sxModifiers = useSxModifiers({
|
|
51
52
|
bg: tone.bg,
|
|
@@ -85,6 +86,11 @@ export const TextField: TextFieldComponent = ({
|
|
|
85
86
|
text={text}
|
|
86
87
|
/>
|
|
87
88
|
)}
|
|
89
|
+
{message ? (
|
|
90
|
+
<Text color="content.destructive" variant="footnote">
|
|
91
|
+
{message}
|
|
92
|
+
</Text>
|
|
93
|
+
) : null}
|
|
88
94
|
</VStack>
|
|
89
95
|
)
|
|
90
96
|
}
|
|
@@ -9,8 +9,12 @@ export type TextFieldProps = {
|
|
|
9
9
|
defaultValue?: string
|
|
10
10
|
/** Stops editing and dims the field. On iOS it outranks `error`. */
|
|
11
11
|
disabled?: boolean
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Marks the field invalid: a destructive outline and label. A message is shown under the field as well — on Android
|
|
14
|
+
* as Material's supporting text, beside its error icon — so the error never rests on colour alone; Material's error
|
|
15
|
+
* colour is a pale red in dark mode. `true` marks it without a message.
|
|
16
|
+
*/
|
|
17
|
+
error?: boolean | string
|
|
14
18
|
/** Shown above the field, in `labelMedium`. */
|
|
15
19
|
label?: string
|
|
16
20
|
/** Wraps the text and grows the field to fit it. iOS ignores it when `secure`. */
|
|
@@ -1,25 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
AnimatedVisibility,
|
|
3
|
+
Box,
|
|
4
|
+
Column,
|
|
5
|
+
Text as ComposeText,
|
|
6
|
+
EnterTransition,
|
|
7
|
+
ExitTransition,
|
|
8
|
+
Row,
|
|
9
|
+
} from '@expo/ui/jetpack-compose'
|
|
10
|
+
import {
|
|
11
|
+
animated,
|
|
12
|
+
clickable,
|
|
13
|
+
defaultMinSize,
|
|
14
|
+
fillMaxWidth,
|
|
15
|
+
graphicsLayer,
|
|
16
|
+
tween,
|
|
17
|
+
} from '@expo/ui/jetpack-compose/modifiers'
|
|
18
|
+
import {duration, useAppTheme} from '@hzblj/kara-theme'
|
|
4
19
|
import {useMemo} from 'react'
|
|
5
20
|
import {Glyph} from '../../glyph'
|
|
6
21
|
import {useHostGuard} from '../../host-context'
|
|
7
22
|
import type {CollapsibleComponent, CollapsibleProps} from './collapsible.types'
|
|
8
23
|
|
|
24
|
+
/** Material's standard easing, at the length a disclosure takes. */
|
|
25
|
+
const TURN = tween({durationMillis: duration.d200, easing: 'fastOutSlowIn'})
|
|
26
|
+
|
|
27
|
+
/** Down from the row, fading in — Compose's default grows from a corner, sideways as well as down. */
|
|
28
|
+
const ENTER = EnterTransition.expandVertically().plus(EnterTransition.fadeIn())
|
|
29
|
+
const EXIT = ExitTransition.shrinkVertically().plus(ExitTransition.fadeOut())
|
|
30
|
+
|
|
31
|
+
const WIDE = [fillMaxWidth()]
|
|
32
|
+
|
|
33
|
+
/** Material's least touch target. */
|
|
34
|
+
const ROW_HEIGHT = 48
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A pressable row and an animated reveal: the content expands down from the row, and the chevron turns a quarter
|
|
38
|
+
* toward it, as a Material disclosure does — it used to jump between two glyphs. The row spans its parent, the chevron
|
|
39
|
+
* at the far end, and is never shorter than a touch target.
|
|
40
|
+
*/
|
|
9
41
|
export const Collapsible: CollapsibleComponent = ({children, expanded, label, onExpandedChange}: CollapsibleProps) => {
|
|
10
42
|
useHostGuard('Collapsible')
|
|
11
43
|
const theme = useAppTheme()
|
|
12
|
-
const modifiers = useMemo(
|
|
44
|
+
const modifiers = useMemo(
|
|
45
|
+
() => [fillMaxWidth(), defaultMinSize({minHeight: ROW_HEIGHT}), clickable(() => onExpandedChange(!expanded))],
|
|
46
|
+
[expanded, onExpandedChange]
|
|
47
|
+
)
|
|
48
|
+
const turn = useMemo(() => [graphicsLayer({rotationZ: animated(expanded ? 90 : 0, TURN)})], [expanded])
|
|
13
49
|
|
|
14
50
|
return (
|
|
15
|
-
<Column>
|
|
51
|
+
<Column modifiers={WIDE}>
|
|
16
52
|
<Row horizontalArrangement="spaceBetween" modifiers={modifiers} verticalAlignment="center">
|
|
17
53
|
<ComposeText color={theme.colorString('content.primary')} style={theme.typography.body}>
|
|
18
54
|
{label}
|
|
19
55
|
</ComposeText>
|
|
20
|
-
<
|
|
56
|
+
<Box modifiers={turn}>
|
|
57
|
+
<Glyph name="chevronRight" size="s20" />
|
|
58
|
+
</Box>
|
|
21
59
|
</Row>
|
|
22
|
-
<AnimatedVisibility visible={expanded}>
|
|
60
|
+
<AnimatedVisibility enterTransition={ENTER} exitTransition={EXIT} visible={expanded}>
|
|
61
|
+
{children}
|
|
62
|
+
</AnimatedVisibility>
|
|
23
63
|
</Column>
|
|
24
64
|
)
|
|
25
65
|
}
|
|
@@ -2,17 +2,12 @@ import {iconResourceName} from '@hzblj/kara-native'
|
|
|
2
2
|
import {TopAppBar, type TopAppBarItem} from '@hzblj/kara-native/jetpack-compose'
|
|
3
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
4
|
import {useMemo} from 'react'
|
|
5
|
-
import {StyleSheet} from 'react-native'
|
|
6
5
|
import {Island} from '../../atoms/island'
|
|
7
6
|
import type {NavigationBarComponent, NavigationBarItem, NavigationBarProps} from './navigation-bar.types'
|
|
8
7
|
|
|
9
8
|
/** Material's small top app bar. */
|
|
10
9
|
const BAR_HEIGHT = 64
|
|
11
10
|
|
|
12
|
-
const styles = StyleSheet.create({
|
|
13
|
-
bar: {height: BAR_HEIGHT, width: '100%'},
|
|
14
|
-
})
|
|
15
|
-
|
|
16
11
|
const DEFAULT_LABELS = {back: 'Back', close: 'Close'} as const
|
|
17
12
|
|
|
18
13
|
const toNativeItem = ({onPress, testID, ...item}: NavigationBarItem): TopAppBarItem =>
|
|
@@ -38,7 +33,7 @@ export const NavigationBar: NavigationBarComponent = ({
|
|
|
38
33
|
const nativeTrailingItems = useMemo(() => trailingItems?.map(toNativeItem), [trailingItems])
|
|
39
34
|
|
|
40
35
|
return (
|
|
41
|
-
<Island
|
|
36
|
+
<Island h={BAR_HEIGHT} matchContents={false} testID={testID} w="100%">
|
|
42
37
|
<TopAppBar
|
|
43
38
|
fontFamily={font.fontFamily}
|
|
44
39
|
fontSize={font.fontSize}
|
|
@@ -4,15 +4,17 @@ import {spacing} from '@hzblj/kara-theme'
|
|
|
4
4
|
import {Children, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
5
5
|
|
|
6
6
|
import {HStack} from '../../atoms/h-stack'
|
|
7
|
+
import {Spacer} from '../../atoms/spacer'
|
|
7
8
|
import {VStack} from '../../atoms/v-stack'
|
|
8
9
|
import {useHostGuard} from '../../host-context'
|
|
9
10
|
import type {PagerComponent, PagerProps} from './pager.types'
|
|
10
11
|
|
|
11
12
|
const DOT_SIZE = 6
|
|
12
13
|
|
|
13
|
-
/** Compose ships no page indicator, so the dots are composed out of atoms. */
|
|
14
|
+
/** Compose ships no page indicator, so the dots are composed out of atoms — centred between two spacers. */
|
|
14
15
|
const PagerDots = ({count, current}: {count: number; current: number}) => (
|
|
15
16
|
<HStack align="center" py="s12" spacing="s6" wide>
|
|
17
|
+
<Spacer />
|
|
16
18
|
{Array.from({length: count}, (_, index) => (
|
|
17
19
|
<VStack
|
|
18
20
|
bg={index === current ? 'content.primary' : 'fill.tertiary'}
|
|
@@ -22,6 +24,7 @@ const PagerDots = ({count, current}: {count: number; current: number}) => (
|
|
|
22
24
|
w={DOT_SIZE}
|
|
23
25
|
/>
|
|
24
26
|
))}
|
|
27
|
+
<Spacer />
|
|
25
28
|
</HStack>
|
|
26
29
|
)
|
|
27
30
|
|
|
@@ -1,9 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {duration, opacity} from '@hzblj/kara-theme'
|
|
2
|
+
import {cloneElement, type ReactNode, useCallback, useRef} from 'react'
|
|
3
|
+
import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native'
|
|
4
|
+
import {type SharedValue, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
|
|
5
|
+
|
|
6
|
+
import {ABox, Box} from '../../atoms/box'
|
|
2
7
|
import type {ScrollEdgeContainerComponent, ScrollEdgeContainerProps} from './scroll-edge-container.types'
|
|
3
8
|
|
|
9
|
+
/** Less than a pixel scrolled is not scrolled — a fling settles a fraction away from the edge. */
|
|
10
|
+
const EDGE_SLOP = 0.5
|
|
11
|
+
|
|
12
|
+
const FILL = {duration: duration.d200}
|
|
13
|
+
|
|
14
|
+
type Bar = {children: ReactNode; filled: SharedValue<number>}
|
|
15
|
+
|
|
16
|
+
/** A bar, and behind it the container colour it fills with — faded in and out, not switched. */
|
|
17
|
+
const EdgeBar = ({children, filled}: Bar) => {
|
|
18
|
+
const fill = useAnimatedStyle(() => ({opacity: filled.value}), [])
|
|
19
|
+
|
|
20
|
+
if (!children) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Box>
|
|
26
|
+
<ABox
|
|
27
|
+
bg="surface.secondary"
|
|
28
|
+
bottom={0}
|
|
29
|
+
left={0}
|
|
30
|
+
pointerEvents="none"
|
|
31
|
+
position="absolute"
|
|
32
|
+
right={0}
|
|
33
|
+
style={fill}
|
|
34
|
+
top={0}
|
|
35
|
+
/>
|
|
36
|
+
{children}
|
|
37
|
+
</Box>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
4
41
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
42
|
+
* Material's take on the scroll edge: a React Native scroll view has no `contentInset` to slide content under a bar on
|
|
43
|
+
* Android, so the bars stack above and below it, and do what a Material top app bar does on scroll — transparent at
|
|
44
|
+
* rest, filled with `surface.secondary` (the scrolled container colour, `surfaceContainer`) once content has scrolled
|
|
45
|
+
* under it. The bottom bar mirrors it: filled while there is more content below. The scroll view keeps its own
|
|
46
|
+
* `onScroll`, `onLayout` and `onContentSizeChange`.
|
|
7
47
|
*/
|
|
8
48
|
export const ScrollEdgeContainer: ScrollEdgeContainerComponent = ({
|
|
9
49
|
automaticInsets: _automaticInsets,
|
|
@@ -13,10 +53,63 @@ export const ScrollEdgeContainer: ScrollEdgeContainerComponent = ({
|
|
|
13
53
|
testID,
|
|
14
54
|
topBar,
|
|
15
55
|
...sx
|
|
16
|
-
}: ScrollEdgeContainerProps) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
56
|
+
}: ScrollEdgeContainerProps) => {
|
|
57
|
+
const topFilled = useSharedValue<number>(opacity.o0)
|
|
58
|
+
const bottomFilled = useSharedValue<number>(opacity.o0)
|
|
59
|
+
const metrics = useRef({content: 0, offset: 0, viewport: 0})
|
|
60
|
+
const state = useRef({bottom: false, top: false})
|
|
61
|
+
const {flex, onContentSizeChange, onLayout, onScroll, scrollEventThrottle} = children.props as {
|
|
62
|
+
flex?: number
|
|
63
|
+
onContentSizeChange?: (width: number, height: number) => void
|
|
64
|
+
onLayout?: (event: LayoutChangeEvent) => void
|
|
65
|
+
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
|
|
66
|
+
scrollEventThrottle?: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Only a change of state animates; a scroll event restating the same one would restart the fade.
|
|
70
|
+
const update = useCallback(() => {
|
|
71
|
+
const {content, offset, viewport} = metrics.current
|
|
72
|
+
const top = offset > EDGE_SLOP
|
|
73
|
+
const bottom = viewport > 0 && offset + viewport < content - EDGE_SLOP
|
|
74
|
+
|
|
75
|
+
if (top !== state.current.top) {
|
|
76
|
+
state.current.top = top
|
|
77
|
+
topFilled.value = withTiming(top ? opacity.o100 : opacity.o0, FILL)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (bottom !== state.current.bottom) {
|
|
81
|
+
state.current.bottom = bottom
|
|
82
|
+
bottomFilled.value = withTiming(bottom ? opacity.o100 : opacity.o0, FILL)
|
|
83
|
+
}
|
|
84
|
+
}, [bottomFilled, topFilled])
|
|
85
|
+
|
|
86
|
+
// `flex` so the scroll view takes the room between the bars — left to its content, it grew to the content's height,
|
|
87
|
+
// ran under the bottom bar and had nothing to scroll.
|
|
88
|
+
const scrollable = cloneElement(children, {
|
|
89
|
+
flex: flex ?? 1,
|
|
90
|
+
onContentSizeChange: (width: number, height: number) => {
|
|
91
|
+
metrics.current.content = height
|
|
92
|
+
update()
|
|
93
|
+
onContentSizeChange?.(width, height)
|
|
94
|
+
},
|
|
95
|
+
onLayout: (event: LayoutChangeEvent) => {
|
|
96
|
+
metrics.current.viewport = event.nativeEvent.layout.height
|
|
97
|
+
update()
|
|
98
|
+
onLayout?.(event)
|
|
99
|
+
},
|
|
100
|
+
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
101
|
+
metrics.current.offset = event.nativeEvent.contentOffset.y
|
|
102
|
+
update()
|
|
103
|
+
onScroll?.(event)
|
|
104
|
+
},
|
|
105
|
+
scrollEventThrottle: scrollEventThrottle ?? 16,
|
|
106
|
+
} as Partial<typeof children.props>)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Box {...sx} testID={testID}>
|
|
110
|
+
<EdgeBar filled={topFilled}>{topBar}</EdgeBar>
|
|
111
|
+
<Box flex={1}>{scrollable}</Box>
|
|
112
|
+
<EdgeBar filled={bottomFilled}>{bottomBar}</EdgeBar>
|
|
113
|
+
</Box>
|
|
114
|
+
)
|
|
115
|
+
}
|