@hamak/ui-shell-impl 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/.turbo/turbo-build.log +1 -0
- package/dist/core/DefaultFeatureManager.d.ts +24 -0
- package/dist/core/DefaultFeatureManager.d.ts.map +1 -0
- package/dist/core/DefaultFeatureManager.js +80 -0
- package/dist/core/DefaultLayoutManager.d.ts +19 -0
- package/dist/core/DefaultLayoutManager.d.ts.map +1 -0
- package/dist/core/DefaultLayoutManager.js +59 -0
- package/dist/core/DefaultRouter.d.ts +30 -0
- package/dist/core/DefaultRouter.d.ts.map +1 -0
- package/dist/core/DefaultRouter.js +111 -0
- package/dist/core/DefaultShell.d.ts +30 -0
- package/dist/core/DefaultShell.d.ts.map +1 -0
- package/dist/core/DefaultShell.js +147 -0
- package/dist/core/DefaultThemeManager.d.ts +27 -0
- package/dist/core/DefaultThemeManager.d.ts.map +1 -0
- package/dist/core/DefaultThemeManager.js +76 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +9 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/plugin/ShellPluginFactory.d.ts +11 -0
- package/dist/plugin/ShellPluginFactory.d.ts.map +1 -0
- package/dist/plugin/ShellPluginFactory.js +73 -0
- package/dist/plugin/index.d.ts +6 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +5 -0
- package/dist/providers/CSSVariablesThemeProvider.d.ts +16 -0
- package/dist/providers/CSSVariablesThemeProvider.d.ts.map +1 -0
- package/dist/providers/CSSVariablesThemeProvider.js +47 -0
- package/dist/providers/HashRouterStrategy.d.ts +20 -0
- package/dist/providers/HashRouterStrategy.d.ts.map +1 -0
- package/dist/providers/HashRouterStrategy.js +57 -0
- package/dist/providers/HistoryRouterStrategy.d.ts +21 -0
- package/dist/providers/HistoryRouterStrategy.d.ts.map +1 -0
- package/dist/providers/HistoryRouterStrategy.js +64 -0
- package/dist/providers/LocalStorageProvider.d.ts +13 -0
- package/dist/providers/LocalStorageProvider.d.ts.map +1 -0
- package/dist/providers/LocalStorageProvider.js +50 -0
- package/dist/providers/MemoryStorageProvider.d.ts +14 -0
- package/dist/providers/MemoryStorageProvider.d.ts.map +1 -0
- package/dist/providers/MemoryStorageProvider.js +22 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +9 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/viewport-utils.d.ts +16 -0
- package/dist/utils/viewport-utils.d.ts.map +1 -0
- package/dist/utils/viewport-utils.js +52 -0
- package/package.json +37 -0
- package/src/core/DefaultFeatureManager.ts +101 -0
- package/src/core/DefaultLayoutManager.ts +74 -0
- package/src/core/DefaultRouter.ts +135 -0
- package/src/core/DefaultShell.ts +176 -0
- package/src/core/DefaultThemeManager.ts +99 -0
- package/src/core/index.ts +10 -0
- package/src/index.ts +51 -0
- package/src/plugin/ShellPluginFactory.ts +98 -0
- package/src/plugin/index.ts +6 -0
- package/src/providers/CSSVariablesThemeProvider.ts +60 -0
- package/src/providers/HashRouterStrategy.ts +71 -0
- package/src/providers/HistoryRouterStrategy.ts +78 -0
- package/src/providers/LocalStorageProvider.ts +53 -0
- package/src/providers/MemoryStorageProvider.ts +30 -0
- package/src/providers/index.ts +10 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/viewport-utils.ts +64 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Feature Manager Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { IFeatureManager } from '@amk/ui-shell-api';
|
|
6
|
+
import type { FeatureFlags } from '@amk/ui-shell-api';
|
|
7
|
+
|
|
8
|
+
export class DefaultFeatureManager implements IFeatureManager {
|
|
9
|
+
private features: FeatureFlags;
|
|
10
|
+
private listeners: Map<string, Set<(value: any) => void>> = new Map();
|
|
11
|
+
|
|
12
|
+
constructor(initialFeatures: FeatureFlags = {}) {
|
|
13
|
+
this.features = { ...initialFeatures };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
isEnabled(key: string): boolean {
|
|
17
|
+
const value = this.features[key];
|
|
18
|
+
return typeof value === 'boolean' ? value : Boolean(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get<T = any>(key: string, defaultValue?: T): T {
|
|
22
|
+
const value = this.features[key];
|
|
23
|
+
return (value !== undefined ? value : defaultValue) as T;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set(key: string, value: any): void {
|
|
27
|
+
const oldValue = this.features[key];
|
|
28
|
+
if (oldValue === value) return;
|
|
29
|
+
|
|
30
|
+
this.features[key] = value;
|
|
31
|
+
this.notifyListeners(key, value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
enable(key: string): void {
|
|
35
|
+
this.set(key, true);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
disable(key: string): void {
|
|
39
|
+
this.set(key, false);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
toggle(key: string): void {
|
|
43
|
+
this.set(key, !this.isEnabled(key));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
update(updates: FeatureFlags): void {
|
|
47
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
48
|
+
this.set(key, value);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getAll(): Readonly<FeatureFlags> {
|
|
53
|
+
return { ...this.features };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
has(key: string): boolean {
|
|
57
|
+
return key in this.features;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
subscribe(key: string, listener: (value: any) => void): () => void {
|
|
61
|
+
if (!this.listeners.has(key)) {
|
|
62
|
+
this.listeners.set(key, new Set());
|
|
63
|
+
}
|
|
64
|
+
this.listeners.get(key)!.add(listener);
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
const listeners = this.listeners.get(key);
|
|
68
|
+
if (listeners) {
|
|
69
|
+
listeners.delete(listener);
|
|
70
|
+
if (listeners.size === 0) {
|
|
71
|
+
this.listeners.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
subscribeAll(listener: (key: string, value: any) => void): () => void {
|
|
78
|
+
const unsubscribers: (() => void)[] = [];
|
|
79
|
+
const currentKeys = Object.keys(this.features);
|
|
80
|
+
|
|
81
|
+
currentKeys.forEach(key => {
|
|
82
|
+
const unsub = this.subscribe(key, value => listener(key, value));
|
|
83
|
+
unsubscribers.push(unsub);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
unsubscribers.forEach(unsub => unsub());
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
destroy(): void {
|
|
92
|
+
this.listeners.clear();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private notifyListeners(key: string, value: any): void {
|
|
96
|
+
const listeners = this.listeners.get(key);
|
|
97
|
+
if (listeners) {
|
|
98
|
+
listeners.forEach(listener => listener(value));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Layout Manager Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ILayoutManager } from '@amk/ui-shell-api';
|
|
6
|
+
import type { LayoutSlot, LayoutArea } from '@amk/ui-shell-api';
|
|
7
|
+
|
|
8
|
+
export class DefaultLayoutManager implements ILayoutManager {
|
|
9
|
+
private slots: Map<string, Set<LayoutSlot>> = new Map();
|
|
10
|
+
private listeners: Set<() => void> = new Set();
|
|
11
|
+
|
|
12
|
+
registerSlot(slot: LayoutSlot): () => void {
|
|
13
|
+
const area = slot.area;
|
|
14
|
+
if (!this.slots.has(area)) {
|
|
15
|
+
this.slots.set(area, new Set());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.slots.get(area)!.add(slot);
|
|
19
|
+
this.notifyListeners();
|
|
20
|
+
|
|
21
|
+
return () => this.unregisterSlot(slot);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
unregisterSlot(slot: LayoutSlot): void {
|
|
25
|
+
const area = slot.area;
|
|
26
|
+
const slots = this.slots.get(area);
|
|
27
|
+
if (slots) {
|
|
28
|
+
slots.delete(slot);
|
|
29
|
+
if (slots.size === 0) {
|
|
30
|
+
this.slots.delete(area);
|
|
31
|
+
}
|
|
32
|
+
this.notifyListeners();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getSlots(area: LayoutArea): LayoutSlot[] {
|
|
37
|
+
const slots = this.slots.get(area);
|
|
38
|
+
if (!slots) return [];
|
|
39
|
+
|
|
40
|
+
return Array.from(slots).sort((a, b) => {
|
|
41
|
+
const priorityA = a.priority || 0;
|
|
42
|
+
const priorityB = b.priority || 0;
|
|
43
|
+
return priorityB - priorityA; // Higher priority first
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getAreas(): LayoutArea[] {
|
|
48
|
+
return Array.from(this.slots.keys()) as LayoutArea[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
hasSlots(area: LayoutArea): boolean {
|
|
52
|
+
const slots = this.slots.get(area);
|
|
53
|
+
return Boolean(slots && slots.size > 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
subscribe(listener: () => void): () => void {
|
|
57
|
+
this.listeners.add(listener);
|
|
58
|
+
return () => this.listeners.delete(listener);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clear(): void {
|
|
62
|
+
this.slots.clear();
|
|
63
|
+
this.notifyListeners();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
destroy(): void {
|
|
67
|
+
this.slots.clear();
|
|
68
|
+
this.listeners.clear();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private notifyListeners(): void {
|
|
72
|
+
this.listeners.forEach(listener => listener());
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Router Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { IRouter } from '@amk/ui-shell-api';
|
|
6
|
+
import type { RouteConfig, RouterOptions } from '@amk/ui-shell-api';
|
|
7
|
+
import type { IRouterStrategy } from '@amk/ui-shell-spi';
|
|
8
|
+
import type { NavigationGuard } from '@amk/ui-shell-spi';
|
|
9
|
+
|
|
10
|
+
export class DefaultRouter implements IRouter {
|
|
11
|
+
private routes: Map<string, RouteConfig> = new Map();
|
|
12
|
+
private currentRoute: RouteConfig | null = null;
|
|
13
|
+
private guards: NavigationGuard[] = [];
|
|
14
|
+
private listeners: Set<(route: RouteConfig) => void> = new Set();
|
|
15
|
+
private strategy: IRouterStrategy;
|
|
16
|
+
|
|
17
|
+
constructor(options: RouterOptions, strategy: IRouterStrategy) {
|
|
18
|
+
this.strategy = strategy;
|
|
19
|
+
this.guards = [];
|
|
20
|
+
this.registerRoutes(options.routes);
|
|
21
|
+
this.setupNavigationListener();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private registerRoutes(routes: RouteConfig[]): void {
|
|
25
|
+
routes.forEach(route => {
|
|
26
|
+
this.routes.set(route.path, route);
|
|
27
|
+
if (route.children) {
|
|
28
|
+
this.registerRoutes(route.children);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async push(path: string): Promise<boolean> {
|
|
34
|
+
const route = this.matchRoute(path);
|
|
35
|
+
if (!route) {
|
|
36
|
+
console.error(`Route not found: ${path}`);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!(await this.runGuards(route))) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (route.beforeEnter && !(await route.beforeEnter(route, this.currentRoute!))) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.currentRoute = route;
|
|
49
|
+
this.strategy.push(path);
|
|
50
|
+
this.notifyListeners(route);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async replace(path: string): Promise<boolean> {
|
|
55
|
+
const route = this.matchRoute(path);
|
|
56
|
+
if (!route) {
|
|
57
|
+
console.error(`Route not found: ${path}`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!(await this.runGuards(route))) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.currentRoute = route;
|
|
66
|
+
this.strategy.replace(path);
|
|
67
|
+
this.notifyListeners(route);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
back(): void {
|
|
72
|
+
this.strategy.back();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
forward(): void {
|
|
76
|
+
this.strategy.forward();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getCurrentRoute(): RouteConfig | null {
|
|
80
|
+
return this.currentRoute;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
subscribe(listener: (route: RouteConfig) => void): () => void {
|
|
84
|
+
this.listeners.add(listener);
|
|
85
|
+
return () => this.listeners.delete(listener);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async loadRouteComponent(route: RouteConfig): Promise<any> {
|
|
89
|
+
try {
|
|
90
|
+
const component = await route.component();
|
|
91
|
+
return component;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`Failed to load route component for ${route.path}:`, error);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
addGuard(guard: NavigationGuard): () => void {
|
|
99
|
+
this.guards.push(guard);
|
|
100
|
+
return () => {
|
|
101
|
+
const index = this.guards.indexOf(guard);
|
|
102
|
+
if (index > -1) {
|
|
103
|
+
this.guards.splice(index, 1);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
destroy(): void {
|
|
109
|
+
this.strategy.destroy();
|
|
110
|
+
this.listeners.clear();
|
|
111
|
+
this.guards = [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private matchRoute(path: string): RouteConfig | null {
|
|
115
|
+
return this.routes.get(path) || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async runGuards(to: RouteConfig): Promise<boolean> {
|
|
119
|
+
for (const guard of this.guards) {
|
|
120
|
+
const result = await guard(to, this.currentRoute);
|
|
121
|
+
if (!result) return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private setupNavigationListener(): void {
|
|
127
|
+
this.strategy.listen((path) => {
|
|
128
|
+
this.push(path);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private notifyListeners(route: RouteConfig): void {
|
|
133
|
+
this.listeners.forEach(listener => listener(route));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Shell Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { IShell, IThemeManager, IFeatureManager, IRouter } from '@amk/ui-shell-api';
|
|
6
|
+
import type { ShellConfig, ShellContext, ShellEvent, ShellEventListener, ShellEventType } from '@amk/ui-shell-api';
|
|
7
|
+
import { DefaultThemeManager } from './DefaultThemeManager';
|
|
8
|
+
import { DefaultFeatureManager } from './DefaultFeatureManager';
|
|
9
|
+
import { LocalStorageProvider } from '../providers/LocalStorageProvider';
|
|
10
|
+
import { CSSVariablesThemeProvider } from '../providers/CSSVariablesThemeProvider';
|
|
11
|
+
|
|
12
|
+
export class DefaultShell implements IShell {
|
|
13
|
+
private themeManager: IThemeManager;
|
|
14
|
+
private featureManager: IFeatureManager;
|
|
15
|
+
private router: IRouter | null = null;
|
|
16
|
+
private eventListeners: Map<string, Set<ShellEventListener>> = new Map();
|
|
17
|
+
private viewportState = {
|
|
18
|
+
width: typeof window !== 'undefined' ? window.innerWidth : 0,
|
|
19
|
+
height: typeof window !== 'undefined' ? window.innerHeight : 0,
|
|
20
|
+
};
|
|
21
|
+
private config: ShellConfig;
|
|
22
|
+
private isReady = false;
|
|
23
|
+
|
|
24
|
+
constructor(config: ShellConfig = {}) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
|
|
27
|
+
// Create theme manager with providers
|
|
28
|
+
const storageProvider = new LocalStorageProvider();
|
|
29
|
+
const themeProvider = new CSSVariablesThemeProvider();
|
|
30
|
+
this.themeManager = new DefaultThemeManager(config.theme, storageProvider, themeProvider);
|
|
31
|
+
|
|
32
|
+
// Create feature manager
|
|
33
|
+
this.featureManager = new DefaultFeatureManager(config.features);
|
|
34
|
+
|
|
35
|
+
this.setupViewportListener();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async initialize(): Promise<void> {
|
|
39
|
+
if (this.isReady) {
|
|
40
|
+
console.warn('Shell is already initialized');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Apply initial theme
|
|
45
|
+
this.themeManager.setTheme(this.themeManager.getTheme());
|
|
46
|
+
|
|
47
|
+
// Subscribe to theme changes
|
|
48
|
+
this.themeManager.subscribe(theme => {
|
|
49
|
+
this.emit('theme:changed', { theme });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Subscribe to feature changes
|
|
53
|
+
this.featureManager.subscribeAll((key, value) => {
|
|
54
|
+
this.emit('feature:toggled', { key, value });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.isReady = true;
|
|
58
|
+
this.emit('shell:ready', {});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getContext(): ShellContext {
|
|
62
|
+
return {
|
|
63
|
+
theme: this.themeManager.getTheme(),
|
|
64
|
+
setTheme: (mode) => this.themeManager.setTheme(mode),
|
|
65
|
+
features: this.featureManager.getAll(),
|
|
66
|
+
isFeatureEnabled: (key) => this.featureManager.isEnabled(key),
|
|
67
|
+
getFeature: (key, defaultValue) => this.featureManager.get(key, defaultValue),
|
|
68
|
+
viewport: {
|
|
69
|
+
width: this.viewportState.width,
|
|
70
|
+
height: this.viewportState.height,
|
|
71
|
+
isMobile: this.viewportState.width < 768,
|
|
72
|
+
isTablet: this.viewportState.width >= 768 && this.viewportState.width < 1024,
|
|
73
|
+
isDesktop: this.viewportState.width >= 1024,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getThemeManager(): IThemeManager {
|
|
79
|
+
return this.themeManager;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getFeatureManager(): IFeatureManager {
|
|
83
|
+
return this.featureManager;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getRouter(): IRouter | null {
|
|
87
|
+
return this.router;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setRouter(router: IRouter): void {
|
|
91
|
+
this.router = router;
|
|
92
|
+
this.router.subscribe(route => {
|
|
93
|
+
this.emit('route:changed', { route });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
emit(type: ShellEventType, payload?: any): void {
|
|
98
|
+
const event: ShellEvent = {
|
|
99
|
+
type,
|
|
100
|
+
payload,
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const listeners = this.eventListeners.get(type);
|
|
105
|
+
if (listeners) {
|
|
106
|
+
listeners.forEach(listener => listener(event));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const wildcardListeners = this.eventListeners.get('*');
|
|
110
|
+
if (wildcardListeners) {
|
|
111
|
+
wildcardListeners.forEach(listener => listener(event));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
on(type: ShellEventType | '*', listener: ShellEventListener): () => void {
|
|
116
|
+
if (!this.eventListeners.has(type)) {
|
|
117
|
+
this.eventListeners.set(type, new Set());
|
|
118
|
+
}
|
|
119
|
+
this.eventListeners.get(type)!.add(listener);
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
const listeners = this.eventListeners.get(type);
|
|
123
|
+
if (listeners) {
|
|
124
|
+
listeners.delete(listener);
|
|
125
|
+
if (listeners.size === 0) {
|
|
126
|
+
this.eventListeners.delete(type);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
off(type: ShellEventType | '*', listener: ShellEventListener): void {
|
|
133
|
+
const listeners = this.eventListeners.get(type);
|
|
134
|
+
if (listeners) {
|
|
135
|
+
listeners.delete(listener);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ready(): boolean {
|
|
140
|
+
return this.isReady;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getConfig(): Readonly<ShellConfig> {
|
|
144
|
+
return { ...this.config };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
destroy(): void {
|
|
148
|
+
this.themeManager.destroy();
|
|
149
|
+
this.featureManager.destroy();
|
|
150
|
+
if (this.router) {
|
|
151
|
+
this.router.destroy();
|
|
152
|
+
}
|
|
153
|
+
if (typeof window !== 'undefined') {
|
|
154
|
+
window.removeEventListener('resize', this.handleResize);
|
|
155
|
+
}
|
|
156
|
+
this.eventListeners.clear();
|
|
157
|
+
this.isReady = false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private setupViewportListener(): void {
|
|
161
|
+
if (typeof window === 'undefined') return;
|
|
162
|
+
window.addEventListener('resize', this.handleResize);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private handleResize = (): void => {
|
|
166
|
+
if (typeof window === 'undefined') return;
|
|
167
|
+
|
|
168
|
+
this.viewportState.width = window.innerWidth;
|
|
169
|
+
this.viewportState.height = window.innerHeight;
|
|
170
|
+
|
|
171
|
+
this.emit('viewport:resized', {
|
|
172
|
+
width: this.viewportState.width,
|
|
173
|
+
height: this.viewportState.height,
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Theme Manager Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { IThemeManager } from '@amk/ui-shell-api';
|
|
6
|
+
import type { ThemeMode, ThemeConfig } from '@amk/ui-shell-api';
|
|
7
|
+
import type { IStorageProvider, IThemeProvider } from '@amk/ui-shell-spi';
|
|
8
|
+
|
|
9
|
+
const THEME_STORAGE_KEY = 'ui-shell-theme';
|
|
10
|
+
|
|
11
|
+
export class DefaultThemeManager implements IThemeManager {
|
|
12
|
+
private currentTheme: ThemeMode = 'system';
|
|
13
|
+
private config: ThemeConfig;
|
|
14
|
+
private listeners: Set<(theme: ThemeMode) => void> = new Set();
|
|
15
|
+
private storageProvider: IStorageProvider;
|
|
16
|
+
private themeProvider: IThemeProvider;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
config: ThemeConfig = { mode: 'system' },
|
|
20
|
+
storageProvider: IStorageProvider,
|
|
21
|
+
themeProvider: IThemeProvider
|
|
22
|
+
) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.storageProvider = storageProvider;
|
|
25
|
+
this.themeProvider = themeProvider;
|
|
26
|
+
this.currentTheme = config.mode || this.loadPersistedTheme();
|
|
27
|
+
this.setupSystemThemeListener();
|
|
28
|
+
this.applyTheme();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getTheme(): ThemeMode {
|
|
32
|
+
return this.currentTheme;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getResolvedTheme(): 'light' | 'dark' {
|
|
36
|
+
if (this.currentTheme === 'system') {
|
|
37
|
+
return this.themeProvider.getSystemPreference();
|
|
38
|
+
}
|
|
39
|
+
return this.currentTheme;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setTheme(mode: ThemeMode): void {
|
|
43
|
+
if (this.currentTheme === mode) return;
|
|
44
|
+
|
|
45
|
+
this.currentTheme = mode;
|
|
46
|
+
this.persistTheme(mode);
|
|
47
|
+
this.applyTheme();
|
|
48
|
+
this.notifyListeners();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
toggleTheme(): void {
|
|
52
|
+
const resolved = this.getResolvedTheme();
|
|
53
|
+
this.setTheme(resolved === 'light' ? 'dark' : 'light');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
subscribe(listener: (theme: ThemeMode) => void): () => void {
|
|
57
|
+
this.listeners.add(listener);
|
|
58
|
+
return () => this.listeners.delete(listener);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setCSSVariables(variables: Record<string, string>): void {
|
|
62
|
+
this.config.cssVariables = { ...this.config.cssVariables, ...variables };
|
|
63
|
+
this.applyTheme();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
destroy(): void {
|
|
67
|
+
this.themeProvider.destroy();
|
|
68
|
+
this.listeners.clear();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private setupSystemThemeListener(): void {
|
|
72
|
+
this.themeProvider.onSystemThemeChange(() => {
|
|
73
|
+
if (this.currentTheme === 'system') {
|
|
74
|
+
this.applyTheme();
|
|
75
|
+
this.notifyListeners();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private applyTheme(): void {
|
|
81
|
+
this.themeProvider.applyTheme(this.currentTheme, this.config);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private loadPersistedTheme(): ThemeMode {
|
|
85
|
+
const stored = this.storageProvider.getItem(THEME_STORAGE_KEY);
|
|
86
|
+
if (stored === 'light' || stored === 'dark' || stored === 'system') {
|
|
87
|
+
return stored;
|
|
88
|
+
}
|
|
89
|
+
return 'system';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private persistTheme(mode: ThemeMode): void {
|
|
93
|
+
this.storageProvider.setItem(THEME_STORAGE_KEY, mode);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private notifyListeners(): void {
|
|
97
|
+
this.listeners.forEach(listener => listener(this.currentTheme));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Implementations
|
|
3
|
+
* Export all core implementation classes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './DefaultShell';
|
|
7
|
+
export * from './DefaultThemeManager';
|
|
8
|
+
export * from './DefaultFeatureManager';
|
|
9
|
+
export * from './DefaultLayoutManager';
|
|
10
|
+
export * from './DefaultRouter';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Shell Implementation
|
|
3
|
+
* Concrete implementations of UI Shell interfaces
|
|
4
|
+
*
|
|
5
|
+
* This package provides default implementations that can be used directly
|
|
6
|
+
* or extended for custom behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const version = '0.1.0';
|
|
10
|
+
|
|
11
|
+
// Export core implementations
|
|
12
|
+
export * from './core';
|
|
13
|
+
|
|
14
|
+
// Export providers
|
|
15
|
+
export * from './providers';
|
|
16
|
+
|
|
17
|
+
// Export utilities
|
|
18
|
+
export * from './utils';
|
|
19
|
+
|
|
20
|
+
// Export plugin integration
|
|
21
|
+
export * from './plugin';
|
|
22
|
+
|
|
23
|
+
// Factory functions
|
|
24
|
+
import { DefaultShell } from './core/DefaultShell';
|
|
25
|
+
import { DefaultLayoutManager } from './core/DefaultLayoutManager';
|
|
26
|
+
import type { ShellConfig } from '@amk/ui-shell-api';
|
|
27
|
+
|
|
28
|
+
export function createShell(config?: ShellConfig): DefaultShell {
|
|
29
|
+
return new DefaultShell(config);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createLayoutManager(): DefaultLayoutManager {
|
|
33
|
+
return new DefaultLayoutManager();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Singleton pattern support
|
|
37
|
+
let globalShell: DefaultShell | null = null;
|
|
38
|
+
|
|
39
|
+
export function getShell(config?: ShellConfig): DefaultShell {
|
|
40
|
+
if (!globalShell) {
|
|
41
|
+
globalShell = createShell(config);
|
|
42
|
+
}
|
|
43
|
+
return globalShell;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resetShell(): void {
|
|
47
|
+
if (globalShell) {
|
|
48
|
+
globalShell.destroy();
|
|
49
|
+
globalShell = null;
|
|
50
|
+
}
|
|
51
|
+
}
|