@ankhorage/surface 1.0.3 → 1.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +17 -1
  3. package/dist/components/navigation/DrawerNavigation.d.ts +4 -0
  4. package/dist/components/navigation/DrawerNavigation.d.ts.map +1 -0
  5. package/dist/components/navigation/DrawerNavigation.js +13 -0
  6. package/dist/components/navigation/DrawerNavigation.js.map +1 -0
  7. package/dist/components/navigation/DrawerNavigationItem.d.ts +4 -0
  8. package/dist/components/navigation/DrawerNavigationItem.d.ts.map +1 -0
  9. package/dist/components/navigation/DrawerNavigationItem.js +9 -0
  10. package/dist/components/navigation/DrawerNavigationItem.js.map +1 -0
  11. package/dist/components/navigation/NavigationItem.d.ts +4 -0
  12. package/dist/components/navigation/NavigationItem.d.ts.map +1 -0
  13. package/dist/components/navigation/NavigationItem.js +41 -0
  14. package/dist/components/navigation/NavigationItem.js.map +1 -0
  15. package/dist/components/navigation/NavigationList.d.ts +4 -0
  16. package/dist/components/navigation/NavigationList.d.ts.map +1 -0
  17. package/dist/components/navigation/NavigationList.js +9 -0
  18. package/dist/components/navigation/NavigationList.js.map +1 -0
  19. package/dist/components/navigation/TabBar.d.ts +4 -0
  20. package/dist/components/navigation/TabBar.d.ts.map +1 -0
  21. package/dist/components/navigation/TabBar.js +15 -0
  22. package/dist/components/navigation/TabBar.js.map +1 -0
  23. package/dist/components/navigation/TabBarItem.d.ts +4 -0
  24. package/dist/components/navigation/TabBarItem.d.ts.map +1 -0
  25. package/dist/components/navigation/TabBarItem.js +44 -0
  26. package/dist/components/navigation/TabBarItem.js.map +1 -0
  27. package/dist/components/navigation/helpers.d.ts +15 -0
  28. package/dist/components/navigation/helpers.d.ts.map +1 -0
  29. package/dist/components/navigation/helpers.js +40 -0
  30. package/dist/components/navigation/helpers.js.map +1 -0
  31. package/dist/components/navigation/index.d.ts +8 -0
  32. package/dist/components/navigation/index.d.ts.map +1 -0
  33. package/dist/components/navigation/index.js +7 -0
  34. package/dist/components/navigation/index.js.map +1 -0
  35. package/dist/components/navigation/types.d.ts +51 -0
  36. package/dist/components/navigation/types.d.ts.map +1 -0
  37. package/dist/components/navigation/types.js +2 -0
  38. package/dist/components/navigation/types.js.map +1 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/primitives/image/Image.d.ts +4 -0
  44. package/dist/primitives/image/Image.d.ts.map +1 -0
  45. package/dist/primitives/image/Image.js +28 -0
  46. package/dist/primitives/image/Image.js.map +1 -0
  47. package/dist/primitives/image/index.d.ts +3 -0
  48. package/dist/primitives/image/index.d.ts.map +1 -0
  49. package/dist/primitives/image/index.js +2 -0
  50. package/dist/primitives/image/index.js.map +1 -0
  51. package/dist/primitives/image/resolveImageProps.d.ts +12 -0
  52. package/dist/primitives/image/resolveImageProps.d.ts.map +1 -0
  53. package/dist/primitives/image/resolveImageProps.js +67 -0
  54. package/dist/primitives/image/resolveImageProps.js.map +1 -0
  55. package/dist/primitives/image/types.d.ts +21 -0
  56. package/dist/primitives/image/types.d.ts.map +1 -0
  57. package/dist/primitives/image/types.js +2 -0
  58. package/dist/primitives/image/types.js.map +1 -0
  59. package/package.json +2 -2
  60. package/src/components/navigation/DrawerNavigation.tsx +25 -0
  61. package/src/components/navigation/DrawerNavigationItem.tsx +17 -0
  62. package/src/components/navigation/NavigationItem.tsx +77 -0
  63. package/src/components/navigation/NavigationList.tsx +24 -0
  64. package/src/components/navigation/TabBar.tsx +25 -0
  65. package/src/components/navigation/TabBarItem.tsx +82 -0
  66. package/src/components/navigation/helpers.test.ts +74 -0
  67. package/src/components/navigation/helpers.ts +72 -0
  68. package/src/components/navigation/index.ts +16 -0
  69. package/src/components/navigation/policy.test.ts +44 -0
  70. package/src/components/navigation/types.ts +59 -0
  71. package/src/index.test.ts +2 -0
  72. package/src/index.ts +20 -0
  73. package/src/primitives/image/Image.tsx +68 -0
  74. package/src/primitives/image/index.ts +2 -0
  75. package/src/primitives/image/resolveImageProps.test.ts +81 -0
  76. package/src/primitives/image/resolveImageProps.ts +92 -0
  77. package/src/primitives/image/types.ts +28 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Image.js","sourceRoot":"","sources":["../../../src/primitives/image/Image.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,KAAK,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EACL,YAAY,EACZ,eAAe,EACf,yBAAyB,EACzB,UAAU,EACV,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,UAAU,KAAK,CAAC,EACpB,MAAM,EACN,cAAc,EACd,GAAG,EACH,kBAAkB,EAClB,KAAK,EACL,MAAM,EACN,WAAW,EACX,GAAG,EACH,UAAU,EACV,MAAM,EACN,KAAK,EACL,MAAM,EACN,OAAO,GACI;IACX,MAAM,EAAE,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAC;IAC7B,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9D,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEvE,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,eAAe,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAEjB,MAAM,iBAAiB,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,kBAAkB,GAAG,eAAe,CAAC,cAAc,CAAC,CAAC;IAC3D,MAAM,cAAc,GAAG,qBAAqB,CAAC,iBAAiB,EAAE,kBAAkB,EAAE,YAAY,CAAC,CAAC;IAElG,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IAEjC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IAC9D,MAAM,0BAA0B,GAAG,yBAAyB,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;IACtF,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;IACxD,MAAM,aAAa,GAAG,iBAAiB,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC;IAEvF,OAAO,CACL,CAAC,OAAO,CACN,kBAAkB,CAAC,CAAC,0BAA0B,CAAC,CAC/C,iBAAiB,CAAC,OAAO,CACzB,GAAG,CAAC,CAAC,WAAW,CAAC,CACjB,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;YACjB,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;YAEjB,IAAI,CAAC,YAAY,IAAI,kBAAkB,EAAE,CAAC;gBACxC,eAAe,CAAC,IAAI,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,CAAC,CACF,UAAU,CAAC,CAAC,kBAAkB,CAAC,CAC/B,MAAM,CAAC,CAAC,cAAc,CAAC,CACvB,KAAK,CAAC,CAAC,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,CAC9B,MAAM,CAAC,CAAC,MAAM,CAAC,EACf,CACH,CAAC;AACJ,CAAC","sourcesContent":["import React from 'react';\nimport { Image as RNImage } from 'react-native';\n\nimport { useTheme } from '../../theme/ThemeContext';\nimport {\n getSourceKey,\n normalizeSource,\n resolveAccessibilityLabel,\n resolveAlt,\n resolveImageStyle,\n resolveRenderedSource,\n resolveResizeMode,\n} from './resolveImageProps';\nimport type { ImageProps } from './types';\n\nexport function Image({\n source,\n fallbackSource,\n alt,\n accessibilityLabel,\n width,\n height,\n aspectRatio,\n fit,\n resizeMode,\n radius,\n style,\n testID,\n onError,\n}: ImageProps) {\n const { theme } = useTheme();\n const [showFallback, setShowFallback] = React.useState(false);\n const primaryKey = React.useMemo(() => getSourceKey(source), [source]);\n\n React.useEffect(() => {\n setShowFallback(false);\n }, [primaryKey]);\n\n const normalizedPrimary = normalizeSource(source);\n const normalizedFallback = normalizeSource(fallbackSource);\n const resolvedSource = resolveRenderedSource(normalizedPrimary, normalizedFallback, showFallback);\n\n if (!resolvedSource) return null;\n\n const resolvedResizeMode = resolveResizeMode(fit, resizeMode);\n const resolvedAccessibilityLabel = resolveAccessibilityLabel(alt, accessibilityLabel);\n const resolvedAlt = resolveAlt(alt, accessibilityLabel);\n const resolvedStyle = resolveImageStyle(theme, { width, height, aspectRatio, radius });\n\n return (\n <RNImage\n accessibilityLabel={resolvedAccessibilityLabel}\n accessibilityRole=\"image\"\n alt={resolvedAlt}\n onError={(event) => {\n onError?.(event);\n\n if (!showFallback && normalizedFallback) {\n setShowFallback(true);\n }\n }}\n resizeMode={resolvedResizeMode}\n source={resolvedSource}\n style={[resolvedStyle, style]}\n testID={testID}\n />\n );\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export { Image } from './Image';
2
+ export type { ImageFit, ImageProps, SurfaceImageSource } from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/primitives/image/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { Image } from './Image';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/primitives/image/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC","sourcesContent":["export { Image } from './Image';\nexport type { ImageFit, ImageProps, SurfaceImageSource } from './types';\n"]}
@@ -0,0 +1,12 @@
1
+ import type { ImageResizeMode, ImageSourcePropType } from 'react-native';
2
+ import type { ImageStyle } from 'react-native';
3
+ import type { SurfaceTheme } from '../../theme/types';
4
+ import type { ImageProps, SurfaceImageSource } from './types';
5
+ export declare function normalizeSource(source: SurfaceImageSource | null | undefined): ImageSourcePropType | undefined;
6
+ export declare function resolveRenderedSource(normalizedPrimary: ImageSourcePropType | undefined, normalizedFallback: ImageSourcePropType | undefined, showFallback: boolean): ImageSourcePropType | undefined;
7
+ export declare function getSourceKey(source: SurfaceImageSource | null | undefined): string | null;
8
+ export declare function resolveResizeMode(fit: ImageProps['fit'], resizeMode: ImageProps['resizeMode']): ImageResizeMode;
9
+ export declare function resolveAccessibilityLabel(alt: ImageProps['alt'], accessibilityLabel: ImageProps['accessibilityLabel']): string | undefined;
10
+ export declare function resolveAlt(alt: ImageProps['alt'], accessibilityLabel: ImageProps['accessibilityLabel']): string | undefined;
11
+ export declare function resolveImageStyle(theme: SurfaceTheme, props: Pick<ImageProps, 'width' | 'height' | 'aspectRatio' | 'radius'>): ImageStyle;
12
+ //# sourceMappingURL=resolveImageProps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveImageProps.d.ts","sourceRoot":"","sources":["../../../src/primitives/image/resolveImageProps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE9D,wBAAgB,eAAe,CAC7B,MAAM,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,GAC5C,mBAAmB,GAAG,SAAS,CAIjC;AAED,wBAAgB,qBAAqB,CACnC,iBAAiB,EAAE,mBAAmB,GAAG,SAAS,EAClD,kBAAkB,EAAE,mBAAmB,GAAG,SAAS,EACnD,YAAY,EAAE,OAAO,GACpB,mBAAmB,GAAG,SAAS,CAGjC;AAYD,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAmBzF;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EACtB,UAAU,EAAE,UAAU,CAAC,YAAY,CAAC,GACnC,eAAe,CAEjB;AAED,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EACtB,kBAAkB,EAAE,UAAU,CAAC,oBAAoB,CAAC,GACnD,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EACtB,kBAAkB,EAAE,UAAU,CAAC,oBAAoB,CAAC,GACnD,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,QAAQ,GAAG,aAAa,GAAG,QAAQ,CAAC,GACrE,UAAU,CAYZ"}
@@ -0,0 +1,67 @@
1
+ import { resolveToken } from '../../theme/resolveToken';
2
+ export function normalizeSource(source) {
3
+ if (source === null || source === undefined)
4
+ return undefined;
5
+ if (typeof source === 'string')
6
+ return { uri: source };
7
+ return source;
8
+ }
9
+ export function resolveRenderedSource(normalizedPrimary, normalizedFallback, showFallback) {
10
+ if (showFallback || !normalizedPrimary)
11
+ return normalizedFallback;
12
+ return normalizedPrimary;
13
+ }
14
+ function getUriFromSource(source) {
15
+ if (Array.isArray(source)) {
16
+ const [first] = source;
17
+ return first?.uri;
18
+ }
19
+ if (typeof source === 'number')
20
+ return undefined;
21
+ return source.uri;
22
+ }
23
+ export function getSourceKey(source) {
24
+ if (source === null || source === undefined)
25
+ return null;
26
+ if (typeof source === 'string')
27
+ return `uri:${source}`;
28
+ if (typeof source === 'number')
29
+ return `asset:${source}`;
30
+ if (Array.isArray(source)) {
31
+ const parts = source
32
+ .map((item) => item.uri)
33
+ .filter((value) => typeof value === 'string');
34
+ return `uri-set:${parts.join('|')}`;
35
+ }
36
+ const uri = getUriFromSource(source);
37
+ if (uri)
38
+ return `uri:${uri}`;
39
+ try {
40
+ return `src:${JSON.stringify(source)}`;
41
+ }
42
+ catch {
43
+ return 'src:unknown';
44
+ }
45
+ }
46
+ export function resolveResizeMode(fit, resizeMode) {
47
+ return fit ?? resizeMode ?? 'cover';
48
+ }
49
+ export function resolveAccessibilityLabel(alt, accessibilityLabel) {
50
+ return accessibilityLabel ?? alt;
51
+ }
52
+ export function resolveAlt(alt, accessibilityLabel) {
53
+ return alt ?? accessibilityLabel;
54
+ }
55
+ export function resolveImageStyle(theme, props) {
56
+ const width = resolveToken(theme.spacing, props.width);
57
+ const height = resolveToken(theme.spacing, props.height);
58
+ const borderRadius = resolveToken(theme.radii, props.radius);
59
+ return {
60
+ width,
61
+ height,
62
+ aspectRatio: props.aspectRatio,
63
+ borderRadius,
64
+ overflow: borderRadius !== undefined ? 'hidden' : undefined,
65
+ };
66
+ }
67
+ //# sourceMappingURL=resolveImageProps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveImageProps.js","sourceRoot":"","sources":["../../../src/primitives/image/resolveImageProps.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageResizeMode, ImageSourcePropType } from 'react-native';\nimport type { ImageStyle } from 'react-native';\n\nimport { resolveToken } from '../../theme/resolveToken';\nimport type { SurfaceTheme } from '../../theme/types';\nimport type { ImageProps, SurfaceImageSource } from './types';\n\nexport function normalizeSource(\n source: SurfaceImageSource | null | undefined,\n): ImageSourcePropType | undefined {\n if (source === null || source === undefined) return undefined;\n if (typeof source === 'string') return { uri: source };\n return source;\n}\n\nexport function resolveRenderedSource(\n normalizedPrimary: ImageSourcePropType | undefined,\n normalizedFallback: ImageSourcePropType | undefined,\n showFallback: boolean,\n): ImageSourcePropType | undefined {\n if (showFallback || !normalizedPrimary) return normalizedFallback;\n return normalizedPrimary;\n}\n\nfunction getUriFromSource(source: ImageSourcePropType): string | undefined {\n if (Array.isArray(source)) {\n const [first] = source;\n return first?.uri;\n }\n\n if (typeof source === 'number') return undefined;\n return source.uri;\n}\n\nexport function getSourceKey(source: SurfaceImageSource | null | undefined): string | null {\n if (source === null || source === undefined) return null;\n if (typeof source === 'string') return `uri:${source}`;\n if (typeof source === 'number') return `asset:${source}`;\n if (Array.isArray(source)) {\n const parts = source\n .map((item) => item.uri)\n .filter((value): value is string => typeof value === 'string');\n return `uri-set:${parts.join('|')}`;\n }\n\n const uri = getUriFromSource(source);\n if (uri) return `uri:${uri}`;\n\n try {\n return `src:${JSON.stringify(source)}`;\n } catch {\n return 'src:unknown';\n }\n}\n\nexport function resolveResizeMode(\n fit: ImageProps['fit'],\n resizeMode: ImageProps['resizeMode'],\n): ImageResizeMode {\n return fit ?? resizeMode ?? 'cover';\n}\n\nexport function resolveAccessibilityLabel(\n alt: ImageProps['alt'],\n accessibilityLabel: ImageProps['accessibilityLabel'],\n): string | undefined {\n return accessibilityLabel ?? alt;\n}\n\nexport function resolveAlt(\n alt: ImageProps['alt'],\n accessibilityLabel: ImageProps['accessibilityLabel'],\n): string | undefined {\n return alt ?? accessibilityLabel;\n}\n\nexport function resolveImageStyle(\n theme: SurfaceTheme,\n props: Pick<ImageProps, 'width' | 'height' | 'aspectRatio' | 'radius'>,\n): ImageStyle {\n const width = resolveToken(theme.spacing, props.width);\n const height = resolveToken(theme.spacing, props.height);\n const borderRadius = resolveToken(theme.radii, props.radius);\n\n return {\n width,\n height,\n aspectRatio: props.aspectRatio,\n borderRadius,\n overflow: borderRadius !== undefined ? 'hidden' : undefined,\n };\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import type { ImageProps as ReactNativeImageProps, ImageResizeMode, ImageSourcePropType } from 'react-native';
2
+ import type { ImageStyle, StyleProp } from 'react-native';
3
+ import type { SurfaceTheme } from '../../theme/types';
4
+ export type SurfaceImageSource = string | ImageSourcePropType;
5
+ export type ImageFit = ImageResizeMode;
6
+ export interface ImageProps {
7
+ source?: SurfaceImageSource | null;
8
+ fallbackSource?: SurfaceImageSource | null;
9
+ alt?: string;
10
+ accessibilityLabel?: string;
11
+ width?: number | string;
12
+ height?: number | string;
13
+ aspectRatio?: number;
14
+ fit?: ImageFit;
15
+ resizeMode?: ImageResizeMode;
16
+ radius?: number | keyof SurfaceTheme['radii'];
17
+ style?: StyleProp<ImageStyle>;
18
+ testID?: string;
19
+ onError?: ReactNativeImageProps['onError'];
20
+ }
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/primitives/image/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,IAAI,qBAAqB,EACnC,eAAe,EACf,mBAAmB,EACpB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,mBAAmB,CAAC;AAE9D,MAAM,MAAM,QAAQ,GAAG,eAAe,CAAC;AAEvC,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACnC,cAAc,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAC;CAC5C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/primitives/image/types.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n ImageProps as ReactNativeImageProps,\n ImageResizeMode,\n ImageSourcePropType,\n} from 'react-native';\nimport type { ImageStyle, StyleProp } from 'react-native';\n\nimport type { SurfaceTheme } from '../../theme/types';\n\nexport type SurfaceImageSource = string | ImageSourcePropType;\n\nexport type ImageFit = ImageResizeMode;\n\nexport interface ImageProps {\n source?: SurfaceImageSource | null;\n fallbackSource?: SurfaceImageSource | null;\n alt?: string;\n accessibilityLabel?: string;\n width?: number | string;\n height?: number | string;\n aspectRatio?: number;\n fit?: ImageFit;\n resizeMode?: ImageResizeMode;\n radius?: number | keyof SurfaceTheme['radii'];\n style?: StyleProp<ImageStyle>;\n testID?: string;\n onError?: ReactNativeImageProps['onError'];\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/surface",
3
3
  "type": "module",
4
- "version": "1.0.3",
4
+ "version": "1.2.0",
5
5
  "description": "Stable UI foundation for React Native and React Native Web.",
6
6
  "homepage": "https://github.com/ankhorage/surface#readme",
7
7
  "bugs": {
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@ankhorage/color-theory": "^0.0.4",
45
- "@ankhorage/contracts": "^1.1.0"
45
+ "@ankhorage/contracts": "^1.2.0"
46
46
  },
47
47
  "files": [
48
48
  "dist",
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+
3
+ import { Stack } from '../../layout';
4
+ import { DrawerNavigationItem } from './DrawerNavigationItem';
5
+ import type { DrawerNavigationProps } from './types';
6
+
7
+ export function DrawerNavigation({
8
+ items,
9
+ header,
10
+ footer,
11
+ compact = false,
12
+ testID,
13
+ }: DrawerNavigationProps) {
14
+ return (
15
+ <Stack gap={compact ? 's' : 'm'} testID={testID}>
16
+ {header}
17
+ <Stack gap={compact ? 'xs' : 's'}>
18
+ {items.map((item) => (
19
+ <DrawerNavigationItem compact={compact} item={item} key={item.id} testID={testID} />
20
+ ))}
21
+ </Stack>
22
+ {footer}
23
+ </Stack>
24
+ );
25
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+
3
+ import { NavigationItem } from './NavigationItem';
4
+ import type { DrawerNavigationItemProps } from './types';
5
+
6
+ export function DrawerNavigationItem({ item, compact = false, testID }: DrawerNavigationItemProps) {
7
+ return (
8
+ <NavigationItem
9
+ compact={compact}
10
+ item={{
11
+ ...item,
12
+ accessibilityRole: item.accessibilityRole ?? 'button',
13
+ }}
14
+ testID={testID}
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Box } from '../../layout';
5
+ import { ButtonBase } from '../../primitives/button-base';
6
+ import { Icon } from '../../primitives/icon';
7
+ import { Text } from '../../primitives/text';
8
+ import { useTheme } from '../../theme/ThemeContext';
9
+ import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
10
+ import type { NavigationItemProps } from './types';
11
+
12
+ export function NavigationItem({ item, compact = false, testID }: NavigationItemProps) {
13
+ const { theme } = useTheme();
14
+ const active = Boolean(item.active);
15
+ const disabled = Boolean(item.disabled);
16
+ const resolvedTestID = item.testID ?? (testID ? `${testID}-item-${item.id}` : undefined);
17
+ const resolvedAccessibilityState = resolveNavigationAccessibilityState(
18
+ active,
19
+ disabled,
20
+ item.accessibilityState,
21
+ );
22
+
23
+ return (
24
+ <ButtonBase
25
+ accessibilityLabel={item.accessibilityLabel}
26
+ accessibilityRole={item.accessibilityRole ?? 'button'}
27
+ accessibilityState={resolvedAccessibilityState}
28
+ disabled={disabled}
29
+ onPress={item.onPress}
30
+ testID={resolvedTestID}
31
+ >
32
+ {(state) => {
33
+ const presentation = resolveNavigationItemPresentation(theme, {
34
+ active,
35
+ disabled,
36
+ state,
37
+ });
38
+
39
+ return (
40
+ <Box
41
+ px="m"
42
+ py={compact ? 's' : 'm'}
43
+ radius="m"
44
+ style={{
45
+ backgroundColor: presentation.backgroundColor,
46
+ flexDirection: 'row',
47
+ alignItems: 'center',
48
+ opacity: presentation.opacity,
49
+ }}
50
+ >
51
+ {item.icon ? (
52
+ <View style={{ marginRight: theme.spacing.s }}>
53
+ <Icon
54
+ color={presentation.contentColor}
55
+ name={item.icon.name}
56
+ provider={item.icon.provider}
57
+ size={compact ? 's' : 'm'}
58
+ />
59
+ </View>
60
+ ) : null}
61
+ <Box flex={1}>
62
+ <Text
63
+ color={presentation.contentColor}
64
+ numberOfLines={1}
65
+ variant={compact ? 'bodySmall' : 'body'}
66
+ weight="medium"
67
+ >
68
+ {item.label}
69
+ </Text>
70
+ </Box>
71
+ {item.badge ? <View style={{ marginLeft: theme.spacing.s }}>{item.badge}</View> : null}
72
+ </Box>
73
+ );
74
+ }}
75
+ </ButtonBase>
76
+ );
77
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+
3
+ import { Stack } from '../../layout';
4
+ import { NavigationItem } from './NavigationItem';
5
+ import type { NavigationListProps } from './types';
6
+
7
+ export function NavigationList({
8
+ items,
9
+ orientation = 'vertical',
10
+ compact = false,
11
+ testID,
12
+ }: NavigationListProps) {
13
+ return (
14
+ <Stack
15
+ direction={orientation === 'horizontal' ? 'row' : 'column'}
16
+ gap={compact ? 'xs' : 's'}
17
+ testID={testID}
18
+ >
19
+ {items.map((item) => (
20
+ <NavigationItem compact={compact} item={item} key={item.id} testID={testID} />
21
+ ))}
22
+ </Stack>
23
+ );
24
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+
3
+ import { Box } from '../../layout';
4
+ import { useTheme } from '../../theme/ThemeContext';
5
+ import { TabBarItem } from './TabBarItem';
6
+ import type { TabBarProps } from './types';
7
+
8
+ export function TabBar({ items, compact = false, testID }: TabBarProps) {
9
+ const { theme } = useTheme();
10
+
11
+ return (
12
+ <Box
13
+ style={{
14
+ flexDirection: 'row',
15
+ borderTopColor: theme.semantics.border.default,
16
+ borderTopWidth: 1,
17
+ }}
18
+ testID={testID}
19
+ >
20
+ {items.map((item) => (
21
+ <TabBarItem compact={compact} item={item} key={item.id} testID={testID} />
22
+ ))}
23
+ </Box>
24
+ );
25
+ }
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Box } from '../../layout';
5
+ import { ButtonBase } from '../../primitives/button-base';
6
+ import { Icon } from '../../primitives/icon';
7
+ import { Text } from '../../primitives/text';
8
+ import { useTheme } from '../../theme/ThemeContext';
9
+ import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
10
+ import type { TabBarItemProps } from './types';
11
+
12
+ export function TabBarItem({ item, compact = false, testID }: TabBarItemProps) {
13
+ const { theme } = useTheme();
14
+ const active = Boolean(item.active);
15
+ const disabled = Boolean(item.disabled);
16
+ const resolvedTestID = item.testID ?? (testID ? `${testID}-item-${item.id}` : undefined);
17
+ const resolvedAccessibilityState = resolveNavigationAccessibilityState(
18
+ active,
19
+ disabled,
20
+ item.accessibilityState,
21
+ );
22
+
23
+ return (
24
+ <ButtonBase
25
+ accessibilityLabel={item.accessibilityLabel}
26
+ accessibilityRole={item.accessibilityRole ?? 'tab'}
27
+ accessibilityState={resolvedAccessibilityState}
28
+ disabled={disabled}
29
+ onPress={item.onPress}
30
+ style={{ flex: 1 }}
31
+ testID={resolvedTestID}
32
+ >
33
+ {(state) => {
34
+ const presentation = resolveNavigationItemPresentation(theme, {
35
+ active,
36
+ disabled,
37
+ state,
38
+ });
39
+ const contentColor = disabled
40
+ ? theme.semantics.content.muted
41
+ : active
42
+ ? theme.semantics.action.primary.base
43
+ : theme.semantics.content.muted;
44
+
45
+ return (
46
+ <Box
47
+ px="m"
48
+ py={compact ? 's' : 'm'}
49
+ style={{
50
+ flex: 1,
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ backgroundColor: presentation.backgroundColor,
54
+ opacity: presentation.opacity,
55
+ }}
56
+ >
57
+ {item.icon ? (
58
+ <View style={{ marginBottom: item.label ? theme.spacing.xs : 0 }}>
59
+ <Icon
60
+ color={contentColor}
61
+ name={item.icon.name}
62
+ provider={item.icon.provider}
63
+ size={compact ? 's' : 'm'}
64
+ />
65
+ </View>
66
+ ) : null}
67
+ {item.label ? (
68
+ <Text
69
+ color={contentColor}
70
+ numberOfLines={1}
71
+ variant={compact ? 'bodySmall' : 'label'}
72
+ weight="medium"
73
+ >
74
+ {item.label}
75
+ </Text>
76
+ ) : null}
77
+ </Box>
78
+ );
79
+ }}
80
+ </ButtonBase>
81
+ );
82
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { createTheme } from '../../theme/createTheme';
4
+ import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
5
+
6
+ describe('navigation helpers', () => {
7
+ it('forces selected and disabled when active/disabled are true', () => {
8
+ expect(
9
+ resolveNavigationAccessibilityState(true, true, {
10
+ selected: false,
11
+ disabled: false,
12
+ }),
13
+ ).toEqual({
14
+ selected: true,
15
+ disabled: true,
16
+ });
17
+ });
18
+
19
+ it('preserves user-provided state when not active/disabled', () => {
20
+ expect(
21
+ resolveNavigationAccessibilityState(false, false, {
22
+ disabled: false,
23
+ selected: false,
24
+ expanded: true,
25
+ }),
26
+ ).toEqual({
27
+ disabled: false,
28
+ selected: false,
29
+ expanded: true,
30
+ });
31
+ });
32
+
33
+ it('resolves active presentation via primary soft semantics', () => {
34
+ const theme = createTheme();
35
+ const state = { pressed: false, hovered: true, focused: false, disabled: false };
36
+ const resolved = resolveNavigationItemPresentation(theme, {
37
+ active: true,
38
+ disabled: false,
39
+ state,
40
+ });
41
+
42
+ expect(resolved.backgroundColor).toBe(theme.semantics.action.primary.softHover);
43
+ expect(resolved.contentColor).toBe(theme.semantics.action.primary.base);
44
+ expect(resolved.opacity).toBe(1);
45
+ });
46
+
47
+ it('resolves inactive presentation via neutral hover/active semantics', () => {
48
+ const theme = createTheme();
49
+ const state = { pressed: true, hovered: true, focused: false, disabled: false };
50
+ const resolved = resolveNavigationItemPresentation(theme, {
51
+ active: false,
52
+ disabled: false,
53
+ state,
54
+ });
55
+
56
+ expect(resolved.backgroundColor).toBe(theme.semantics.neutral.surfaceActive);
57
+ expect(resolved.contentColor).toBe(theme.semantics.content.default);
58
+ expect(resolved.opacity).toBe(1);
59
+ });
60
+
61
+ it('ignores hover/pressed presentation when disabled', () => {
62
+ const theme = createTheme();
63
+ const state = { pressed: true, hovered: true, focused: false, disabled: true };
64
+ const resolved = resolveNavigationItemPresentation(theme, {
65
+ active: true,
66
+ disabled: true,
67
+ state,
68
+ });
69
+
70
+ expect(resolved.backgroundColor).toBe(theme.semantics.action.primary.softBg);
71
+ expect(resolved.contentColor).toBe(theme.semantics.content.muted);
72
+ expect(resolved.opacity).toBe(0.72);
73
+ });
74
+ });
@@ -0,0 +1,72 @@
1
+ import type { AccessibilityState } from 'react-native';
2
+
3
+ import type { InteractionState } from '../../internal/resolvers/resolveInteractiveState';
4
+ import type { SurfaceTheme } from '../../theme/types';
5
+
6
+ export function resolveNavigationAccessibilityState(
7
+ active: boolean,
8
+ disabled: boolean,
9
+ accessibilityState?: AccessibilityState,
10
+ ): AccessibilityState {
11
+ const resolved: AccessibilityState = { ...(accessibilityState ?? {}) };
12
+
13
+ if (active) {
14
+ resolved.selected = true;
15
+ }
16
+
17
+ if (disabled) {
18
+ resolved.disabled = true;
19
+ }
20
+
21
+ return resolved;
22
+ }
23
+
24
+ export interface ResolvedNavigationItemPresentation {
25
+ backgroundColor: string;
26
+ contentColor: string;
27
+ opacity: number;
28
+ }
29
+
30
+ export function resolveNavigationItemPresentation(
31
+ theme: SurfaceTheme,
32
+ {
33
+ active,
34
+ disabled,
35
+ state,
36
+ }: {
37
+ active: boolean;
38
+ disabled: boolean;
39
+ state: InteractionState;
40
+ },
41
+ ): ResolvedNavigationItemPresentation {
42
+ const interactionState = disabled ? { ...state, hovered: false, pressed: false } : state;
43
+
44
+ const opacity = disabled ? 0.72 : 1;
45
+ const contentColor = disabled
46
+ ? theme.semantics.content.muted
47
+ : active
48
+ ? theme.semantics.action.primary.base
49
+ : theme.semantics.content.default;
50
+
51
+ if (active) {
52
+ return {
53
+ backgroundColor: interactionState.pressed
54
+ ? theme.semantics.action.primary.softActive
55
+ : interactionState.hovered
56
+ ? theme.semantics.action.primary.softHover
57
+ : theme.semantics.action.primary.softBg,
58
+ contentColor,
59
+ opacity,
60
+ };
61
+ }
62
+
63
+ return {
64
+ backgroundColor: interactionState.pressed
65
+ ? theme.semantics.neutral.surfaceActive
66
+ : interactionState.hovered
67
+ ? theme.semantics.neutral.surfaceHover
68
+ : 'transparent',
69
+ contentColor,
70
+ opacity,
71
+ };
72
+ }
@@ -0,0 +1,16 @@
1
+ export { DrawerNavigation } from './DrawerNavigation';
2
+ export { DrawerNavigationItem } from './DrawerNavigationItem';
3
+ export { NavigationItem } from './NavigationItem';
4
+ export { NavigationList } from './NavigationList';
5
+ export { TabBar } from './TabBar';
6
+ export { TabBarItem } from './TabBarItem';
7
+ export type {
8
+ DrawerNavigationItemProps,
9
+ DrawerNavigationProps,
10
+ NavigationItemIcon,
11
+ NavigationItemProps,
12
+ NavigationItemSpec,
13
+ NavigationListProps,
14
+ TabBarItemProps,
15
+ TabBarProps,
16
+ } from './types';
@@ -0,0 +1,44 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { extname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { describe, expect, it } from 'bun:test';
6
+
7
+ const navigationDir = fileURLToPath(new URL('.', import.meta.url));
8
+
9
+ const forbiddenPatterns = [
10
+ 'expo-router',
11
+ '@react-navigation',
12
+ 'AppManifest',
13
+ 'NavigatorSpec',
14
+ 'generated app',
15
+ 'templates',
16
+ ] as const;
17
+
18
+ function listNavigationSources() {
19
+ return readdirSync(navigationDir)
20
+ .filter((entry) => {
21
+ const extension = extname(entry);
22
+ if (!(extension === '.ts' || extension === '.tsx')) {
23
+ return false;
24
+ }
25
+
26
+ return !entry.includes('.test.');
27
+ })
28
+ .map((entry) => join(navigationDir, entry));
29
+ }
30
+
31
+ describe('navigation module boundaries', () => {
32
+ it('does not import routers, manifests, or generated app code', () => {
33
+ const sources = listNavigationSources().map((path) => ({
34
+ path,
35
+ content: readFileSync(path, 'utf8'),
36
+ }));
37
+
38
+ sources.forEach(({ content, path }) => {
39
+ forbiddenPatterns.forEach((pattern) => {
40
+ expect(content, path).not.toContain(pattern);
41
+ });
42
+ });
43
+ });
44
+ });