@hamak/ui-shell-impl 0.4.6 → 0.4.16
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/package.json +8 -5
- package/.turbo/turbo-build.log +0 -1
- package/CHANGELOG.md +0 -41
- package/project.json +0 -24
- package/src/core/DefaultFeatureManager.ts +0 -101
- package/src/core/DefaultLayoutManager.ts +0 -74
- package/src/core/DefaultRouter.ts +0 -135
- package/src/core/DefaultShell.ts +0 -176
- package/src/core/DefaultThemeManager.ts +0 -99
- package/src/core/index.ts +0 -10
- package/src/index.ts +0 -51
- package/src/plugin/ShellPluginFactory.ts +0 -98
- package/src/plugin/index.ts +0 -6
- package/src/providers/CSSVariablesThemeProvider.ts +0 -60
- package/src/providers/HashRouterStrategy.ts +0 -71
- package/src/providers/HistoryRouterStrategy.ts +0 -78
- package/src/providers/LocalStorageProvider.ts +0 -53
- package/src/providers/MemoryStorageProvider.ts +0 -30
- package/src/providers/index.ts +0 -10
- package/src/utils/index.ts +0 -6
- package/src/utils/viewport-utils.ts +0 -64
- package/tsconfig.es2015.json +0 -24
- package/tsconfig.json +0 -20
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hamak/ui-shell-impl",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "UI Shell Implementation - Core UI shell functionality",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"sideEffects": false,
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
10
13
|
"repository": {
|
|
11
14
|
"type": "git",
|
|
12
15
|
"url": "https://github.com/amah/app-framework.git",
|
|
@@ -32,10 +35,10 @@
|
|
|
32
35
|
}
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
|
-
"@hamak/ui-shell-api": "
|
|
36
|
-
"@hamak/ui-shell-spi": "
|
|
37
|
-
"@hamak/microkernel-api": "
|
|
38
|
-
"@hamak/microkernel-spi": "
|
|
38
|
+
"@hamak/ui-shell-api": "*",
|
|
39
|
+
"@hamak/ui-shell-spi": "*",
|
|
40
|
+
"@hamak/microkernel-api": "*",
|
|
41
|
+
"@hamak/microkernel-spi": "*"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"typescript": "~5.4.0"
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
$ tsc -p tsconfig.json && tsc -p tsconfig.es2015.json
|
package/CHANGELOG.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
## 0.4.0 (2025-11-10)
|
|
2
|
-
|
|
3
|
-
### 🚀 Features
|
|
4
|
-
|
|
5
|
-
- implement notification plugin with UI and backend components ([c19ffcf](https://github.com/amah/app-framework/commit/c19ffcf))
|
|
6
|
-
- add ES2015 build support and fix TypeScript config for logging packages ([be5e45e](https://github.com/amah/app-framework/commit/be5e45e))
|
|
7
|
-
- complete logging system build and add optional console interception ([f390bc6](https://github.com/amah/app-framework/commit/f390bc6))
|
|
8
|
-
- implement core pluggable logging system (Phase 1) ([2abdc1a](https://github.com/amah/app-framework/commit/2abdc1a))
|
|
9
|
-
|
|
10
|
-
### 🩹 Fixes
|
|
11
|
-
|
|
12
|
-
- add notification packages to workspaces ([97a234d](https://github.com/amah/app-framework/commit/97a234d))
|
|
13
|
-
|
|
14
|
-
### ❤️ Thank You
|
|
15
|
-
|
|
16
|
-
- Amah
|
|
17
|
-
- Claude
|
|
18
|
-
|
|
19
|
-
## 0.3.0 (2025-11-06)
|
|
20
|
-
|
|
21
|
-
### 🚀 Features
|
|
22
|
-
|
|
23
|
-
- migrate from Turbo to Nx 22 with comprehensive monorepo setup ([e63801e](https://github.com/amah/app-framework/commit/e63801e))
|
|
24
|
-
- add Nx Release for automated dependency management ([01d474f](https://github.com/amah/app-framework/commit/01d474f))
|
|
25
|
-
- migrate from Turbo to Nx 22 monorepo orchestration ([d374271](https://github.com/amah/app-framework/commit/d374271))
|
|
26
|
-
- add configurable main padding and resizable sidebar to DashboardLayout ([c1d25bf](https://github.com/amah/app-framework/commit/c1d25bf))
|
|
27
|
-
- add debug logging and version management system ([ea514fc](https://github.com/amah/app-framework/commit/ea514fc))
|
|
28
|
-
- **ui-store:** add STORE_EXTENSIONS_TOKEN for DI-based middleware/reducer registration ([e855bdd](https://github.com/amah/app-framework/commit/e855bdd))
|
|
29
|
-
- Rename package scope from @amk to @hamak and configure npm publishing ([b6040b5](https://github.com/amah/app-framework/commit/b6040b5))
|
|
30
|
-
- Add hybrid local/CI-CD development workflow with bun link ([d09f528](https://github.com/amah/app-framework/commit/d09f528))
|
|
31
|
-
- Add Turborepo for intelligent build orchestration and fix test type errors ([ba41db8](https://github.com/amah/app-framework/commit/ba41db8))
|
|
32
|
-
- Add Redux store integration with ui-store package and demo ([e5aafa8](https://github.com/amah/app-framework/commit/e5aafa8))
|
|
33
|
-
|
|
34
|
-
### 🩹 Fixes
|
|
35
|
-
|
|
36
|
-
- move git config to top-level release.git in nx.json ([1bb2187](https://github.com/amah/app-framework/commit/1bb2187))
|
|
37
|
-
|
|
38
|
-
### ❤️ Thank You
|
|
39
|
-
|
|
40
|
-
- Amah
|
|
41
|
-
- Claude
|
package/project.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@hamak/ui-shell-impl",
|
|
3
|
-
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
-
"sourceRoot": "packages/ui-shell/ui-shell-impl/src",
|
|
5
|
-
"projectType": "library",
|
|
6
|
-
"targets": {
|
|
7
|
-
"build": {
|
|
8
|
-
"executor": "nx:run-commands",
|
|
9
|
-
"outputs": ["{projectRoot}/dist"],
|
|
10
|
-
"options": {
|
|
11
|
-
"command": "tsc -p tsconfig.json && tsc -p tsconfig.es2015.json",
|
|
12
|
-
"cwd": "{projectRoot}"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"clean": {
|
|
16
|
-
"executor": "nx:run-commands",
|
|
17
|
-
"options": {
|
|
18
|
-
"command": "rm -rf dist",
|
|
19
|
-
"cwd": "{projectRoot}"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
"tags": ["type:library", "scope:ui-shell"]
|
|
24
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default Feature Manager Implementation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { IFeatureManager } from '@hamak/ui-shell-api';
|
|
6
|
-
import type { FeatureFlags } from '@hamak/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
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default Layout Manager Implementation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { ILayoutManager } from '@hamak/ui-shell-api';
|
|
6
|
-
import type { LayoutSlot, LayoutArea } from '@hamak/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
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default Router Implementation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { IRouter } from '@hamak/ui-shell-api';
|
|
6
|
-
import type { RouteConfig, RouterOptions } from '@hamak/ui-shell-api';
|
|
7
|
-
import type { IRouterStrategy } from '@hamak/ui-shell-spi';
|
|
8
|
-
import type { NavigationGuard } from '@hamak/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
|
-
}
|
package/src/core/DefaultShell.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default Shell Implementation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { IShell, IThemeManager, IFeatureManager, IRouter } from '@hamak/ui-shell-api';
|
|
6
|
-
import type { ShellConfig, ShellContext, ShellEvent, ShellEventListener, ShellEventType } from '@hamak/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
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default Theme Manager Implementation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { IThemeManager } from '@hamak/ui-shell-api';
|
|
6
|
-
import type { ThemeMode, ThemeConfig } from '@hamak/ui-shell-api';
|
|
7
|
-
import type { IStorageProvider, IThemeProvider } from '@hamak/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
|
-
}
|
package/src/core/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
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.2.2';
|
|
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 '@hamak/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
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shell Plugin Factory
|
|
3
|
-
* Creates microkernel plugin for UI Shell integration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { PluginModule } from '@hamak/microkernel-spi';
|
|
7
|
-
import type { ActivateContext } from '@hamak/microkernel-api';
|
|
8
|
-
import type { ShellConfig } from '@hamak/ui-shell-api';
|
|
9
|
-
import {
|
|
10
|
-
SHELL_TOKEN,
|
|
11
|
-
THEME_MANAGER_TOKEN,
|
|
12
|
-
FEATURE_MANAGER_TOKEN,
|
|
13
|
-
LAYOUT_MANAGER_TOKEN,
|
|
14
|
-
ShellCommands,
|
|
15
|
-
ShellEvents,
|
|
16
|
-
} from '@hamak/ui-shell-api';
|
|
17
|
-
import { DefaultShell } from '../core/DefaultShell';
|
|
18
|
-
import { DefaultLayoutManager } from '../core/DefaultLayoutManager';
|
|
19
|
-
|
|
20
|
-
export function createShellPlugin(config?: ShellConfig): PluginModule {
|
|
21
|
-
let shell: DefaultShell;
|
|
22
|
-
let layoutManager: DefaultLayoutManager;
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
initialize(ctx) {
|
|
26
|
-
shell = new DefaultShell(config);
|
|
27
|
-
layoutManager = new DefaultLayoutManager();
|
|
28
|
-
|
|
29
|
-
// Provide shell services
|
|
30
|
-
ctx.provide({ provide: SHELL_TOKEN, useValue: shell });
|
|
31
|
-
ctx.provide({ provide: THEME_MANAGER_TOKEN, useValue: shell.getThemeManager() });
|
|
32
|
-
ctx.provide({ provide: FEATURE_MANAGER_TOKEN, useValue: shell.getFeatureManager() });
|
|
33
|
-
ctx.provide({ provide: LAYOUT_MANAGER_TOKEN, useValue: layoutManager });
|
|
34
|
-
|
|
35
|
-
// Register commands
|
|
36
|
-
ctx.commands.register(ShellCommands.SET_THEME, (mode: 'light' | 'dark' | 'system') => {
|
|
37
|
-
shell.getThemeManager().setTheme(mode);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
ctx.commands.register(ShellCommands.TOGGLE_THEME, () => {
|
|
41
|
-
shell.getThemeManager().toggleTheme();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
ctx.commands.register(ShellCommands.ENABLE_FEATURE, (key: string) => {
|
|
45
|
-
shell.getFeatureManager().enable(key);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
ctx.commands.register(ShellCommands.DISABLE_FEATURE, (key: string) => {
|
|
49
|
-
shell.getFeatureManager().disable(key);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
ctx.commands.register(ShellCommands.TOGGLE_FEATURE, (key: string) => {
|
|
53
|
-
shell.getFeatureManager().toggle(key);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
ctx.commands.register(ShellCommands.NAVIGATE, (path: string) => {
|
|
57
|
-
const router = shell.getRouter();
|
|
58
|
-
if (!router) {
|
|
59
|
-
console.warn('Router not initialized');
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
return router.push(path);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
ctx.commands.register(ShellCommands.NAVIGATE_BACK, () => {
|
|
66
|
-
const router = shell.getRouter();
|
|
67
|
-
if (!router) {
|
|
68
|
-
console.warn('Router not initialized');
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
router.back();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Forward shell events to microkernel hooks
|
|
75
|
-
shell.on('shell:ready', () => ctx.hooks.emit(ShellEvents.READY, {}));
|
|
76
|
-
shell.on('theme:changed', (event) => ctx.hooks.emit(ShellEvents.THEME_CHANGED, event.payload));
|
|
77
|
-
shell.on('feature:toggled', (event) => ctx.hooks.emit(ShellEvents.FEATURE_TOGGLED, event.payload));
|
|
78
|
-
shell.on('route:changed', (event) => ctx.hooks.emit(ShellEvents.ROUTE_CHANGED, event.payload));
|
|
79
|
-
shell.on('viewport:resized', (event) => ctx.hooks.emit(ShellEvents.VIEWPORT_RESIZED, event.payload));
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
async activate(ctx: ActivateContext) {
|
|
83
|
-
await shell.initialize();
|
|
84
|
-
const shellContext = shell.getContext();
|
|
85
|
-
ctx.hooks.emit('shell:activated', { context: shellContext });
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
deactivate() {
|
|
89
|
-
shell.destroy();
|
|
90
|
-
layoutManager.destroy();
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Helper functions
|
|
96
|
-
export function getShellFromContext(ctx: ActivateContext): DefaultShell {
|
|
97
|
-
return ctx.resolve<DefaultShell>(SHELL_TOKEN);
|
|
98
|
-
}
|
package/src/plugin/index.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CSS Variables Theme Provider
|
|
3
|
-
* Implements theming using CSS custom properties
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IThemeProvider } from '@hamak/ui-shell-spi';
|
|
7
|
-
import type { ThemeMode, ThemeConfig } from '@hamak/ui-shell-api';
|
|
8
|
-
|
|
9
|
-
export class CSSVariablesThemeProvider implements IThemeProvider {
|
|
10
|
-
private mediaQuery?: MediaQueryList;
|
|
11
|
-
private listeners: Set<(theme: 'light' | 'dark') => void> = new Set();
|
|
12
|
-
|
|
13
|
-
applyTheme(mode: ThemeMode, config: ThemeConfig): void {
|
|
14
|
-
if (typeof document === 'undefined') return;
|
|
15
|
-
|
|
16
|
-
const resolved = mode === 'system' ? this.getSystemPreference() : mode;
|
|
17
|
-
|
|
18
|
-
document.documentElement.setAttribute('data-theme', resolved);
|
|
19
|
-
document.documentElement.style.colorScheme = resolved;
|
|
20
|
-
|
|
21
|
-
// Apply custom CSS variables
|
|
22
|
-
if (config.cssVariables) {
|
|
23
|
-
Object.entries(config.cssVariables).forEach(([key, value]) => {
|
|
24
|
-
document.documentElement.style.setProperty(key, value);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
getSystemPreference(): 'light' | 'dark' {
|
|
30
|
-
if (typeof window === 'undefined') return 'light';
|
|
31
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
onSystemThemeChange(callback: (theme: 'light' | 'dark') => void): () => void {
|
|
35
|
-
if (typeof window === 'undefined') {
|
|
36
|
-
return () => {};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
this.listeners.add(callback);
|
|
40
|
-
|
|
41
|
-
if (!this.mediaQuery) {
|
|
42
|
-
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
43
|
-
this.mediaQuery.addEventListener('change', this.handleMediaQueryChange);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return () => {
|
|
47
|
-
this.listeners.delete(callback);
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
destroy(): void {
|
|
52
|
-
this.mediaQuery?.removeEventListener('change', this.handleMediaQueryChange);
|
|
53
|
-
this.listeners.clear();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private handleMediaQueryChange = (): void => {
|
|
57
|
-
const theme = this.getSystemPreference();
|
|
58
|
-
this.listeners.forEach(listener => listener(theme));
|
|
59
|
-
};
|
|
60
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hash Router Strategy
|
|
3
|
-
* Uses URL hash for routing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IRouterStrategy } from '@hamak/ui-shell-spi';
|
|
7
|
-
|
|
8
|
-
export class HashRouterStrategy implements IRouterStrategy {
|
|
9
|
-
private listeners: Set<(path: string) => void> = new Set();
|
|
10
|
-
|
|
11
|
-
constructor() {
|
|
12
|
-
this.setupListener();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
push(path: string): void {
|
|
16
|
-
if (typeof window === 'undefined') return;
|
|
17
|
-
|
|
18
|
-
window.location.hash = path;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
replace(path: string): void {
|
|
22
|
-
if (typeof window === 'undefined') return;
|
|
23
|
-
|
|
24
|
-
window.location.replace(`#${path}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
back(): void {
|
|
28
|
-
if (typeof window !== 'undefined') {
|
|
29
|
-
window.history.back();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
forward(): void {
|
|
34
|
-
if (typeof window !== 'undefined') {
|
|
35
|
-
window.history.forward();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getCurrentPath(): string {
|
|
40
|
-
if (typeof window === 'undefined') return '/';
|
|
41
|
-
|
|
42
|
-
return window.location.hash.slice(1) || '/';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
listen(callback: (path: string) => void): () => void {
|
|
46
|
-
this.listeners.add(callback);
|
|
47
|
-
return () => this.listeners.delete(callback);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
destroy(): void {
|
|
51
|
-
if (typeof window !== 'undefined') {
|
|
52
|
-
window.removeEventListener('hashchange', this.handleHashChange);
|
|
53
|
-
}
|
|
54
|
-
this.listeners.clear();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private setupListener(): void {
|
|
58
|
-
if (typeof window !== 'undefined') {
|
|
59
|
-
window.addEventListener('hashchange', this.handleHashChange);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private handleHashChange = (): void => {
|
|
64
|
-
const path = this.getCurrentPath();
|
|
65
|
-
this.notifyListeners(path);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
private notifyListeners(path: string): void {
|
|
69
|
-
this.listeners.forEach(listener => listener(path));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* History Router Strategy
|
|
3
|
-
* Uses browser History API for routing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IRouterStrategy } from '@hamak/ui-shell-spi';
|
|
7
|
-
|
|
8
|
-
export class HistoryRouterStrategy implements IRouterStrategy {
|
|
9
|
-
private base: string;
|
|
10
|
-
private listeners: Set<(path: string) => void> = new Set();
|
|
11
|
-
|
|
12
|
-
constructor(base: string = '/') {
|
|
13
|
-
this.base = base;
|
|
14
|
-
this.setupListener();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
push(path: string): void {
|
|
18
|
-
if (typeof window === 'undefined') return;
|
|
19
|
-
|
|
20
|
-
const fullPath = this.base + path;
|
|
21
|
-
window.history.pushState({}, '', fullPath);
|
|
22
|
-
this.notifyListeners(path);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
replace(path: string): void {
|
|
26
|
-
if (typeof window === 'undefined') return;
|
|
27
|
-
|
|
28
|
-
const fullPath = this.base + path;
|
|
29
|
-
window.history.replaceState({}, '', fullPath);
|
|
30
|
-
this.notifyListeners(path);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
back(): void {
|
|
34
|
-
if (typeof window !== 'undefined') {
|
|
35
|
-
window.history.back();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
forward(): void {
|
|
40
|
-
if (typeof window !== 'undefined') {
|
|
41
|
-
window.history.forward();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
getCurrentPath(): string {
|
|
46
|
-
if (typeof window === 'undefined') return '/';
|
|
47
|
-
|
|
48
|
-
const path = window.location.pathname;
|
|
49
|
-
return path.startsWith(this.base) ? path.slice(this.base.length) : path;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
listen(callback: (path: string) => void): () => void {
|
|
53
|
-
this.listeners.add(callback);
|
|
54
|
-
return () => this.listeners.delete(callback);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
destroy(): void {
|
|
58
|
-
if (typeof window !== 'undefined') {
|
|
59
|
-
window.removeEventListener('popstate', this.handlePopState);
|
|
60
|
-
}
|
|
61
|
-
this.listeners.clear();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private setupListener(): void {
|
|
65
|
-
if (typeof window !== 'undefined') {
|
|
66
|
-
window.addEventListener('popstate', this.handlePopState);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private handlePopState = (): void => {
|
|
71
|
-
const path = this.getCurrentPath();
|
|
72
|
-
this.notifyListeners(path);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
private notifyListeners(path: string): void {
|
|
76
|
-
this.listeners.forEach(listener => listener(path));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LocalStorage Provider
|
|
3
|
-
* Implementation using browser localStorage
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IStorageProvider } from '@hamak/ui-shell-spi';
|
|
7
|
-
|
|
8
|
-
export class LocalStorageProvider implements IStorageProvider {
|
|
9
|
-
getItem(key: string): string | null {
|
|
10
|
-
if (!this.isAvailable()) return null;
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
return localStorage.getItem(key);
|
|
14
|
-
} catch (e) {
|
|
15
|
-
console.warn('Failed to get item from localStorage:', e);
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
setItem(key: string, value: string): void {
|
|
21
|
-
if (!this.isAvailable()) return;
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
localStorage.setItem(key, value);
|
|
25
|
-
} catch (e) {
|
|
26
|
-
console.warn('Failed to set item in localStorage:', e);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
removeItem(key: string): void {
|
|
31
|
-
if (!this.isAvailable()) return;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
localStorage.removeItem(key);
|
|
35
|
-
} catch (e) {
|
|
36
|
-
console.warn('Failed to remove item from localStorage:', e);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
clear(): void {
|
|
41
|
-
if (!this.isAvailable()) return;
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
localStorage.clear();
|
|
45
|
-
} catch (e) {
|
|
46
|
-
console.warn('Failed to clear localStorage:', e);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
isAvailable(): boolean {
|
|
51
|
-
return typeof localStorage !== 'undefined';
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory Storage Provider
|
|
3
|
-
* In-memory storage for SSR or testing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IStorageProvider } from '@hamak/ui-shell-spi';
|
|
7
|
-
|
|
8
|
-
export class MemoryStorageProvider implements IStorageProvider {
|
|
9
|
-
private storage: Map<string, string> = new Map();
|
|
10
|
-
|
|
11
|
-
getItem(key: string): string | null {
|
|
12
|
-
return this.storage.get(key) || null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
setItem(key: string, value: string): void {
|
|
16
|
-
this.storage.set(key, value);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
removeItem(key: string): void {
|
|
20
|
-
this.storage.delete(key);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
clear(): void {
|
|
24
|
-
this.storage.clear();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
isAvailable(): boolean {
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
}
|
package/src/providers/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Providers
|
|
3
|
-
* Export all provider implementations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export * from './LocalStorageProvider';
|
|
7
|
-
export * from './MemoryStorageProvider';
|
|
8
|
-
export * from './CSSVariablesThemeProvider';
|
|
9
|
-
export * from './HistoryRouterStrategy';
|
|
10
|
-
export * from './HashRouterStrategy';
|
package/src/utils/index.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Viewport Utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { BreakpointName, Breakpoint } from '@hamak/ui-shell-api';
|
|
6
|
-
|
|
7
|
-
export const BREAKPOINTS: Record<BreakpointName, Breakpoint> = {
|
|
8
|
-
xs: { name: 'xs', minWidth: 0, maxWidth: 639 },
|
|
9
|
-
sm: { name: 'sm', minWidth: 640, maxWidth: 767 },
|
|
10
|
-
md: { name: 'md', minWidth: 768, maxWidth: 1023 },
|
|
11
|
-
lg: { name: 'lg', minWidth: 1024, maxWidth: 1279 },
|
|
12
|
-
xl: { name: 'xl', minWidth: 1280, maxWidth: 1535 },
|
|
13
|
-
'2xl': { name: '2xl', minWidth: 1536 },
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export class ViewportUtils {
|
|
17
|
-
static getCurrentBreakpoint(): BreakpointName {
|
|
18
|
-
if (typeof window === 'undefined') return 'md';
|
|
19
|
-
|
|
20
|
-
const width = window.innerWidth;
|
|
21
|
-
|
|
22
|
-
for (const [name, breakpoint] of Object.entries(BREAKPOINTS)) {
|
|
23
|
-
if (width >= breakpoint.minWidth && (!breakpoint.maxWidth || width <= breakpoint.maxWidth)) {
|
|
24
|
-
return name as BreakpointName;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return 'md';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
static matchesBreakpoint(breakpoint: BreakpointName): boolean {
|
|
32
|
-
if (typeof window === 'undefined') return false;
|
|
33
|
-
|
|
34
|
-
const width = window.innerWidth;
|
|
35
|
-
const bp = BREAKPOINTS[breakpoint];
|
|
36
|
-
|
|
37
|
-
return width >= bp.minWidth && (!bp.maxWidth || width <= bp.maxWidth);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
static isMinBreakpoint(breakpoint: BreakpointName): boolean {
|
|
41
|
-
if (typeof window === 'undefined') return false;
|
|
42
|
-
|
|
43
|
-
const width = window.innerWidth;
|
|
44
|
-
const bp = BREAKPOINTS[breakpoint];
|
|
45
|
-
|
|
46
|
-
return width >= bp.minWidth;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
static getDimensions(): { width: number; height: number } {
|
|
50
|
-
if (typeof window === 'undefined') {
|
|
51
|
-
return { width: 0, height: 0 };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
width: window.innerWidth,
|
|
56
|
-
height: window.innerHeight,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
static isTouchDevice(): boolean {
|
|
61
|
-
if (typeof window === 'undefined') return false;
|
|
62
|
-
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
63
|
-
}
|
|
64
|
-
}
|
package/tsconfig.es2015.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "./tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"target": "ES2015",
|
|
5
|
-
"lib": [
|
|
6
|
-
"ES2015",
|
|
7
|
-
"DOM",
|
|
8
|
-
"DOM.Iterable"
|
|
9
|
-
],
|
|
10
|
-
"outDir": "dist/es2015",
|
|
11
|
-
"declaration": false,
|
|
12
|
-
"declarationMap": false,
|
|
13
|
-
"sourceMap": false,
|
|
14
|
-
"downlevelIteration": true,
|
|
15
|
-
"composite": false
|
|
16
|
-
},
|
|
17
|
-
"include": [
|
|
18
|
-
"src/**/*"
|
|
19
|
-
],
|
|
20
|
-
"exclude": [
|
|
21
|
-
"node_modules",
|
|
22
|
-
"dist"
|
|
23
|
-
]
|
|
24
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ES2022",
|
|
5
|
-
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"declarationMap": true,
|
|
8
|
-
"outDir": "dist",
|
|
9
|
-
"rootDir": "src",
|
|
10
|
-
"strict": true,
|
|
11
|
-
"esModuleInterop": true,
|
|
12
|
-
"skipLibCheck": true,
|
|
13
|
-
"forceConsistentCasingInFileNames": true,
|
|
14
|
-
"moduleResolution": "bundler",
|
|
15
|
-
"resolveJsonModule": true,
|
|
16
|
-
"allowSyntheticDefaultImports": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist"]
|
|
20
|
-
}
|