@hzblj/kara-router 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/navigation-theme.js +45 -0
- package/lib/module/navigation-theme.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/tabs/index.js +5 -0
- package/lib/module/tabs/index.js.map +1 -0
- package/lib/module/tabs/tabs.js +69 -0
- package/lib/module/tabs/tabs.js.map +1 -0
- package/lib/module/tabs/tabs.types.js +4 -0
- package/lib/module/tabs/tabs.types.js.map +1 -0
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/navigation-theme.d.ts +7 -0
- package/lib/typescript/tabs/index.d.ts +2 -0
- package/lib/typescript/tabs/tabs.d.ts +9 -0
- package/lib/typescript/tabs/tabs.types.d.ts +34 -0
- package/package.json +84 -4
- package/src/index.ts +2 -0
- package/src/navigation-theme.ts +43 -0
- package/src/tabs/index.ts +2 -0
- package/src/tabs/tabs.tsx +50 -0
- package/src/tabs/tabs.types.ts +38 -0
package/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hzblj/kara-router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Expo Router pieces of kara-ui: native tabs (`Tabs`) and `navigationTheme`, which maps the active kara theme onto React Navigation's.
|
|
4
|
+
|
|
5
|
+
**Install [`@hzblj/kara-ui`](https://www.npmjs.com/package/@hzblj/kara-ui) instead.** This package is one of its building
|
|
6
|
+
blocks, published so the umbrella can depend on it; it is versioned together with it and has no standalone docs.
|
|
7
|
+
|
|
8
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,cAAc,uBAAoB;AAClC,cAAc,iBAAQ","ignoreList":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { fontFace, resolvePaletteColor } from '@hzblj/kara-theme';
|
|
4
|
+
import { DefaultTheme } from 'expo-router';
|
|
5
|
+
/**
|
|
6
|
+
* React Navigation types every colour as `string`, but the values only ever end up in a React Native
|
|
7
|
+
* style — which accepts any `ColorValue`, including the opaque `PlatformColor` a system token resolves to
|
|
8
|
+
* on iOS. Narrowing here rather than at each call site keeps the assertion to one place.
|
|
9
|
+
*/
|
|
10
|
+
const navColor = (theme, value) => resolvePaletteColor(theme, value);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The app's own face for that weight where the theme names one, and React Navigation's platform default otherwise —
|
|
14
|
+
* which is the platform's face too, so headers and tab labels follow the same typeface as every other piece of text.
|
|
15
|
+
*/
|
|
16
|
+
const navFont = (theme, weight, fallback) => {
|
|
17
|
+
const fontFamily = fontFace(theme.fonts, weight);
|
|
18
|
+
return fontFamily ? {
|
|
19
|
+
fontFamily,
|
|
20
|
+
fontWeight: fallback.fontWeight
|
|
21
|
+
} : fallback;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maps the active theme onto React Navigation's, for expo-router's `ThemeProvider`. Call it inside
|
|
26
|
+
* `KaraProvider` with `useAppTheme()`, so the system half of the palette is resolved for the same variant.
|
|
27
|
+
*/
|
|
28
|
+
export const navigationTheme = theme => ({
|
|
29
|
+
colors: {
|
|
30
|
+
background: 'transparent',
|
|
31
|
+
border: navColor(theme, theme.border.secondary.base),
|
|
32
|
+
card: navColor(theme, theme.surface.primary),
|
|
33
|
+
notification: navColor(theme, theme.fill.accent.primary),
|
|
34
|
+
primary: navColor(theme, theme.fill.accent.primary),
|
|
35
|
+
text: navColor(theme, theme.content.primary)
|
|
36
|
+
},
|
|
37
|
+
dark: theme.dark,
|
|
38
|
+
fonts: {
|
|
39
|
+
bold: navFont(theme, 'bold', DefaultTheme.fonts.bold),
|
|
40
|
+
heavy: navFont(theme, 'bold', DefaultTheme.fonts.heavy),
|
|
41
|
+
medium: navFont(theme, 'medium', DefaultTheme.fonts.medium),
|
|
42
|
+
regular: navFont(theme, 'regular', DefaultTheme.fonts.regular)
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=navigation-theme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["fontFace","resolvePaletteColor","DefaultTheme","navColor","theme","value","navFont","weight","fallback","fontFamily","fonts","fontWeight","navigationTheme","colors","background","border","secondary","base","card","surface","primary","notification","fill","accent","text","content","dark","bold","heavy","medium","regular"],"sourceRoot":"../../src","sources":["navigation-theme.ts"],"mappings":";;AAAA,SAAyBA,QAAQ,EAAqBC,mBAAmB,QAAmB,mBAAmB;AAC/G,SAAQC,YAAY,QAAsC,aAAa;AAIvE;AACA;AACA;AACA;AACA;AACA,MAAMC,QAAQ,GAAGA,CAACC,KAAY,EAAEC,KAAmB,KAAaJ,mBAAmB,CAACG,KAAK,EAAEC,KAAK,CAAW;;AAE3G;AACA;AACA;AACA;AACA,MAAMC,OAAO,GAAGA,CAACF,KAAY,EAAEG,MAAkB,EAAEC,QAAwB,KAAqB;EAC9F,MAAMC,UAAU,GAAGT,QAAQ,CAACI,KAAK,CAACM,KAAK,EAAEH,MAAM,CAAC;EAEhD,OAAOE,UAAU,GAAG;IAACA,UAAU;IAAEE,UAAU,EAAEH,QAAQ,CAACG;EAAU,CAAC,GAAGH,QAAQ;AAC9E,CAAC;;AAED;AACA;AACA;AACA;AACA,OAAO,MAAMI,eAAe,GAAIR,KAAY,KAAuB;EACjES,MAAM,EAAE;IACNC,UAAU,EAAE,aAAa;IACzBC,MAAM,EAAEZ,QAAQ,CAACC,KAAK,EAAEA,KAAK,CAACW,MAAM,CAACC,SAAS,CAACC,IAAI,CAAC;IACpDC,IAAI,EAAEf,QAAQ,CAACC,KAAK,EAAEA,KAAK,CAACe,OAAO,CAACC,OAAO,CAAC;IAC5CC,YAAY,EAAElB,QAAQ,CAACC,KAAK,EAAEA,KAAK,CAACkB,IAAI,CAACC,MAAM,CAACH,OAAO,CAAC;IACxDA,OAAO,EAAEjB,QAAQ,CAACC,KAAK,EAAEA,KAAK,CAACkB,IAAI,CAACC,MAAM,CAACH,OAAO,CAAC;IACnDI,IAAI,EAAErB,QAAQ,CAACC,KAAK,EAAEA,KAAK,CAACqB,OAAO,CAACL,OAAO;EAC7C,CAAC;EACDM,IAAI,EAAEtB,KAAK,CAACsB,IAAI;EAChBhB,KAAK,EAAE;IACLiB,IAAI,EAAErB,OAAO,CAACF,KAAK,EAAE,MAAM,EAAEF,YAAY,CAACQ,KAAK,CAACiB,IAAI,CAAC;IACrDC,KAAK,EAAEtB,OAAO,CAACF,KAAK,EAAE,MAAM,EAAEF,YAAY,CAACQ,KAAK,CAACkB,KAAK,CAAC;IACvDC,MAAM,EAAEvB,OAAO,CAACF,KAAK,EAAE,QAAQ,EAAEF,YAAY,CAACQ,KAAK,CAACmB,MAAM,CAAC;IAC3DC,OAAO,EAAExB,OAAO,CAACF,KAAK,EAAE,SAAS,EAAEF,YAAY,CAACQ,KAAK,CAACoB,OAAO;EAC/D;AACF,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["tabs/index.ts"],"mappings":";;AAAA,cAAc,WAAQ;AACtB,cAAc,iBAAc","ignoreList":[]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { iconResourceName } from '@hzblj/kara-native';
|
|
4
|
+
import { NativeTabs } from 'expo-router/unstable-native-tabs';
|
|
5
|
+
import { Children, isValidElement } from 'react';
|
|
6
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
|
+
const {
|
|
8
|
+
Icon,
|
|
9
|
+
Label
|
|
10
|
+
} = NativeTabs.Trigger;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Declares one tab and renders nothing itself — `Tabs` reads these props and emits the trigger.
|
|
14
|
+
* The navigator collects its tabs by matching child element type exactly, so a component that
|
|
15
|
+
* merely *renders* a trigger is invisible to it and the tab bar comes up empty.
|
|
16
|
+
*/
|
|
17
|
+
const TabsScreen = () => null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The tab's icon, natively on both sides: the app icon's image set in the iOS asset catalog, its vector drawable on
|
|
21
|
+
* Android — one name, the one the config plugin gave both. No icon font, no bitmap rendered in JavaScript.
|
|
22
|
+
*/
|
|
23
|
+
const renderTrigger = ({
|
|
24
|
+
hidden,
|
|
25
|
+
icon,
|
|
26
|
+
name,
|
|
27
|
+
selectedIcon,
|
|
28
|
+
testID,
|
|
29
|
+
title
|
|
30
|
+
}) => {
|
|
31
|
+
const asset = iconResourceName(icon);
|
|
32
|
+
const selected = selectedIcon ? iconResourceName(selectedIcon) : undefined;
|
|
33
|
+
return /*#__PURE__*/_jsxs(NativeTabs.Trigger, {
|
|
34
|
+
hidden: hidden,
|
|
35
|
+
name: name,
|
|
36
|
+
testID: testID,
|
|
37
|
+
children: [/*#__PURE__*/_jsx(Icon, {
|
|
38
|
+
drawable: selected ? {
|
|
39
|
+
default: asset,
|
|
40
|
+
selected
|
|
41
|
+
} : asset,
|
|
42
|
+
xcasset: selected ? {
|
|
43
|
+
default: asset,
|
|
44
|
+
selected
|
|
45
|
+
} : asset
|
|
46
|
+
}), /*#__PURE__*/_jsx(Label, {
|
|
47
|
+
children: title
|
|
48
|
+
})]
|
|
49
|
+
}, name);
|
|
50
|
+
};
|
|
51
|
+
const TabsRoot = ({
|
|
52
|
+
children,
|
|
53
|
+
minimizeBehavior = 'never'
|
|
54
|
+
}) => /*#__PURE__*/_jsx(NativeTabs, {
|
|
55
|
+
minimizeBehavior: minimizeBehavior,
|
|
56
|
+
children: Children.map(children, child => /*#__PURE__*/isValidElement(child) && child.type === TabsScreen ? renderTrigger(child.props) : child)
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Native bottom tabs — a SwiftUI `TabView` on iOS and a Material navigation bar on Android, both
|
|
61
|
+
* driven by Expo Router's own navigator. Nothing here may come from the standalone
|
|
62
|
+
* `@react-navigation/*` packages: Expo Router 57 vendors its own fork of them, so a navigator built
|
|
63
|
+
* against the npm copies registers into a context tree that never exists and throws
|
|
64
|
+
* "Couldn't register the navigator" the moment this layout mounts.
|
|
65
|
+
*/
|
|
66
|
+
export const Tabs = Object.assign(TabsRoot, {
|
|
67
|
+
Screen: TabsScreen
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=tabs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["iconResourceName","NativeTabs","Children","isValidElement","jsx","_jsx","jsxs","_jsxs","Icon","Label","Trigger","TabsScreen","renderTrigger","hidden","icon","name","selectedIcon","testID","title","asset","selected","undefined","children","drawable","default","xcasset","TabsRoot","minimizeBehavior","map","child","type","props","Tabs","Object","assign","Screen"],"sourceRoot":"../../../src","sources":["tabs/tabs.tsx"],"mappings":";;AAAA,SAAQA,gBAAgB,QAAO,oBAAoB;AACnD,SAAQC,UAAU,QAAO,kCAAkC;AAC3D,SAAQC,QAAQ,EAAEC,cAAc,QAAO,OAAO;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAI9C,MAAM;EAACC,IAAI;EAAEC;AAAK,CAAC,GAAGR,UAAU,CAACS,OAAO;;AAExC;AACA;AACA;AACA;AACA;AACA,MAAMC,UAAmC,GAAGA,CAAA,KAAM,IAAI;;AAEtD;AACA;AACA;AACA;AACA,MAAMC,aAAa,GAAGA,CAAC;EAACC,MAAM;EAAEC,IAAI;EAAEC,IAAI;EAAEC,YAAY;EAAEC,MAAM;EAAEC;AAAsB,CAAC,KAAK;EAC5F,MAAMC,KAAK,GAAGnB,gBAAgB,CAACc,IAAI,CAAC;EACpC,MAAMM,QAAQ,GAAGJ,YAAY,GAAGhB,gBAAgB,CAACgB,YAAY,CAAC,GAAGK,SAAS;EAE1E,oBACEd,KAAA,CAACN,UAAU,CAACS,OAAO;IAACG,MAAM,EAAEA,MAAO;IAAYE,IAAI,EAAEA,IAAK;IAACE,MAAM,EAAEA,MAAO;IAAAK,QAAA,gBACxEjB,IAAA,CAACG,IAAI;MACHe,QAAQ,EAAEH,QAAQ,GAAG;QAACI,OAAO,EAAEL,KAAK;QAAEC;MAAQ,CAAC,GAAGD,KAAM;MACxDM,OAAO,EAAEL,QAAQ,GAAG;QAACI,OAAO,EAAEL,KAAK;QAAEC;MAAQ,CAAC,GAAGD;IAAM,CACxD,CAAC,eACFd,IAAA,CAACI,KAAK;MAAAa,QAAA,EAAEJ;IAAK,CAAQ,CAAC;EAAA,GALiBH,IAMrB,CAAC;AAEzB,CAAC;AAED,MAAMW,QAAQ,GAAGA,CAAC;EAACJ,QAAQ;EAAEK,gBAAgB,GAAG;AAAkB,CAAC,kBACjEtB,IAAA,CAACJ,UAAU;EAAC0B,gBAAgB,EAAEA,gBAAiB;EAAAL,QAAA,EAC5CpB,QAAQ,CAAC0B,GAAG,CAACN,QAAQ,EAAEO,KAAK,IAC3B,aAAA1B,cAAc,CAAkB0B,KAAK,CAAC,IAAIA,KAAK,CAACC,IAAI,KAAKnB,UAAU,GAAGC,aAAa,CAACiB,KAAK,CAACE,KAAK,CAAC,GAAGF,KACrG;AAAC,CACS,CACb;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMG,IAAmB,GAAGC,MAAM,CAACC,MAAM,CAACR,QAAQ,EAAE;EAACS,MAAM,EAAExB;AAAU,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["tabs/tabs.types.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Theme } from '@hzblj/kara-theme';
|
|
2
|
+
import { type Theme as NavigationTheme } from 'expo-router';
|
|
3
|
+
/**
|
|
4
|
+
* Maps the active theme onto React Navigation's, for expo-router's `ThemeProvider`. Call it inside
|
|
5
|
+
* `KaraProvider` with `useAppTheme()`, so the system half of the palette is resolved for the same variant.
|
|
6
|
+
*/
|
|
7
|
+
export declare const navigationTheme: (theme: Theme) => NavigationTheme;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TabsComponent } from './tabs.types';
|
|
2
|
+
/**
|
|
3
|
+
* Native bottom tabs — a SwiftUI `TabView` on iOS and a Material navigation bar on Android, both
|
|
4
|
+
* driven by Expo Router's own navigator. Nothing here may come from the standalone
|
|
5
|
+
* `@react-navigation/*` packages: Expo Router 57 vendors its own fork of them, so a navigator built
|
|
6
|
+
* against the npm copies registers into a context tree that never exists and throws
|
|
7
|
+
* "Couldn't register the navigator" the moment this layout mounts.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Tabs: TabsComponent;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IconName } from '@hzblj/kara-components';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
export type TabsScreenProps = {
|
|
4
|
+
/**
|
|
5
|
+
* Drops the tab from the bar *and* makes the route unreachable by navigation. Prefer this over
|
|
6
|
+
* conditionally rendering the screen: every child route needs a declared tab, and toggling this
|
|
7
|
+
* at runtime remounts the navigator, so pass a build constant.
|
|
8
|
+
*/
|
|
9
|
+
hidden?: boolean;
|
|
10
|
+
/** One of the app's icons — the asset-catalog image on iOS, the vector drawable on Android. */
|
|
11
|
+
icon: IconName;
|
|
12
|
+
name: string;
|
|
13
|
+
/** Swapped in while the tab is active — the filled counterpart of `icon`, say. */
|
|
14
|
+
selectedIcon?: IconName;
|
|
15
|
+
testID?: string;
|
|
16
|
+
title: string;
|
|
17
|
+
};
|
|
18
|
+
/** Maps to `UITabBarController.minimizeBehavior`. iOS 26+; ignored everywhere else. */
|
|
19
|
+
export type TabsMinimizeBehavior = 'automatic' | 'never' | 'onScrollDown' | 'onScrollUp';
|
|
20
|
+
export type TabsProps = {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the iOS 26 tab bar shrinks to a pill while scrolling.
|
|
24
|
+
*
|
|
25
|
+
* Off by default, unlike the system, which minimizes on scroll. Minimizing changes the tab bar's
|
|
26
|
+
* height mid-gesture, and a native tabs screen re-insets the first scroll view inside it to match —
|
|
27
|
+
* so the content shifts under the finger every time the scroll direction changes, and every list in
|
|
28
|
+
* the app inherits that. Opt in per screen once the content is built to absorb it.
|
|
29
|
+
*/
|
|
30
|
+
minimizeBehavior?: TabsMinimizeBehavior;
|
|
31
|
+
};
|
|
32
|
+
export type TabsComponent = ((props: TabsProps) => ReactNode) & {
|
|
33
|
+
Screen: (props: TabsScreenProps) => ReactNode;
|
|
34
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzblj/kara-router",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Expo Router pieces of @hzblj/kara-ui: native tabs and a navigation theme mapped from the active kara theme.",
|
|
5
|
+
"author": "Jan Blazej",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hzblj/kara-ui.git",
|
|
10
|
+
"directory": "packages/router"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/hzblj/kara-ui/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/hzblj/kara-ui/tree/main/packages/router#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"expo",
|
|
19
|
+
"kara-ui"
|
|
20
|
+
],
|
|
21
|
+
"main": "./lib/module/index.js",
|
|
22
|
+
"types": "./lib/typescript/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"source": "./src/index.ts",
|
|
26
|
+
"types": "./lib/typescript/index.d.ts",
|
|
27
|
+
"default": "./lib/module/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"lib",
|
|
34
|
+
"!**/*.stories.tsx",
|
|
35
|
+
"!**/contract.check.ts",
|
|
36
|
+
"!**/story-decorators.tsx",
|
|
37
|
+
"!**/__tests__"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "bob build && tsc -p ./tsconfig.build.json && node ../../scripts/fallback-declarations.mjs",
|
|
41
|
+
"clean": "rm -rf lib",
|
|
42
|
+
"prepack": "yarn run -T turbo run build --filter=$npm_package_name...",
|
|
43
|
+
"lint": "yarn run -T concurrently \"yarn biome:check\" \"yarn tsc:check\"",
|
|
44
|
+
"lint:fix": "yarn run -T biome check --write .",
|
|
45
|
+
"biome:check": "yarn run -T biome check .",
|
|
46
|
+
"tsc:check": "yarn run -T concurrently \"tsc -p ./tsconfig.ios.json\" \"tsc -p ./tsconfig.android.json\""
|
|
47
|
+
},
|
|
48
|
+
"react-native-builder-bob": {
|
|
49
|
+
"source": "src",
|
|
50
|
+
"output": "lib",
|
|
51
|
+
"exclude": "{**/__tests__/**,**/__fixtures__/**,**/__mocks__/**,**/*.stories.tsx,**/contract.check.ts,**/story-decorators.tsx}",
|
|
52
|
+
"targets": [
|
|
53
|
+
[
|
|
54
|
+
"module",
|
|
55
|
+
{
|
|
56
|
+
"esm": true
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@hzblj/kara-components": "workspace:^",
|
|
63
|
+
"@hzblj/kara-native": "workspace:^",
|
|
64
|
+
"@hzblj/kara-theme": "workspace:^"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"expo-router": "*",
|
|
68
|
+
"react": "*",
|
|
69
|
+
"react-native": "*"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@expo/ui": "~57.0.18",
|
|
73
|
+
"@storybook/react-native": "^10.6.0",
|
|
74
|
+
"@types/react": "~19.2.18",
|
|
75
|
+
"expo": "~57.0.22",
|
|
76
|
+
"expo-router": "~57.0.21",
|
|
77
|
+
"react": "19.2.3",
|
|
78
|
+
"react-native": "0.86.3",
|
|
79
|
+
"react-native-builder-bob": "^0.43.0",
|
|
80
|
+
"storybook": "^10.6.0",
|
|
81
|
+
"typescript": "~7.0.2"
|
|
82
|
+
},
|
|
83
|
+
"publishConfig": {
|
|
84
|
+
"access": "public"
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {type FontWeight, fontFace, type PaletteColor, resolvePaletteColor, type Theme} from '@hzblj/kara-theme'
|
|
2
|
+
import {DefaultTheme, type Theme as NavigationTheme} from 'expo-router'
|
|
3
|
+
|
|
4
|
+
type NavigationFont = NavigationTheme['fonts']['regular']
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* React Navigation types every colour as `string`, but the values only ever end up in a React Native
|
|
8
|
+
* style — which accepts any `ColorValue`, including the opaque `PlatformColor` a system token resolves to
|
|
9
|
+
* on iOS. Narrowing here rather than at each call site keeps the assertion to one place.
|
|
10
|
+
*/
|
|
11
|
+
const navColor = (theme: Theme, value: PaletteColor): string => resolvePaletteColor(theme, value) as string
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The app's own face for that weight where the theme names one, and React Navigation's platform default otherwise —
|
|
15
|
+
* which is the platform's face too, so headers and tab labels follow the same typeface as every other piece of text.
|
|
16
|
+
*/
|
|
17
|
+
const navFont = (theme: Theme, weight: FontWeight, fallback: NavigationFont): NavigationFont => {
|
|
18
|
+
const fontFamily = fontFace(theme.fonts, weight)
|
|
19
|
+
|
|
20
|
+
return fontFamily ? {fontFamily, fontWeight: fallback.fontWeight} : fallback
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maps the active theme onto React Navigation's, for expo-router's `ThemeProvider`. Call it inside
|
|
25
|
+
* `KaraProvider` with `useAppTheme()`, so the system half of the palette is resolved for the same variant.
|
|
26
|
+
*/
|
|
27
|
+
export const navigationTheme = (theme: Theme): NavigationTheme => ({
|
|
28
|
+
colors: {
|
|
29
|
+
background: 'transparent',
|
|
30
|
+
border: navColor(theme, theme.border.secondary.base),
|
|
31
|
+
card: navColor(theme, theme.surface.primary),
|
|
32
|
+
notification: navColor(theme, theme.fill.accent.primary),
|
|
33
|
+
primary: navColor(theme, theme.fill.accent.primary),
|
|
34
|
+
text: navColor(theme, theme.content.primary),
|
|
35
|
+
},
|
|
36
|
+
dark: theme.dark,
|
|
37
|
+
fonts: {
|
|
38
|
+
bold: navFont(theme, 'bold', DefaultTheme.fonts.bold),
|
|
39
|
+
heavy: navFont(theme, 'bold', DefaultTheme.fonts.heavy),
|
|
40
|
+
medium: navFont(theme, 'medium', DefaultTheme.fonts.medium),
|
|
41
|
+
regular: navFont(theme, 'regular', DefaultTheme.fonts.regular),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {iconResourceName} from '@hzblj/kara-native'
|
|
2
|
+
import {NativeTabs} from 'expo-router/unstable-native-tabs'
|
|
3
|
+
import {Children, isValidElement} from 'react'
|
|
4
|
+
|
|
5
|
+
import type {TabsComponent, TabsProps, TabsScreenProps} from './tabs.types'
|
|
6
|
+
|
|
7
|
+
const {Icon, Label} = NativeTabs.Trigger
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Declares one tab and renders nothing itself — `Tabs` reads these props and emits the trigger.
|
|
11
|
+
* The navigator collects its tabs by matching child element type exactly, so a component that
|
|
12
|
+
* merely *renders* a trigger is invisible to it and the tab bar comes up empty.
|
|
13
|
+
*/
|
|
14
|
+
const TabsScreen: TabsComponent['Screen'] = () => null
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The tab's icon, natively on both sides: the app icon's image set in the iOS asset catalog, its vector drawable on
|
|
18
|
+
* Android — one name, the one the config plugin gave both. No icon font, no bitmap rendered in JavaScript.
|
|
19
|
+
*/
|
|
20
|
+
const renderTrigger = ({hidden, icon, name, selectedIcon, testID, title}: TabsScreenProps) => {
|
|
21
|
+
const asset = iconResourceName(icon)
|
|
22
|
+
const selected = selectedIcon ? iconResourceName(selectedIcon) : undefined
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<NativeTabs.Trigger hidden={hidden} key={name} name={name} testID={testID}>
|
|
26
|
+
<Icon
|
|
27
|
+
drawable={selected ? {default: asset, selected} : asset}
|
|
28
|
+
xcasset={selected ? {default: asset, selected} : asset}
|
|
29
|
+
/>
|
|
30
|
+
<Label>{title}</Label>
|
|
31
|
+
</NativeTabs.Trigger>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const TabsRoot = ({children, minimizeBehavior = 'never'}: TabsProps) => (
|
|
36
|
+
<NativeTabs minimizeBehavior={minimizeBehavior}>
|
|
37
|
+
{Children.map(children, child =>
|
|
38
|
+
isValidElement<TabsScreenProps>(child) && child.type === TabsScreen ? renderTrigger(child.props) : child
|
|
39
|
+
)}
|
|
40
|
+
</NativeTabs>
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Native bottom tabs — a SwiftUI `TabView` on iOS and a Material navigation bar on Android, both
|
|
45
|
+
* driven by Expo Router's own navigator. Nothing here may come from the standalone
|
|
46
|
+
* `@react-navigation/*` packages: Expo Router 57 vendors its own fork of them, so a navigator built
|
|
47
|
+
* against the npm copies registers into a context tree that never exists and throws
|
|
48
|
+
* "Couldn't register the navigator" the moment this layout mounts.
|
|
49
|
+
*/
|
|
50
|
+
export const Tabs: TabsComponent = Object.assign(TabsRoot, {Screen: TabsScreen})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type {IconName} from '@hzblj/kara-components'
|
|
2
|
+
import type {ReactNode} from 'react'
|
|
3
|
+
|
|
4
|
+
export type TabsScreenProps = {
|
|
5
|
+
/**
|
|
6
|
+
* Drops the tab from the bar *and* makes the route unreachable by navigation. Prefer this over
|
|
7
|
+
* conditionally rendering the screen: every child route needs a declared tab, and toggling this
|
|
8
|
+
* at runtime remounts the navigator, so pass a build constant.
|
|
9
|
+
*/
|
|
10
|
+
hidden?: boolean
|
|
11
|
+
/** One of the app's icons — the asset-catalog image on iOS, the vector drawable on Android. */
|
|
12
|
+
icon: IconName
|
|
13
|
+
name: string
|
|
14
|
+
/** Swapped in while the tab is active — the filled counterpart of `icon`, say. */
|
|
15
|
+
selectedIcon?: IconName
|
|
16
|
+
testID?: string
|
|
17
|
+
title: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Maps to `UITabBarController.minimizeBehavior`. iOS 26+; ignored everywhere else. */
|
|
21
|
+
export type TabsMinimizeBehavior = 'automatic' | 'never' | 'onScrollDown' | 'onScrollUp'
|
|
22
|
+
|
|
23
|
+
export type TabsProps = {
|
|
24
|
+
children: ReactNode
|
|
25
|
+
/**
|
|
26
|
+
* Whether the iOS 26 tab bar shrinks to a pill while scrolling.
|
|
27
|
+
*
|
|
28
|
+
* Off by default, unlike the system, which minimizes on scroll. Minimizing changes the tab bar's
|
|
29
|
+
* height mid-gesture, and a native tabs screen re-insets the first scroll view inside it to match —
|
|
30
|
+
* so the content shifts under the finger every time the scroll direction changes, and every list in
|
|
31
|
+
* the app inherits that. Opt in per screen once the content is built to absorb it.
|
|
32
|
+
*/
|
|
33
|
+
minimizeBehavior?: TabsMinimizeBehavior
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type TabsComponent = ((props: TabsProps) => ReactNode) & {
|
|
37
|
+
Screen: (props: TabsScreenProps) => ReactNode
|
|
38
|
+
}
|