@tamer4lynx/tamer-app-shell 0.0.1

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 ADDED
@@ -0,0 +1,67 @@
1
+ # tamer-app-shell
2
+
3
+ App chrome: AppBar, TabBar, and Content layout for Lynx.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @tamer4lynx/tamer-app-shell
9
+ ```
10
+
11
+ Add to your app's dependencies and run `t4l link`. Depends on **tamer-icons**, **tamer-insets**, **tamer-router**, **tamer-screen**.
12
+
13
+ ## Usage
14
+
15
+ ```tsx
16
+ import {
17
+ AppShellProvider,
18
+ AppBar,
19
+ TabBar,
20
+ Content,
21
+ Screen,
22
+ SafeArea,
23
+ useAppShellContext,
24
+ useAppShellRouter,
25
+ } from '@tamer4lynx/tamer-app-shell'
26
+
27
+ // Wrap app with provider
28
+ <AppShellProvider showAppBar showTabBar barHeight={56}>
29
+ <Screen>
30
+ <SafeArea edges={['top', 'bottom']}>
31
+ <AppBar
32
+ title="My App"
33
+ leftAction={{ icon: 'arrow_back', onTap: () => router.back() }}
34
+ rightActions={[{ icon: 'settings', onTap: openSettings }]}
35
+ />
36
+ <Content>
37
+ {children}
38
+ </Content>
39
+ <TabBar tabs={[{ path: '/', icon: 'home', label: 'Home' }, { path: '/about', icon: 'info', label: 'About' }]} />
40
+ </SafeArea>
41
+ </Screen>
42
+ </AppShellProvider>
43
+ ```
44
+
45
+ Typically used via **tamer-router** `Stack` and `Tabs` layouts, which compose AppShellProvider, AppBar, TabBar, and Content internally.
46
+
47
+ ## API
48
+
49
+ | Component | Props | Description |
50
+ |-----------|-------|-------------|
51
+ | `AppShellProvider` | `showAppBar?`, `showTabBar?`, `barHeight?` | Context for app chrome visibility |
52
+ | `AppBar` | `title?`, `barHeight?`, `leftAction?`, `rightActions?`, `foregroundColor?`, `actionColor?` | Top app bar |
53
+ | `TabBar` | `tabs`, `style?`, `iconColor?` | Bottom tab bar |
54
+ | `Content` | ViewProps | Main content area |
55
+ | `Screen` | ViewProps | Full-screen container (re-export from tamer-screen) |
56
+ | `SafeArea` | ViewProps, `edges?` | Safe area wrapper (re-export from tamer-screen) |
57
+
58
+ | Hook | Returns | Description |
59
+ |------|---------|-------------|
60
+ | `useAppShellContext()` | `{ showAppBar, showTabBar, barHeight } \| null` | App shell visibility |
61
+ | `useAppShellRouter()` | `{ back, canGoBack, replace } \| null` | Router integration for back/replace |
62
+
63
+ **AppBarAction:** `{ icon, set?, onTap }`
64
+
65
+ ## Platform
66
+
67
+ Uses **lynx.ext.json**. Run `t4l link` after adding to your app.
@@ -0,0 +1,55 @@
1
+ .AppShellActionButton {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ position: relative;
6
+ overflow: hidden;
7
+ border-radius: 50%;
8
+ transition: transform 0.12s ease-out, opacity 0.12s ease-out;
9
+ }
10
+
11
+ .AppShellActionButton--pressed {
12
+ transform: scale(0.92);
13
+ opacity: 0.85;
14
+ }
15
+
16
+ .AppShellActionButton-ripple {
17
+ position: absolute;
18
+ left: 0;
19
+ top: 0;
20
+ width: 100%;
21
+ height: 100%;
22
+ border-radius: 50%;
23
+ background: rgba(255, 255, 255, 0.6);
24
+ transform: scale(0);
25
+ opacity: 0;
26
+ transform-origin: center;
27
+ overflow: hidden;
28
+ }
29
+
30
+ .AppShellActionButton--pressed .AppShellActionButton-ripple {
31
+ animation: AppShellRipple 0.3s ease-out forwards;
32
+ }
33
+
34
+ @keyframes AppShellRipple {
35
+ 0% {
36
+ transform: scale(0);
37
+ opacity: 0.8;
38
+ }
39
+ 100% {
40
+ transform: scale(2.5);
41
+ opacity: 0;
42
+ }
43
+ }
44
+
45
+ .AppShellTabItem {
46
+ display: flex;
47
+ flex-direction: column;
48
+ align-items: center;
49
+ transition: transform 0.1s ease-out, opacity 0.1s ease-out;
50
+ }
51
+
52
+ .AppShellTabItem--pressed {
53
+ transform: scale(0.94);
54
+ opacity: 0.9;
55
+ }
@@ -0,0 +1,65 @@
1
+ import './app-shell.css';
2
+ import { type IconSet } from '@tamer4lynx/tamer-icons';
3
+ import type { ReactNode } from '@lynx-js/react';
4
+ import type { ViewProps } from '@lynx-js/types';
5
+ export { Screen, SafeArea, useSafeAreaContext } from '@tamer4lynx/tamer-screen';
6
+ export interface AppShellRouterContextValue {
7
+ back: () => void;
8
+ canGoBack: () => boolean;
9
+ replace: (route: string, options?: {
10
+ mode?: string;
11
+ direction?: string;
12
+ tab?: boolean;
13
+ }) => void;
14
+ }
15
+ export declare const AppShellRouterContext: import("react").Context<AppShellRouterContextValue | null>;
16
+ export declare function useAppShellRouter(): AppShellRouterContextValue | null;
17
+ export declare const px: (value: number) => string;
18
+ export interface AppShellContextValue {
19
+ showAppBar: boolean;
20
+ showTabBar: boolean;
21
+ barHeight: number;
22
+ }
23
+ export declare const AppShellContext: import("react").Context<AppShellContextValue | null>;
24
+ export declare function useAppShellContext(): AppShellContextValue | null;
25
+ export interface AppBarAction {
26
+ icon: string;
27
+ set?: IconSet;
28
+ onTap: () => void;
29
+ }
30
+ export interface AppBarProps extends ViewProps {
31
+ title?: string;
32
+ barHeight?: number;
33
+ leftAction?: AppBarAction | false;
34
+ rightActions?: AppBarAction[];
35
+ foregroundColor?: string;
36
+ actionColor?: string;
37
+ }
38
+ export declare function AppBar({ title, barHeight, leftAction, rightActions, foregroundColor, actionColor, style, children, ...rest }: AppBarProps): import("@lynx-js/react").JSX.Element;
39
+ export interface TabItem {
40
+ icon: string;
41
+ set?: IconSet;
42
+ label?: string;
43
+ path?: string;
44
+ onTap?: () => void;
45
+ }
46
+ export interface TabBarIconColor {
47
+ active?: string;
48
+ inactive?: string;
49
+ }
50
+ export interface TabBarProps extends ViewProps {
51
+ tabs: TabItem[];
52
+ iconColor?: TabBarIconColor;
53
+ }
54
+ export declare function TabBar({ tabs, iconColor, style, ...rest }: TabBarProps): import("@lynx-js/react").JSX.Element;
55
+ export interface ContentProps extends ViewProps {
56
+ }
57
+ export declare function Content({ children, style, ...rest }: ContentProps): import("@lynx-js/react").JSX.Element;
58
+ export interface AppShellProviderProps {
59
+ children: ReactNode;
60
+ showAppBar?: boolean;
61
+ showTabBar?: boolean;
62
+ barHeight?: number;
63
+ }
64
+ export declare function AppShellProvider({ children, showAppBar, showTabBar, barHeight, }: AppShellProviderProps): import("@lynx-js/react").JSX.Element;
65
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,iBAAiB,CAAA;AAKxB,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAG/C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAE/E,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,SAAS,EAAE,MAAM,OAAO,CAAA;IACxB,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;CACjG;AAED,eAAO,MAAM,qBAAqB,4DAAyD,CAAA;AAE3F,wBAAgB,iBAAiB,IAAI,0BAA0B,GAAG,IAAI,CAErE;AAGD,eAAO,MAAM,EAAE,GAAI,OAAO,MAAM,WAA6B,CAAA;AAE7D,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,eAAe,sDAAmD,CAAA;AAE/E,wBAAgB,kBAAkB,gCAEjC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,YAAY,GAAG,KAAK,CAAA;IACjC,YAAY,CAAC,EAAE,YAAY,EAAE,CAAA;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AA2BD,wBAAgB,MAAM,CAAC,EACrB,KAAK,EACL,SAA8B,EAC9B,UAAU,EACV,YAAiB,EACjB,eAAwB,EACxB,WAAW,EACX,KAAK,EACL,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,WAAW,wCAsDb;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,IAAI,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,SAAS,CAAC,EAAE,eAAe,CAAA;CAC5B;AAkCD,wBAAgB,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE,WAAW,wCA8DtE;AAED,MAAM,WAAW,YAAa,SAAQ,SAAS;CAAG;AAElD,wBAAgB,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE,YAAY,wCAkBjE;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,SAAS,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,UAAiB,EACjB,UAAkB,EAClB,SAA8B,GAC/B,EAAE,qBAAqB,wCAGvB"}
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@lynx-js/react/jsx-runtime";
2
+ /// <reference types="@lynx-js/react" />
3
+ import './app-shell.css';
4
+ import { createContext, useCallback, useContext, useState } from '@lynx-js/react';
5
+ import { useLocation } from 'react-router';
6
+ import { useInsets, useKeyboard } from '@tamer4lynx/tamer-insets';
7
+ import { useSafeAreaContext } from '@tamer4lynx/tamer-screen';
8
+ import { Icon } from '@tamer4lynx/tamer-icons';
9
+ export { Screen, SafeArea, useSafeAreaContext } from '@tamer4lynx/tamer-screen';
10
+ export const AppShellRouterContext = createContext(null);
11
+ export function useAppShellRouter() {
12
+ return useContext(AppShellRouterContext);
13
+ }
14
+ const DEFAULT_BAR_HEIGHT = 56;
15
+ export const px = (value) => `${Math.round(value)}px`;
16
+ export const AppShellContext = createContext(null);
17
+ export function useAppShellContext() {
18
+ return useContext(AppShellContext);
19
+ }
20
+ const ACTION_SIZE = 48;
21
+ const ACTION_ICON_SIZE = 32;
22
+ function ActionButton({ action, color = '#fff' }) {
23
+ const [pressed, setPressed] = useState(false);
24
+ return (_jsxs("view", { className: `AppShellActionButton${pressed ? ' AppShellActionButton--pressed' : ''}`, style: {
25
+ width: px(ACTION_SIZE),
26
+ height: px(ACTION_SIZE),
27
+ borderRadius: px(ACTION_SIZE / 2),
28
+ overflow: 'hidden',
29
+ }, bindtap: action.onTap, bindtouchstart: () => setPressed(true), bindtouchend: () => setPressed(false), bindtouchcancel: () => setPressed(false), children: [_jsx(Icon, { name: action.icon, set: action.set ?? 'material', size: ACTION_ICON_SIZE, color: color }), _jsx("view", { className: "AppShellActionButton-ripple" })] }));
30
+ }
31
+ export function AppBar({ title, barHeight = DEFAULT_BAR_HEIGHT, leftAction, rightActions = [], foregroundColor = '#fff', actionColor, style, children, ...rest }) {
32
+ const insets = useInsets();
33
+ const safeArea = useSafeAreaContext();
34
+ const isSafeAreaChild = safeArea?.hasTop ?? false;
35
+ const router = useAppShellRouter();
36
+ const back = router?.back ?? (() => { });
37
+ const canGoBack = router?.canGoBack ?? (() => false);
38
+ const resolvedTitleColor = foregroundColor;
39
+ const resolvedActionColor = actionColor ?? foregroundColor;
40
+ const showDefaultBack = leftAction === undefined && canGoBack();
41
+ const left = leftAction === false ? null : leftAction ? (_jsx(ActionButton, { action: leftAction, color: resolvedActionColor })) : showDefaultBack ? (_jsx(ActionButton, { action: { icon: 'arrow_back', onTap: back }, color: resolvedActionColor })) : null;
42
+ const right = rightActions.length > 0 ? (_jsx("view", { style: { display: 'flex', flexDirection: 'row', alignItems: 'center' }, children: rightActions.map((action, i) => (_jsx(ActionButton, { action: action, color: resolvedActionColor }, i))) })) : null;
43
+ const effectiveBarHeight = isSafeAreaChild ? DEFAULT_BAR_HEIGHT + insets.top : barHeight;
44
+ return (_jsxs("view", { style: {
45
+ height: px(effectiveBarHeight),
46
+ ...(isSafeAreaChild ? { marginTop: `-${Math.round(insets.top)}px`, paddingTop: px(insets.top) } : {}),
47
+ paddingLeft: '16px',
48
+ paddingRight: '16px',
49
+ display: 'flex',
50
+ flexDirection: 'row',
51
+ justifyContent: 'space-between',
52
+ alignItems: 'center',
53
+ flexShrink: 0,
54
+ borderBottomWidth: '1px',
55
+ borderBottomColor: '#e0e0e0',
56
+ ...(style ?? {}),
57
+ }, ...rest, children: [_jsx("view", { style: { display: 'flex', flexDirection: 'row', alignItems: 'center', minWidth: px(ACTION_SIZE) }, children: left }), children ?? (title ? _jsx("text", { style: { display: 'block', width: '100%', fontWeight: 'bold', fontSize: px(18), textAlign: 'center', color: resolvedTitleColor }, children: title }) : _jsx("view", { style: { flex: 1 } })), _jsx("view", { style: { display: 'flex', flexDirection: 'row', alignItems: 'center', minWidth: px(ACTION_SIZE), justifyContent: 'flex-end' }, children: right })] }));
58
+ }
59
+ const DEFAULT_ICON_COLOR = { active: '#FFF', inactive: '#000' };
60
+ function TabBarItem({ item, isActive, onTap, iconColor = DEFAULT_ICON_COLOR, }) {
61
+ const [pressed, setPressed] = useState(false);
62
+ const iconC = isActive ? (iconColor.active ?? DEFAULT_ICON_COLOR.active) : (iconColor.inactive ?? DEFAULT_ICON_COLOR.inactive);
63
+ return (_jsxs("view", { className: `AppShellTabItem${pressed ? ' AppShellTabItem--pressed' : ''}`, style: { opacity: isActive ? 1 : 0.6 }, bindtap: onTap, bindtouchstart: () => setPressed(true), bindtouchend: () => setPressed(false), bindtouchcancel: () => setPressed(false), children: [_jsx(Icon, { name: item.icon, set: item.set ?? 'material', size: 24, color: iconC }), item.label ? (_jsx("text", { style: { marginTop: px(4), color: iconC }, children: item.label })) : null] }));
64
+ }
65
+ export function TabBar({ tabs, iconColor, style, ...rest }) {
66
+ const insets = useInsets();
67
+ const keyboard = useKeyboard();
68
+ const safeArea = useSafeAreaContext();
69
+ const isSafeAreaChild = safeArea?.hasBottom ?? false;
70
+ const router = useAppShellRouter();
71
+ const replace = router?.replace ?? (() => { });
72
+ const location = useLocation();
73
+ const handleTap = useCallback((item) => {
74
+ 'background only';
75
+ if (!item.path) {
76
+ item.onTap?.();
77
+ return;
78
+ }
79
+ const pathname = location.pathname || '/';
80
+ const isCurrent = (t) => {
81
+ const p = t.path || '/';
82
+ if (p === '/')
83
+ return pathname === '/' || pathname === '';
84
+ return pathname === p || pathname.startsWith(p + '/');
85
+ };
86
+ if (isCurrent(item))
87
+ return;
88
+ replace(item.path, { tab: true });
89
+ }, [replace, tabs, location.pathname]);
90
+ return (_jsx("view", { style: {
91
+ ...(isSafeAreaChild ? { marginBottom: `-${Math.round(insets.bottom)}px` } : {}),
92
+ flexDirection: 'row',
93
+ paddingLeft: px(8),
94
+ paddingRight: px(8),
95
+ alignItems: 'center',
96
+ justifyContent: 'space-around',
97
+ borderTopWidth: '1px',
98
+ borderTopColor: '#e0e0e0',
99
+ ...keyboard.visible ? { position: 'absolute', display: 'block', overflow: 'hidden', maxHeight: '0px', height: '0px', paddingBottom: '0px', paddingTop: '0px', bottom: '-50px' } : { display: 'flex', paddingBottom: px(insets.bottom), paddingTop: px(12) },
100
+ ...(style ?? {}),
101
+ }, ...rest, children: tabs.map((item, i) => {
102
+ const pathname = location.pathname || '/';
103
+ const p = item.path || '/';
104
+ const isActive = item.path
105
+ ? p === '/' ? pathname === '/' || pathname === '' : pathname === p || pathname.startsWith(p + '/')
106
+ : false;
107
+ return (_jsx(TabBarItem, { item: item, isActive: isActive, onTap: () => handleTap(item), iconColor: iconColor }, i));
108
+ }) }));
109
+ }
110
+ export function Content({ children, style, ...rest }) {
111
+ const scrollStyle = {
112
+ display: 'flex',
113
+ flex: '1 1 100%',
114
+ flexGrow: 1,
115
+ flexShrink: 1,
116
+ flexDirection: 'column',
117
+ justifyContent: 'flex-start',
118
+ height: '100%',
119
+ ...(style ?? {}),
120
+ };
121
+ return (_jsx("view", { style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }, children: _jsx("scroll-view", { "scroll-y": true, style: scrollStyle, ...rest, children: children }) }));
122
+ }
123
+ export function AppShellProvider({ children, showAppBar = true, showTabBar = false, barHeight = DEFAULT_BAR_HEIGHT, }) {
124
+ const value = { showAppBar, showTabBar, barHeight };
125
+ return _jsx(AppShellContext.Provider, { value: value, children: children });
126
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@tamer4lynx/tamer-app-shell",
3
+ "publishConfig": { "access": "public", "tag": "prerelease" },
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "description": "App chrome: AppBar, TabBar, and Content layout for Lynx.",
7
+ "main": "dist/src/index.js",
8
+ "types": "dist/src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/src/index.d.ts",
12
+ "import": "./dist/src/index.js",
13
+ "default": "./dist/src/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "peerDependencies": {
20
+ "@lynx-js/react": ">=0.100.0",
21
+ "react": "^17.0.0",
22
+ "react-router": "^6.0.0"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc && cp src/app-shell.css dist/src/app-shell.css"
26
+ },
27
+ "dependencies": {
28
+ "@tamer4lynx/tamer-icons": "^0.0.1",
29
+ "@tamer4lynx/tamer-insets": "^0.0.1",
30
+ "@tamer4lynx/tamer-screen": "^0.0.1"
31
+ },
32
+ "devDependencies": {
33
+ "@lynx-js/react": "^0.112.1",
34
+ "@lynx-js/types": "3.3.0",
35
+ "@types/react": "^17.0.0",
36
+ "react": "^17.0.2",
37
+ "react-router": "^6.28.0",
38
+ "typescript": "~5.8.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/tamer4lynx/tamer-app-shell.git"
46
+ },
47
+ "homepage": "https://github.com/tamer4lynx/tamer-app-shell#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/tamer4lynx/tamer-app-shell/issues"
50
+ },
51
+ "author": "Nanofuxion",
52
+ "license": "MIT"
53
+ }