@ateam-ai-mobile/plugin-runtime 0.1.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/dist/index.d.mts +1129 -0
- package/dist/index.d.ts +1129 -0
- package/dist/index.js +2388 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2369 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +31 -0
- package/src/api/plugins.ts +257 -0
- package/src/bridge/native.ts +266 -0
- package/src/bridge/types.ts +268 -0
- package/src/bundles/bundleManager.ts +396 -0
- package/src/bundles/storage.ts +68 -0
- package/src/hooks/usePlugin.ts +185 -0
- package/src/hooks/useRemotePlugins.ts +97 -0
- package/src/host.ts +117 -0
- package/src/index.ts +79 -0
- package/src/registry/SurfaceDispatcher.tsx +66 -0
- package/src/registry/store.ts +131 -0
- package/src/registry/types.ts +60 -0
- package/src/sdk/components.ts +370 -0
- package/src/sdk/index.ts +76 -0
- package/src/sdk/primitives.ts +22 -0
- package/src/sdk/registration.ts +52 -0
- package/src/sdk/types.ts +315 -0
- package/src/sdk/useApi.ts +78 -0
- package/src/sdk/usePluginTheme.ts +86 -0
- package/src/surfaces/AdaOrb.tsx +318 -0
- package/src/surfaces/SurfaceCard.tsx +137 -0
- package/src/surfaces/SurfaceDrawer.tsx +216 -0
- package/src/surfaces/SurfaceFullscreen.tsx +207 -0
- package/src/utils/sha256.ts +85 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface registry + active-state store.
|
|
3
|
+
*
|
|
4
|
+
* - `manifests`: map of registered surface plugins
|
|
5
|
+
* - `activeDrawer` / `activeFullscreen`: which surface (if any) is currently mounted
|
|
6
|
+
* (only one of each at a time)
|
|
7
|
+
*
|
|
8
|
+
* Plugins are registered by calling `register(...)` from bootstrap or by an
|
|
9
|
+
* "engine" event. The dispatcher subscribes to active state and mounts.
|
|
10
|
+
*/
|
|
11
|
+
import { create } from 'zustand';
|
|
12
|
+
import type {
|
|
13
|
+
SurfaceManifest,
|
|
14
|
+
SurfaceType,
|
|
15
|
+
SurfaceVisibility,
|
|
16
|
+
SurfacePlacement,
|
|
17
|
+
InlineSurfaceManifest,
|
|
18
|
+
MountSurfaceManifest,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
interface SurfaceState {
|
|
22
|
+
manifests: Record<string, SurfaceManifest>;
|
|
23
|
+
activeDrawer: string | null;
|
|
24
|
+
activeFullscreen: string | null;
|
|
25
|
+
/** Props passed by the engine when summoning a surface via sys.focusUiPlugin.
|
|
26
|
+
* Keyed by surface id; cleared on hide. Plugins read these on first mount. */
|
|
27
|
+
activeProps: Record<string, any>;
|
|
28
|
+
/** Increments on every `show()` call. Used as a React `key` so the Surface
|
|
29
|
+
* Component remounts even if the same surface is shown again — guarantees
|
|
30
|
+
* the plugin's data-fetching effects re-run, so the user always sees fresh
|
|
31
|
+
* data when they open a drawer. */
|
|
32
|
+
mountKey: number;
|
|
33
|
+
|
|
34
|
+
register: (manifest: SurfaceManifest) => void;
|
|
35
|
+
unregister: (id: string) => void;
|
|
36
|
+
|
|
37
|
+
/** Open a surface in its declared container. Props (optional) are forwarded
|
|
38
|
+
* to the mounted plugin on first render. */
|
|
39
|
+
show: (id: string, props?: any) => void;
|
|
40
|
+
/** Close the surface with the given id (or whatever is active for the matching type) */
|
|
41
|
+
hide: (id?: string) => void;
|
|
42
|
+
|
|
43
|
+
/** All registered manifests, optionally filtered.
|
|
44
|
+
* `placement: 'unset'` selects only plugins WITHOUT a placement (default list). */
|
|
45
|
+
list: (filter?: {
|
|
46
|
+
type?: SurfaceType;
|
|
47
|
+
visibility?: SurfaceVisibility[];
|
|
48
|
+
placement?: SurfacePlacement | 'unset';
|
|
49
|
+
}) => SurfaceManifest[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const useSurfaceStore = create<SurfaceState>((set, get) => ({
|
|
53
|
+
manifests: {},
|
|
54
|
+
activeDrawer: null,
|
|
55
|
+
activeFullscreen: null,
|
|
56
|
+
activeProps: {},
|
|
57
|
+
mountKey: 0,
|
|
58
|
+
|
|
59
|
+
register: (manifest) =>
|
|
60
|
+
set(s => ({ manifests: { ...s.manifests, [manifest.id]: manifest } })),
|
|
61
|
+
|
|
62
|
+
unregister: (id) =>
|
|
63
|
+
set(s => {
|
|
64
|
+
const next = { ...s.manifests };
|
|
65
|
+
delete next[id];
|
|
66
|
+
const nextProps = { ...s.activeProps };
|
|
67
|
+
delete nextProps[id];
|
|
68
|
+
return { manifests: next, activeProps: nextProps };
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
show: (id, props) => {
|
|
72
|
+
const m = get().manifests[id];
|
|
73
|
+
if (!m) {
|
|
74
|
+
console.warn('[SurfaceStore] show: unknown surface id', id);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
set(s => {
|
|
78
|
+
const nextProps = props !== undefined
|
|
79
|
+
? { ...s.activeProps, [id]: props }
|
|
80
|
+
: s.activeProps;
|
|
81
|
+
// Bump mountKey on every show so the dispatcher remounts the Component,
|
|
82
|
+
// forcing the plugin's data fetch to re-run with fresh results.
|
|
83
|
+
const mountKey = s.mountKey + 1;
|
|
84
|
+
if (m.type === 'drawer') return { activeDrawer: id, activeProps: nextProps, mountKey };
|
|
85
|
+
if (m.type === 'fullscreen') return { activeFullscreen: id, activeProps: nextProps, mountKey };
|
|
86
|
+
return { mountKey };
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
hide: (id) => {
|
|
91
|
+
const s = get();
|
|
92
|
+
if (!id) {
|
|
93
|
+
// Hide whichever is active and clear their props
|
|
94
|
+
const cleanup = { ...s.activeProps };
|
|
95
|
+
if (s.activeDrawer) delete cleanup[s.activeDrawer];
|
|
96
|
+
if (s.activeFullscreen) delete cleanup[s.activeFullscreen];
|
|
97
|
+
set({ activeDrawer: null, activeFullscreen: null, activeProps: cleanup });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const cleanup = { ...s.activeProps };
|
|
101
|
+
delete cleanup[id];
|
|
102
|
+
const patch: Partial<SurfaceState> = { activeProps: cleanup };
|
|
103
|
+
if (s.activeDrawer === id) patch.activeDrawer = null;
|
|
104
|
+
if (s.activeFullscreen === id) patch.activeFullscreen = null;
|
|
105
|
+
set(patch);
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
list: (filter) => {
|
|
109
|
+
const all = Object.values(get().manifests);
|
|
110
|
+
return all.filter(m => {
|
|
111
|
+
if (filter?.type && m.type !== filter.type) return false;
|
|
112
|
+
if (filter?.visibility && !filter.visibility.includes(m.visibility)) return false;
|
|
113
|
+
if (filter?.placement) {
|
|
114
|
+
if (filter.placement === 'unset') {
|
|
115
|
+
if (m.placement) return false;
|
|
116
|
+
} else if (m.placement !== filter.placement) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
// Convenience guards
|
|
126
|
+
export function isMountSurface(m: SurfaceManifest): m is MountSurfaceManifest {
|
|
127
|
+
return m.type === 'drawer' || m.type === 'fullscreen';
|
|
128
|
+
}
|
|
129
|
+
export function isInlineSurface(m: SurfaceManifest): m is InlineSurfaceManifest {
|
|
130
|
+
return m.type === 'card' || m.type === 'header' || m.type === 'ambient' || m.type === 'nudge';
|
|
131
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface manifest types — the contract a plugin declares to live inside
|
|
3
|
+
* the Ada host shell.
|
|
4
|
+
*
|
|
5
|
+
* Future: this maps directly onto the platform-side `surface` field on
|
|
6
|
+
* plugin manifests. While that schema is in flight, the mobile shell
|
|
7
|
+
* registers built-in surfaces locally (see bootstrap.tsx).
|
|
8
|
+
*/
|
|
9
|
+
import type { ComponentType } from 'react';
|
|
10
|
+
|
|
11
|
+
export type SurfaceType =
|
|
12
|
+
| 'drawer'
|
|
13
|
+
| 'card'
|
|
14
|
+
| 'fullscreen'
|
|
15
|
+
| 'header'
|
|
16
|
+
| 'ambient'
|
|
17
|
+
| 'nudge';
|
|
18
|
+
|
|
19
|
+
export type SurfaceVisibility = 'always' | 'user' | 'engine';
|
|
20
|
+
|
|
21
|
+
/** Where in the host's chrome the plugin lives. Generic, host-agnostic role.
|
|
22
|
+
* - `featured`: signature surface — host shows it prominently (e.g. pinned card on home).
|
|
23
|
+
* - `menu`: lives as a row in the host's navigation menu (side menu / hamburger).
|
|
24
|
+
* - undefined: regular plugin — host puts it in the default plugin list / picker. */
|
|
25
|
+
export type SurfacePlacement = 'featured' | 'menu';
|
|
26
|
+
|
|
27
|
+
export interface SurfaceManifestBase {
|
|
28
|
+
/** Stable plugin/surface id. e.g. "mcp:memory:drawer" */
|
|
29
|
+
id: string;
|
|
30
|
+
type: SurfaceType;
|
|
31
|
+
visibility: SurfaceVisibility;
|
|
32
|
+
/** Optional: where in host chrome to render. Omit = default plugin list. */
|
|
33
|
+
placement?: SurfacePlacement;
|
|
34
|
+
/** Short emoji or symbol — shown on cards / header pills */
|
|
35
|
+
icon?: string;
|
|
36
|
+
title: string;
|
|
37
|
+
subtitle?: string;
|
|
38
|
+
/** Marks plugins that handle sensitive data (host shows lock pill) */
|
|
39
|
+
privacy?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A surface that mounts a host component when active (drawer, fullscreen) */
|
|
43
|
+
export interface MountSurfaceManifest extends SurfaceManifestBase {
|
|
44
|
+
type: 'drawer' | 'fullscreen';
|
|
45
|
+
/** React component the dispatcher mounts when this surface is active.
|
|
46
|
+
* Receives `dismiss` to close itself, plus optional `props` forwarded
|
|
47
|
+
* from `sys.focusUiPlugin(id, props)`. */
|
|
48
|
+
Component: ComponentType<{ dismiss: () => void; props?: any }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A surface that renders inline (card list / header / ambient) */
|
|
52
|
+
export interface InlineSurfaceManifest extends SurfaceManifestBase {
|
|
53
|
+
type: 'card' | 'header' | 'ambient' | 'nudge';
|
|
54
|
+
/** For cards: chat prompt to send when tapped */
|
|
55
|
+
prompt?: string;
|
|
56
|
+
/** Custom tap handler. Overrides `prompt` if present. */
|
|
57
|
+
onPress?: () => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type SurfaceManifest = MountSurfaceManifest | InlineSurfaceManifest;
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-built UI components for native plugins.
|
|
3
|
+
* These provide consistent styling using ThemeTokens.
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import {
|
|
7
|
+
View,
|
|
8
|
+
Text,
|
|
9
|
+
Pressable,
|
|
10
|
+
TextInput,
|
|
11
|
+
ActivityIndicator,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import type { ThemeTokens } from './types';
|
|
15
|
+
|
|
16
|
+
// ── Card ────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
interface CardProps {
|
|
19
|
+
theme: ThemeTokens;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
style?: any;
|
|
22
|
+
onPress?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Card({ theme, children, style, onPress }: CardProps) {
|
|
26
|
+
const cardStyle = [
|
|
27
|
+
styles.card,
|
|
28
|
+
{
|
|
29
|
+
backgroundColor: theme.colors.surface,
|
|
30
|
+
borderColor: theme.colors.border,
|
|
31
|
+
},
|
|
32
|
+
style,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
if (onPress) {
|
|
36
|
+
return React.createElement(Pressable, { style: cardStyle, onPress }, children);
|
|
37
|
+
}
|
|
38
|
+
return React.createElement(View, { style: cardStyle }, children);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Badge ───────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
interface BadgeProps {
|
|
44
|
+
theme: ThemeTokens;
|
|
45
|
+
label: string;
|
|
46
|
+
color?: 'accent' | 'success' | 'warning' | 'error' | 'purple';
|
|
47
|
+
style?: any;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BADGE_COLORS = {
|
|
51
|
+
accent: { bg: 'accentSoft', text: 'accent' },
|
|
52
|
+
success: { bg: 'successSoft', text: 'success' },
|
|
53
|
+
warning: { bg: 'warningSoft', text: 'warning' },
|
|
54
|
+
error: { bg: 'errorSoft', text: 'error' },
|
|
55
|
+
purple: { bg: 'purpleSoft', text: 'purple' },
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
export function Badge({ theme, label, color = 'accent', style }: BadgeProps) {
|
|
59
|
+
const mapping = BADGE_COLORS[color];
|
|
60
|
+
return React.createElement(
|
|
61
|
+
View,
|
|
62
|
+
{
|
|
63
|
+
style: [
|
|
64
|
+
styles.badge,
|
|
65
|
+
{
|
|
66
|
+
backgroundColor: theme.colors[mapping.bg],
|
|
67
|
+
borderRadius: theme.borderRadius.sm,
|
|
68
|
+
},
|
|
69
|
+
style,
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
React.createElement(Text, {
|
|
73
|
+
style: [styles.badgeText, { color: theme.colors[mapping.text] }],
|
|
74
|
+
}, label),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── TabBar ──────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
interface TabBarProps {
|
|
81
|
+
theme: ThemeTokens;
|
|
82
|
+
tabs: string[];
|
|
83
|
+
activeTab: string;
|
|
84
|
+
onTabChange: (tab: string) => void;
|
|
85
|
+
style?: any;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function TabBar({ theme, tabs, activeTab, onTabChange, style }: TabBarProps) {
|
|
89
|
+
return React.createElement(
|
|
90
|
+
View,
|
|
91
|
+
{
|
|
92
|
+
style: [
|
|
93
|
+
styles.tabBar,
|
|
94
|
+
{ borderBottomColor: theme.colors.border },
|
|
95
|
+
style,
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
tabs.map((tab) =>
|
|
99
|
+
React.createElement(
|
|
100
|
+
Pressable,
|
|
101
|
+
{
|
|
102
|
+
key: tab,
|
|
103
|
+
style: [
|
|
104
|
+
styles.tab,
|
|
105
|
+
{
|
|
106
|
+
borderBottomColor: tab === activeTab ? theme.colors.accent : 'transparent',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
onPress: () => onTabChange(tab),
|
|
110
|
+
},
|
|
111
|
+
React.createElement(Text, {
|
|
112
|
+
style: {
|
|
113
|
+
color: tab === activeTab ? theme.colors.accent : theme.colors.textSecondary,
|
|
114
|
+
fontSize: theme.fontSize.sm,
|
|
115
|
+
fontWeight: tab === activeTab ? '600' : '400',
|
|
116
|
+
},
|
|
117
|
+
}, tab),
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── ListItem ────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
interface ListItemProps {
|
|
126
|
+
theme: ThemeTokens;
|
|
127
|
+
title: string;
|
|
128
|
+
subtitle?: string;
|
|
129
|
+
right?: React.ReactNode;
|
|
130
|
+
onPress?: () => void;
|
|
131
|
+
style?: any;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function ListItem({ theme, title, subtitle, right, onPress, style }: ListItemProps) {
|
|
135
|
+
const content = [
|
|
136
|
+
React.createElement(
|
|
137
|
+
View,
|
|
138
|
+
{ key: 'content', style: styles.listItemContent },
|
|
139
|
+
React.createElement(Text, {
|
|
140
|
+
style: { color: theme.colors.text, fontSize: theme.fontSize.md },
|
|
141
|
+
numberOfLines: 1,
|
|
142
|
+
}, title),
|
|
143
|
+
subtitle
|
|
144
|
+
? React.createElement(Text, {
|
|
145
|
+
style: { color: theme.colors.textSecondary, fontSize: theme.fontSize.xs, marginTop: 2 },
|
|
146
|
+
numberOfLines: 1,
|
|
147
|
+
}, subtitle)
|
|
148
|
+
: null,
|
|
149
|
+
),
|
|
150
|
+
right ? React.createElement(View, { key: 'right' }, right) : null,
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
if (onPress) {
|
|
154
|
+
return React.createElement(
|
|
155
|
+
Pressable,
|
|
156
|
+
{ style: [styles.listItem, { borderBottomColor: theme.colors.border }, style], onPress },
|
|
157
|
+
...content,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return React.createElement(
|
|
161
|
+
View,
|
|
162
|
+
{ style: [styles.listItem, { borderBottomColor: theme.colors.border }, style] },
|
|
163
|
+
...content,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── SearchBar ───────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
interface SearchBarProps {
|
|
170
|
+
theme: ThemeTokens;
|
|
171
|
+
value: string;
|
|
172
|
+
onChangeText: (text: string) => void;
|
|
173
|
+
placeholder?: string;
|
|
174
|
+
style?: any;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function SearchBar({ theme, value, onChangeText, placeholder = 'Search...', style }: SearchBarProps) {
|
|
178
|
+
return React.createElement(TextInput, {
|
|
179
|
+
style: [
|
|
180
|
+
styles.searchBar,
|
|
181
|
+
{
|
|
182
|
+
backgroundColor: theme.colors.input,
|
|
183
|
+
color: theme.colors.text,
|
|
184
|
+
borderColor: theme.colors.border,
|
|
185
|
+
borderRadius: theme.borderRadius.sm,
|
|
186
|
+
fontSize: theme.fontSize.sm,
|
|
187
|
+
},
|
|
188
|
+
style,
|
|
189
|
+
],
|
|
190
|
+
value,
|
|
191
|
+
onChangeText,
|
|
192
|
+
placeholder,
|
|
193
|
+
placeholderTextColor: theme.colors.textMuted,
|
|
194
|
+
autoCapitalize: 'none',
|
|
195
|
+
autoCorrect: false,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── EmptyState ──────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
interface EmptyStateProps {
|
|
202
|
+
theme: ThemeTokens;
|
|
203
|
+
title: string;
|
|
204
|
+
subtitle?: string;
|
|
205
|
+
style?: any;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function EmptyState({ theme, title, subtitle, style }: EmptyStateProps) {
|
|
209
|
+
return React.createElement(
|
|
210
|
+
View,
|
|
211
|
+
{ style: [styles.emptyState, style] },
|
|
212
|
+
React.createElement(Text, {
|
|
213
|
+
style: { color: theme.colors.textSecondary, fontSize: theme.fontSize.md, textAlign: 'center' },
|
|
214
|
+
}, title),
|
|
215
|
+
subtitle
|
|
216
|
+
? React.createElement(Text, {
|
|
217
|
+
style: { color: theme.colors.textMuted, fontSize: theme.fontSize.sm, textAlign: 'center', marginTop: 4 },
|
|
218
|
+
}, subtitle)
|
|
219
|
+
: null,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── ErrorState ──────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
interface ErrorStateProps {
|
|
226
|
+
theme: ThemeTokens;
|
|
227
|
+
message: string;
|
|
228
|
+
onRetry?: () => void;
|
|
229
|
+
style?: any;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function ErrorState({ theme, message, onRetry, style }: ErrorStateProps) {
|
|
233
|
+
return React.createElement(
|
|
234
|
+
View,
|
|
235
|
+
{ style: [styles.emptyState, style] },
|
|
236
|
+
React.createElement(Text, {
|
|
237
|
+
style: { color: theme.colors.error, fontSize: theme.fontSize.md, textAlign: 'center' },
|
|
238
|
+
}, message),
|
|
239
|
+
onRetry
|
|
240
|
+
? React.createElement(
|
|
241
|
+
Pressable,
|
|
242
|
+
{
|
|
243
|
+
style: [styles.retryButton, { backgroundColor: theme.colors.accentSoft, borderRadius: theme.borderRadius.sm }],
|
|
244
|
+
onPress: onRetry,
|
|
245
|
+
},
|
|
246
|
+
React.createElement(Text, {
|
|
247
|
+
style: { color: theme.colors.accent, fontSize: theme.fontSize.sm, fontWeight: '600' },
|
|
248
|
+
}, 'Retry'),
|
|
249
|
+
)
|
|
250
|
+
: null,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── LoadingState ────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
interface LoadingStateProps {
|
|
257
|
+
theme: ThemeTokens;
|
|
258
|
+
message?: string;
|
|
259
|
+
style?: any;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function LoadingState({ theme, message = 'Loading...', style }: LoadingStateProps) {
|
|
263
|
+
return React.createElement(
|
|
264
|
+
View,
|
|
265
|
+
{ style: [styles.emptyState, style] },
|
|
266
|
+
React.createElement(ActivityIndicator, { size: 'small', color: theme.colors.accent }),
|
|
267
|
+
React.createElement(Text, {
|
|
268
|
+
style: { color: theme.colors.textSecondary, fontSize: theme.fontSize.sm, marginTop: 8 },
|
|
269
|
+
}, message),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── ActionButton (FAB) ──────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
interface ActionButtonProps {
|
|
276
|
+
theme: ThemeTokens;
|
|
277
|
+
label?: string;
|
|
278
|
+
icon?: React.ReactNode;
|
|
279
|
+
onPress: () => void;
|
|
280
|
+
style?: any;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function ActionButton({ theme, label, icon, onPress, style }: ActionButtonProps) {
|
|
284
|
+
return React.createElement(
|
|
285
|
+
Pressable,
|
|
286
|
+
{
|
|
287
|
+
style: [
|
|
288
|
+
styles.fab,
|
|
289
|
+
{ backgroundColor: theme.colors.accent, borderRadius: theme.borderRadius.full },
|
|
290
|
+
style,
|
|
291
|
+
],
|
|
292
|
+
onPress,
|
|
293
|
+
},
|
|
294
|
+
icon || React.createElement(Text, { style: styles.fabText }, label || '+'),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Styles ──────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
const styles = StyleSheet.create({
|
|
301
|
+
card: {
|
|
302
|
+
padding: 12,
|
|
303
|
+
borderRadius: 12,
|
|
304
|
+
borderWidth: 1,
|
|
305
|
+
marginBottom: 8,
|
|
306
|
+
},
|
|
307
|
+
badge: {
|
|
308
|
+
paddingHorizontal: 8,
|
|
309
|
+
paddingVertical: 3,
|
|
310
|
+
},
|
|
311
|
+
badgeText: {
|
|
312
|
+
fontSize: 11,
|
|
313
|
+
fontWeight: '600',
|
|
314
|
+
},
|
|
315
|
+
tabBar: {
|
|
316
|
+
flexDirection: 'row',
|
|
317
|
+
borderBottomWidth: 1,
|
|
318
|
+
},
|
|
319
|
+
tab: {
|
|
320
|
+
flex: 1,
|
|
321
|
+
padding: 12,
|
|
322
|
+
alignItems: 'center',
|
|
323
|
+
borderBottomWidth: 2,
|
|
324
|
+
},
|
|
325
|
+
listItem: {
|
|
326
|
+
flexDirection: 'row',
|
|
327
|
+
alignItems: 'center',
|
|
328
|
+
paddingVertical: 12,
|
|
329
|
+
paddingHorizontal: 16,
|
|
330
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
331
|
+
},
|
|
332
|
+
listItemContent: {
|
|
333
|
+
flex: 1,
|
|
334
|
+
},
|
|
335
|
+
searchBar: {
|
|
336
|
+
height: 40,
|
|
337
|
+
paddingHorizontal: 12,
|
|
338
|
+
borderWidth: 1,
|
|
339
|
+
},
|
|
340
|
+
emptyState: {
|
|
341
|
+
flex: 1,
|
|
342
|
+
justifyContent: 'center',
|
|
343
|
+
alignItems: 'center',
|
|
344
|
+
padding: 32,
|
|
345
|
+
},
|
|
346
|
+
retryButton: {
|
|
347
|
+
marginTop: 12,
|
|
348
|
+
paddingHorizontal: 16,
|
|
349
|
+
paddingVertical: 8,
|
|
350
|
+
},
|
|
351
|
+
fab: {
|
|
352
|
+
position: 'absolute',
|
|
353
|
+
bottom: 24,
|
|
354
|
+
right: 24,
|
|
355
|
+
width: 56,
|
|
356
|
+
height: 56,
|
|
357
|
+
justifyContent: 'center',
|
|
358
|
+
alignItems: 'center',
|
|
359
|
+
shadowColor: '#000',
|
|
360
|
+
shadowOffset: { width: 0, height: 2 },
|
|
361
|
+
shadowOpacity: 0.25,
|
|
362
|
+
shadowRadius: 4,
|
|
363
|
+
elevation: 5,
|
|
364
|
+
},
|
|
365
|
+
fabText: {
|
|
366
|
+
color: '#fff',
|
|
367
|
+
fontSize: 24,
|
|
368
|
+
fontWeight: '600',
|
|
369
|
+
},
|
|
370
|
+
});
|
package/src/sdk/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @adas/plugin-sdk namespace — everything a PLUGIN author imports.
|
|
3
|
+
* Remote bundles resolve `require('@adas/plugin-sdk')` to exactly this
|
|
4
|
+
* module via the bundle manager's require shim.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Registration API
|
|
8
|
+
export { register, registerService } from './registration';
|
|
9
|
+
import { register, registerService } from './registration';
|
|
10
|
+
export const PluginSDK = { register, registerService } as const;
|
|
11
|
+
|
|
12
|
+
// Types
|
|
13
|
+
export type {
|
|
14
|
+
PluginBridge,
|
|
15
|
+
PluginNative,
|
|
16
|
+
PluginContext,
|
|
17
|
+
PluginProps,
|
|
18
|
+
ThemeTokens,
|
|
19
|
+
HapticsAPI,
|
|
20
|
+
CameraAPI,
|
|
21
|
+
LocationAPI,
|
|
22
|
+
StorageAPI,
|
|
23
|
+
NotificationsAPI,
|
|
24
|
+
ConnectivityAPI,
|
|
25
|
+
FileSystemAPI,
|
|
26
|
+
CookiesAPI,
|
|
27
|
+
SharingAPI,
|
|
28
|
+
ScannerAPI,
|
|
29
|
+
NfcAPI,
|
|
30
|
+
BiometricsAPI,
|
|
31
|
+
BiometricsAPIv2,
|
|
32
|
+
NativeCapabilities,
|
|
33
|
+
PluginHooks,
|
|
34
|
+
PluginManifest,
|
|
35
|
+
RenderMode,
|
|
36
|
+
RegisteredPlugin,
|
|
37
|
+
RegisteredUIPlugin,
|
|
38
|
+
RegisteredServicePlugin,
|
|
39
|
+
} from './types';
|
|
40
|
+
|
|
41
|
+
// RN primitives (plugins import from the SDK, not from react-native)
|
|
42
|
+
export {
|
|
43
|
+
View,
|
|
44
|
+
Text,
|
|
45
|
+
ScrollView,
|
|
46
|
+
FlatList,
|
|
47
|
+
SectionList,
|
|
48
|
+
Pressable,
|
|
49
|
+
TouchableOpacity,
|
|
50
|
+
TextInput,
|
|
51
|
+
Image,
|
|
52
|
+
ActivityIndicator,
|
|
53
|
+
Modal,
|
|
54
|
+
Switch,
|
|
55
|
+
StyleSheet,
|
|
56
|
+
Animated,
|
|
57
|
+
Platform,
|
|
58
|
+
RefreshControl,
|
|
59
|
+
} from './primitives';
|
|
60
|
+
|
|
61
|
+
// Pre-built components
|
|
62
|
+
export {
|
|
63
|
+
Card,
|
|
64
|
+
Badge,
|
|
65
|
+
TabBar,
|
|
66
|
+
ListItem,
|
|
67
|
+
SearchBar,
|
|
68
|
+
EmptyState,
|
|
69
|
+
ErrorState,
|
|
70
|
+
LoadingState,
|
|
71
|
+
ActionButton,
|
|
72
|
+
} from './components';
|
|
73
|
+
|
|
74
|
+
// Theme + API hooks
|
|
75
|
+
export { usePluginTheme, setPluginThemeColors } from './usePluginTheme';
|
|
76
|
+
export { useApi } from './useApi';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exported React Native primitives.
|
|
3
|
+
* Plugins import these from '@adas/plugin-sdk' — never from 'react-native' directly.
|
|
4
|
+
*/
|
|
5
|
+
export {
|
|
6
|
+
View,
|
|
7
|
+
Text,
|
|
8
|
+
ScrollView,
|
|
9
|
+
FlatList,
|
|
10
|
+
SectionList,
|
|
11
|
+
Pressable,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
TextInput,
|
|
14
|
+
Image,
|
|
15
|
+
ActivityIndicator,
|
|
16
|
+
Modal,
|
|
17
|
+
Switch,
|
|
18
|
+
StyleSheet,
|
|
19
|
+
Animated,
|
|
20
|
+
Platform,
|
|
21
|
+
RefreshControl,
|
|
22
|
+
} from 'react-native';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin registration API.
|
|
3
|
+
* Solution developers use these to register their plugins.
|
|
4
|
+
*/
|
|
5
|
+
import type {
|
|
6
|
+
NativeCapabilities,
|
|
7
|
+
PluginHooks,
|
|
8
|
+
PluginProps,
|
|
9
|
+
RegisteredUIPlugin,
|
|
10
|
+
RegisteredServicePlugin,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register a UI or hybrid plugin (has a visual component).
|
|
15
|
+
*/
|
|
16
|
+
export function register(
|
|
17
|
+
id: string,
|
|
18
|
+
config: {
|
|
19
|
+
type?: 'ui' | 'hybrid';
|
|
20
|
+
capabilities?: NativeCapabilities;
|
|
21
|
+
Component: React.FC<PluginProps>;
|
|
22
|
+
},
|
|
23
|
+
): RegisteredUIPlugin {
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
type: config.type || 'ui',
|
|
27
|
+
capabilities: config.capabilities,
|
|
28
|
+
Component: config.Component,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a headless service plugin (no UI).
|
|
34
|
+
*/
|
|
35
|
+
export function registerService(
|
|
36
|
+
id: string,
|
|
37
|
+
config: {
|
|
38
|
+
capabilities?: NativeCapabilities;
|
|
39
|
+
hooks?: PluginHooks;
|
|
40
|
+
onStart: (props: Omit<PluginProps, 'theme'>) => Promise<void> | void;
|
|
41
|
+
onStop?: () => Promise<void> | void;
|
|
42
|
+
},
|
|
43
|
+
): RegisteredServicePlugin {
|
|
44
|
+
return {
|
|
45
|
+
id,
|
|
46
|
+
type: 'service',
|
|
47
|
+
capabilities: config.capabilities,
|
|
48
|
+
hooks: config.hooks,
|
|
49
|
+
onStart: config.onStart,
|
|
50
|
+
onStop: config.onStop,
|
|
51
|
+
};
|
|
52
|
+
}
|