@akinon/ui-shell-dev 1.0.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.
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Akinon UI Shell Development</title>
8
+ <!--SHELL_CONFIG-->
9
+ <style>
10
+ body {
11
+ margin: 0;
12
+ font-family: 'Jost Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
13
+ -webkit-font-smoothing: antialiased;
14
+ -moz-osx-font-smoothing: grayscale;
15
+ }
16
+
17
+ #root {
18
+ width: 100vw;
19
+ height: 100vh;
20
+ overflow: hidden;
21
+ }
22
+
23
+ * {
24
+ box-sizing: border-box;
25
+ }
26
+ </style>
27
+ <script type="module" crossorigin src="/assets/index-DUUd718F.js"></script>
28
+ <link rel="stylesheet" crossorigin href="/assets/index-CuIRnKVE.css">
29
+ </head>
30
+ <body>
31
+ <div id="root"></div>
32
+ </body>
33
+ </html>
package/index.html ADDED
@@ -0,0 +1,32 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Akinon UI Shell Development</title>
8
+ <!--SHELL_CONFIG-->
9
+ <style>
10
+ body {
11
+ margin: 0;
12
+ font-family: 'Jost Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
13
+ -webkit-font-smoothing: antialiased;
14
+ -moz-osx-font-smoothing: grayscale;
15
+ }
16
+
17
+ #root {
18
+ width: 100vw;
19
+ height: 100vh;
20
+ overflow: hidden;
21
+ }
22
+
23
+ * {
24
+ box-sizing: border-box;
25
+ }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div id="root"></div>
30
+ <script type="module" src="/src/main.tsx"></script>
31
+ </body>
32
+ </html>
package/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { ShellDevServer } from './src/server/dev-server.mjs';
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@akinon/ui-shell-dev",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Development shell application for Akinon UI Protocol plugins",
6
+ "type": "module",
7
+ "main": "index.mjs",
8
+ "files": [
9
+ "dist",
10
+ "src",
11
+ "index.html",
12
+ "index.mjs",
13
+ "vite.config.ts",
14
+ "tsconfig.json"
15
+ ],
16
+ "dependencies": {
17
+ "@akinon/ui-system": "^1.0.0",
18
+ "@akinon/ui-layout": "^1.0.0",
19
+ "@akinon/ui-menu": "^1.0.0",
20
+ "@akinon/ui-breadcrumb": "^1.0.0",
21
+ "@akinon/ui-button": "^1.0.0",
22
+ "@akinon/icons": "^1.0.0",
23
+ "@akinon/fonts-jost-variable": "^1.0.0",
24
+ "react": "^18.2.0",
25
+ "react-dom": "^18.2.0",
26
+ "express": "^4.18.2",
27
+ "cors": "^2.8.5",
28
+ "vite": "^5.2.0"
29
+ },
30
+ "devDependencies": {
31
+ "@akinon/typescript-config": "^1.0.0",
32
+ "@types/react": "^18.2.66",
33
+ "@types/react-dom": "^18.2.22",
34
+ "@types/express": "^4.17.21",
35
+ "@types/cors": "^2.8.17",
36
+ "@vitejs/plugin-react-swc": "^3.7.2",
37
+ "typescript": "^5.2.2"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "scripts": {
43
+ "build": "vite build",
44
+ "dev": "vite",
45
+ "typecheck": "tsc --noEmit",
46
+ "lint": "eslint",
47
+ "clean": "rm -rf dist node_modules"
48
+ }
49
+ }
@@ -0,0 +1,64 @@
1
+ import { Layout } from '@akinon/ui-layout';
2
+ import React, { useState } from 'react';
3
+
4
+ import { getSidebarItems, getTheme } from '../themes';
5
+ import type { ShellConfig } from '../types';
6
+ import { Header } from './Header';
7
+ import { PluginLoader } from './PluginLoader';
8
+ import { Sidebar } from './Sidebar';
9
+
10
+ interface AppLayoutProps {
11
+ config: ShellConfig;
12
+ }
13
+
14
+ export const AppLayout: React.FC<AppLayoutProps> = ({ config }) => {
15
+ const [collapsed, setCollapsed] = useState(false);
16
+ const theme = getTheme(config.shell.theme);
17
+ const sidebarItems =
18
+ config.sidebar?.items || getSidebarItems(config.shell.theme);
19
+
20
+ const sidebarWidth = collapsed
21
+ ? theme.sidebar.collapsedWidth
22
+ : theme.sidebar.width;
23
+
24
+ return (
25
+ <Layout
26
+ style={{ minHeight: '100vh', backgroundColor: theme.colors.background }}
27
+ >
28
+ <Sidebar items={sidebarItems} theme={theme} collapsed={collapsed} />
29
+
30
+ <Layout
31
+ style={{ marginLeft: sidebarWidth, transition: 'margin-left 0.2s' }}
32
+ >
33
+ <Header
34
+ theme={theme}
35
+ title={config.shell.title}
36
+ collapsed={collapsed}
37
+ onToggleCollapse={() => setCollapsed(!collapsed)}
38
+ sidebarWidth={sidebarWidth}
39
+ />
40
+
41
+ <Layout.Content
42
+ style={{
43
+ marginTop: theme.header.height,
44
+ padding: '24px',
45
+ minHeight: `calc(100vh - ${theme.header.height}px)`,
46
+ backgroundColor: theme.colors.background
47
+ }}
48
+ >
49
+ <div
50
+ style={{
51
+ backgroundColor: 'white',
52
+ borderRadius: '8px',
53
+ height: 'calc(100vh - 112px)',
54
+ overflow: 'hidden',
55
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
56
+ }}
57
+ >
58
+ <PluginLoader config={config} />
59
+ </div>
60
+ </Layout.Content>
61
+ </Layout>
62
+ </Layout>
63
+ );
64
+ };
@@ -0,0 +1,66 @@
1
+ import { Button } from '@akinon/ui-button';
2
+ import { Layout } from '@akinon/ui-layout';
3
+ import React from 'react';
4
+
5
+ import type { ThemeConfig } from '../types';
6
+
7
+ interface HeaderProps {
8
+ theme: ThemeConfig;
9
+ title: string;
10
+ collapsed: boolean;
11
+ onToggleCollapse: () => void;
12
+ sidebarWidth: number;
13
+ }
14
+
15
+ export const Header: React.FC<HeaderProps> = ({
16
+ theme,
17
+ title,
18
+ collapsed,
19
+ onToggleCollapse,
20
+ sidebarWidth
21
+ }) => {
22
+ return (
23
+ <Layout.Header
24
+ style={{
25
+ backgroundColor: theme.colors.header,
26
+ padding: '0 24px',
27
+ height: theme.header.height,
28
+ lineHeight: `${theme.header.height}px`,
29
+ position: 'fixed',
30
+ top: 0,
31
+ right: 0,
32
+ left: sidebarWidth,
33
+ zIndex: 100,
34
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
35
+ display: 'flex',
36
+ alignItems: 'center',
37
+ justifyContent: 'space-between',
38
+ transition: 'left 0.2s'
39
+ }}
40
+ >
41
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
42
+ <Button
43
+ type="text"
44
+ onClick={onToggleCollapse}
45
+ style={{ fontSize: '16px' }}
46
+ >
47
+ {collapsed ? 'โ˜ฐ' : 'โฌ…'}
48
+ </Button>
49
+ <h1 style={{ margin: 0, fontSize: '18px', fontWeight: '500' }}>
50
+ {title}
51
+ </h1>
52
+ </div>
53
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
54
+ <span style={{ color: '#666' }}>Development Mode</span>
55
+ <div
56
+ style={{
57
+ width: 8,
58
+ height: 8,
59
+ borderRadius: '50%',
60
+ backgroundColor: '#52c41a'
61
+ }}
62
+ />
63
+ </div>
64
+ </Layout.Header>
65
+ );
66
+ };
@@ -0,0 +1,112 @@
1
+ import React, { useEffect, useState } from 'react';
2
+
3
+ import type { ShellConfig } from '../types';
4
+
5
+ interface PluginLoaderProps {
6
+ config: ShellConfig;
7
+ }
8
+
9
+ export const PluginLoader: React.FC<PluginLoaderProps> = ({ config }) => {
10
+ const [isLoading, setIsLoading] = useState(true);
11
+ const [error, setError] = useState<string | null>(null);
12
+
13
+ useEffect(() => {
14
+ const timer = setTimeout(() => {
15
+ setIsLoading(false);
16
+ }, 1000);
17
+
18
+ return () => clearTimeout(timer);
19
+ }, []);
20
+
21
+ const handleIframeLoad = () => {
22
+ setIsLoading(false);
23
+ setError(null);
24
+ };
25
+
26
+ const handleIframeError = () => {
27
+ setIsLoading(false);
28
+ setError('Failed to load plugin. Make sure your plugin is running.');
29
+ };
30
+
31
+ if (error) {
32
+ return (
33
+ <div
34
+ style={{
35
+ padding: '48px',
36
+ textAlign: 'center',
37
+ backgroundColor: '#fff',
38
+ borderRadius: '8px',
39
+ margin: '24px',
40
+ border: '1px solid #f0f0f0'
41
+ }}
42
+ >
43
+ <div style={{ fontSize: '48px', marginBottom: '16px' }}>โš ๏ธ</div>
44
+ <h3 style={{ margin: '0 0 8px 0', color: '#ff4d4f' }}>
45
+ Plugin Load Error
46
+ </h3>
47
+ <p style={{ margin: '0 0 16px 0', color: '#666' }}>{error}</p>
48
+ <p style={{ margin: 0, fontSize: '14px', color: '#999' }}>
49
+ Expected plugin URL: <code>{config.plugin.url}</code>
50
+ </p>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ return (
56
+ <div style={{ position: 'relative', width: '100%', height: '100%' }}>
57
+ {isLoading && (
58
+ <div
59
+ style={{
60
+ position: 'absolute',
61
+ top: 0,
62
+ left: 0,
63
+ right: 0,
64
+ bottom: 0,
65
+ display: 'flex',
66
+ alignItems: 'center',
67
+ justifyContent: 'center',
68
+ backgroundColor: '#fff',
69
+ zIndex: 10
70
+ }}
71
+ >
72
+ <div style={{ textAlign: 'center' }}>
73
+ <div
74
+ style={{
75
+ width: '32px',
76
+ height: '32px',
77
+ border: '3px solid #f3f3f3',
78
+ borderTop: '3px solid #1890ff',
79
+ borderRadius: '50%',
80
+ animation: 'spin 1s linear infinite',
81
+ margin: '0 auto 16px'
82
+ }}
83
+ />
84
+ <p style={{ margin: 0, color: '#666' }}>Loading plugin...</p>
85
+ </div>
86
+ </div>
87
+ )}
88
+ <iframe
89
+ src={config.plugin.url}
90
+ style={{
91
+ width: '100%',
92
+ height: '100%',
93
+ border: 'none',
94
+ backgroundColor: 'white'
95
+ }}
96
+ onLoad={handleIframeLoad}
97
+ onError={handleIframeError}
98
+ title="Plugin Content"
99
+ />
100
+ <style>{`
101
+ @keyframes spin {
102
+ 0% {
103
+ transform: rotate(0deg);
104
+ }
105
+ 100% {
106
+ transform: rotate(360deg);
107
+ }
108
+ }
109
+ `}</style>
110
+ </div>
111
+ );
112
+ };
@@ -0,0 +1,74 @@
1
+ import { Layout } from '@akinon/ui-layout';
2
+ import { Menu } from '@akinon/ui-menu';
3
+ import React from 'react';
4
+
5
+ import type { SidebarItem, ThemeConfig } from '../types';
6
+
7
+ interface SidebarProps {
8
+ items: SidebarItem[];
9
+ theme: ThemeConfig;
10
+ collapsed?: boolean;
11
+ }
12
+
13
+ export const Sidebar: React.FC<SidebarProps> = ({
14
+ items,
15
+ theme,
16
+ collapsed = false
17
+ }) => {
18
+ const menuItems = items.map(item => ({
19
+ key: item.key,
20
+ label: item.label,
21
+ icon: item.icon,
22
+ children: item.children?.map(child => ({
23
+ key: child.key,
24
+ label: child.label
25
+ }))
26
+ }));
27
+
28
+ const selectedKeys = items.filter(item => item.active).map(item => item.key);
29
+
30
+ return (
31
+ <Layout.Sider
32
+ width={theme.sidebar.width}
33
+ collapsedWidth={theme.sidebar.collapsedWidth}
34
+ collapsed={collapsed}
35
+ style={{
36
+ backgroundColor: theme.colors.sidebar,
37
+ height: '100vh',
38
+ position: 'fixed',
39
+ left: 0,
40
+ top: 0,
41
+ bottom: 0,
42
+ zIndex: 200
43
+ }}
44
+ >
45
+ <div
46
+ style={{
47
+ height: theme.header.height,
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
52
+ color: 'white',
53
+ fontSize: '18px',
54
+ fontWeight: 'bold'
55
+ }}
56
+ >
57
+ {collapsed ? 'A' : 'Akinon'}
58
+ </div>
59
+ <Menu
60
+ theme="dark"
61
+ mode="inline"
62
+ selectedKeys={selectedKeys}
63
+ defaultOpenKeys={items
64
+ .filter(item => item.children)
65
+ .map(item => item.key)}
66
+ items={menuItems}
67
+ style={{
68
+ backgroundColor: 'transparent',
69
+ border: 'none'
70
+ }}
71
+ />
72
+ </Layout.Sider>
73
+ );
74
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,4 @@
1
+ export { ShellDevServer } from './server/dev-server';
2
+ export { ShellApp } from './shell-app';
3
+ export { getSidebarItems, getTheme } from './themes';
4
+ export type { ShellConfig, SidebarItem, ThemeConfig } from './types';
package/src/main.tsx ADDED
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+
4
+ import { ShellApp } from './shell-app';
5
+ import type { ShellConfig } from './types';
6
+
7
+ // Get configuration injected by the server
8
+ const config: ShellConfig = (
9
+ window as unknown as { __SHELL_CONFIG__?: ShellConfig }
10
+ ).__SHELL_CONFIG__ || {
11
+ plugin: {
12
+ url: 'http://localhost:4002',
13
+ path: './src'
14
+ },
15
+ shell: {
16
+ port: 4000,
17
+ theme: 'omnitron',
18
+ title: 'Development Shell'
19
+ },
20
+ sidebar: {
21
+ items: []
22
+ }
23
+ };
24
+
25
+ ReactDOM.createRoot(document.getElementById('root')!).render(
26
+ <React.StrictMode>
27
+ <ShellApp config={config} />
28
+ </React.StrictMode>
29
+ );
@@ -0,0 +1,193 @@
1
+ import cors from 'cors';
2
+ import express from 'express';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { createServer as createViteServer } from 'vite';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ export class ShellDevServer {
12
+ constructor(config) {
13
+ this.app = express();
14
+ this.config = config;
15
+ this.viteServer = null;
16
+ this.setupMiddleware();
17
+ }
18
+
19
+ setupMiddleware() {
20
+ // Enable CORS for plugin communication
21
+ this.app.use(
22
+ cors({
23
+ origin: true,
24
+ credentials: true
25
+ })
26
+ );
27
+
28
+ // Parse JSON bodies
29
+ this.app.use(express.json());
30
+
31
+ // Serve static assets
32
+ this.app.use('/assets', express.static(path.join(__dirname, '../assets')));
33
+ }
34
+
35
+ async start() {
36
+ try {
37
+ console.log('๐Ÿ”ง Starting shell server initialization...');
38
+
39
+ const shellRoot = path.resolve(__dirname, '../..');
40
+ const distPath = path.resolve(shellRoot, 'dist');
41
+ // Only use production mode if explicitly set via env var
42
+ const isProduction = process.env.NODE_ENV === 'production' && fs.existsSync(distPath);
43
+
44
+ console.log(`๐Ÿ“ Shell root: ${shellRoot}`);
45
+ console.log(`๐Ÿญ Production mode: ${isProduction}`);
46
+
47
+ // API endpoints
48
+ this.setupApiRoutes();
49
+
50
+ if (isProduction) {
51
+ // Production: serve built assets
52
+ this.app.use('/assets', express.static(path.join(distPath, 'assets')));
53
+
54
+ this.app.get('*', (req, res, next) => {
55
+ if (req.path.startsWith('/api')) {
56
+ return next();
57
+ }
58
+
59
+ console.log(`๐Ÿ“„ Serving production HTML for: ${req.url}`);
60
+
61
+ let template = fs.readFileSync(
62
+ path.resolve(distPath, 'index.html'),
63
+ 'utf-8'
64
+ );
65
+
66
+ // Inject config
67
+ template = template.replace(
68
+ '<!--SHELL_CONFIG-->',
69
+ `<script>window.__SHELL_CONFIG__ = ${JSON.stringify(this.config)};</script>`
70
+ );
71
+
72
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(template);
73
+ });
74
+ } else {
75
+ // Development: use Vite middleware
76
+ this.viteServer = await createViteServer({
77
+ root: shellRoot,
78
+ configFile: path.resolve(shellRoot, 'vite.config.ts'),
79
+ server: {
80
+ middlewareMode: true,
81
+ cors: true
82
+ },
83
+ appType: 'spa'
84
+ });
85
+
86
+ console.log('โœ… Vite server created successfully');
87
+
88
+ // Use Vite middleware for assets and HMR
89
+ this.app.use(this.viteServer.middlewares);
90
+
91
+ console.log('โœ… Vite middleware configured');
92
+
93
+ // Serve the shell app (only for HTML requests)
94
+ this.app.get('*', async (req, res, next) => {
95
+ if (req.path.startsWith('/api') || req.path.startsWith('/src') || req.path.includes('.')) {
96
+ return next();
97
+ }
98
+
99
+ try {
100
+ console.log(`๐Ÿ“„ Serving development HTML for: ${req.url}`);
101
+
102
+ let template = fs.readFileSync(
103
+ path.resolve(__dirname, '../../index.html'),
104
+ 'utf-8'
105
+ );
106
+
107
+ template = await this.viteServer.transformIndexHtml(
108
+ req.url,
109
+ template
110
+ );
111
+
112
+ // Inject config
113
+ template = template.replace(
114
+ '<!--SHELL_CONFIG-->',
115
+ `<script>window.__SHELL_CONFIG__ = ${JSON.stringify(this.config)};</script>`
116
+ );
117
+
118
+ console.log(`โœ… HTML processed successfully for ${req.url}`);
119
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(template);
120
+ } catch (error) {
121
+ console.error(`โŒ Error serving ${req.url}:`, error);
122
+ this.viteServer.ssrFixStacktrace(error);
123
+ next(error);
124
+ }
125
+ });
126
+ }
127
+
128
+ // Start server
129
+ const server = this.app.listen(this.config.shell.port, '0.0.0.0', () => {
130
+ console.log(
131
+ `๐Ÿš€ Shell development server running on http://localhost:${this.config.shell.port}`
132
+ );
133
+ console.log(`๐Ÿ“ฑ Plugin expected at: ${this.config.plugin.url}`);
134
+ console.log(`๐ŸŽจ Theme: ${this.config.shell.theme}`);
135
+ console.log(`๐Ÿ” Server bound to 0.0.0.0:${this.config.shell.port}`);
136
+ });
137
+
138
+ // Handle graceful shutdown
139
+ process.on('SIGTERM', () => {
140
+ console.log('๐Ÿ›‘ Shutting down shell server...');
141
+ server.close(() => {
142
+ if (this.viteServer) {
143
+ this.viteServer.close();
144
+ }
145
+ process.exit(0);
146
+ });
147
+ });
148
+
149
+ return server;
150
+ } catch (error) {
151
+ console.error('โŒ Failed to start shell server:', error);
152
+ throw error;
153
+ }
154
+ }
155
+
156
+ setupApiRoutes() {
157
+ // Health check
158
+ this.app.get('/api/health', (req, res) => {
159
+ console.log('๐Ÿฅ Health check accessed');
160
+ res.json({
161
+ status: 'ok',
162
+ config: this.config,
163
+ timestamp: new Date().toISOString()
164
+ });
165
+ });
166
+
167
+ // Simple test endpoint
168
+ this.app.get('/test', (req, res) => {
169
+ console.log('๐Ÿงช Test endpoint accessed');
170
+ res.send('Shell server is working!');
171
+ });
172
+
173
+ // Configuration endpoint
174
+ this.app.get('/api/config', (req, res) => {
175
+ res.json(this.config);
176
+ });
177
+
178
+ // Plugin proxy endpoint (for development)
179
+ this.app.get('/api/plugin/health', async (req, res) => {
180
+ try {
181
+ const response = await fetch(`${this.config.plugin.url}/api/health`);
182
+ const data = await response.json();
183
+ res.json({ plugin: data, shell: { status: 'ok' } });
184
+ } catch (error) {
185
+ res.status(502).json({
186
+ error: 'Plugin not available',
187
+ plugin_url: this.config.plugin.url,
188
+ message: error.message
189
+ });
190
+ }
191
+ });
192
+ }
193
+ }