@akinon/ui-shell-dev 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{u as n,c as u}from"./index-D6a6pO8M.js";n(function(e,t){t._reactRoot||(t._reactRoot=u.createRoot(t));var o=t._reactRoot;return o.render(e),function(){return new Promise(function(r){setTimeout(function(){o.unmount(),r()},0)})}});
1
+ import{u as n,c as u}from"./index-BLfOr6po.js";n(function(e,t){t._reactRoot||(t._reactRoot=u.createRoot(t));var o=t._reactRoot;return o.render(e),function(){return new Promise(function(r){setTimeout(function(){o.unmount(),r()},0)})}});
package/dist/index.html CHANGED
@@ -24,7 +24,7 @@
24
24
  box-sizing: border-box;
25
25
  }
26
26
  </style>
27
- <script type="module" crossorigin src="/assets/index-D6a6pO8M.js"></script>
27
+ <script type="module" crossorigin src="/assets/index-BLfOr6po.js"></script>
28
28
  <link rel="stylesheet" crossorigin href="/assets/index-CX4XmW02.css">
29
29
  </head>
30
30
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/ui-shell-dev",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "private": false,
5
5
  "description": "Development shell application for Akinon UI Protocol plugins",
6
6
  "type": "module",
@@ -22,10 +22,10 @@
22
22
  "react-router-dom": "^6.28.0",
23
23
  "vite": "*",
24
24
  "@akinon/icons": "1.1.0",
25
- "@akinon/app-shell": "1.4.0",
26
- "@akinon/ui-react": "1.3.0",
25
+ "@akinon/ui-react": "1.3.1",
26
+ "@akinon/ui-utils": "1.1.0",
27
27
  "@akinon/fonts-jost-variable": "2.1.0",
28
- "@akinon/ui-utils": "1.1.0"
28
+ "@akinon/app-shell": "1.4.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/cors": "^2.8.17",
@@ -1,6 +1,6 @@
1
1
  import { useAppShell } from '@akinon/app-shell';
2
- import { IMenuProps, Menu } from '@akinon/ui-react';
3
- import { useEffect, useState } from 'react';
2
+ import { IMenuProps, Menu, TMenuItemType } from '@akinon/ui-react';
3
+ import { useEffect, useMemo, useState } from 'react';
4
4
  import React from 'react';
5
5
  import { useLocation, useNavigate } from 'react-router-dom';
6
6
 
@@ -24,21 +24,112 @@ export const Navigation: React.FC = () => {
24
24
  configs as Map<number, unknown>
25
25
  );
26
26
  const pluginItems = getPluginNavigationItems(apps);
27
+ const navigationItems = useMemo(
28
+ () => [...items, ...fullpageItems, ...pluginItems, ...itemsBottom],
29
+ [items, fullpageItems, pluginItems, itemsBottom]
30
+ );
31
+
32
+ const { allItemKeys, childToParentKey } = useMemo(() => {
33
+ function collectKeys(items: TMenuItemType[] | undefined): string[] {
34
+ if (!items) return [];
35
+ const keys: string[] = [];
36
+ items.forEach(item => {
37
+ if ('key' in item && item.key) keys.push(item.key);
38
+ if ('children' in item && item.children) {
39
+ (item.children as TMenuItemType[]).forEach(child => {
40
+ if ('key' in child && child.key) keys.push(child.key);
41
+ });
42
+ }
43
+ });
44
+ return keys;
45
+ }
46
+
47
+ const keys = collectKeys(navigationItems);
48
+
49
+ const map = new Map<string, string>();
50
+ fullpageItems.forEach(parent => {
51
+ if ('children' in parent && parent.children) {
52
+ (parent.children as TMenuItemType[]).forEach(child => {
53
+ if ('key' in child && child.key && 'key' in parent && parent.key) {
54
+ map.set(child.key, parent.key);
55
+ }
56
+ });
57
+ }
58
+ });
59
+
60
+ return { allItemKeys: keys, childToParentKey: map };
61
+ }, [navigationItems, fullpageItems]);
62
+
63
+ const parentKeys = useMemo(() => {
64
+ const keys = new Set<string>();
65
+ fullpageItems.forEach(p => {
66
+ if (
67
+ 'key' in p &&
68
+ p.key &&
69
+ 'children' in p &&
70
+ Array.isArray(p.children) &&
71
+ (p.children as TMenuItemType[]).length > 0
72
+ ) {
73
+ keys.add(p.key as string);
74
+ }
75
+ });
76
+ return keys;
77
+ }, [fullpageItems]);
27
78
 
28
79
  useEffect(() => {
29
- setSelectedKey(location.pathname);
30
- }, [location]);
80
+ const pathname = location.pathname;
81
+ // Pick the longest menu key that is a prefix of the current path
82
+ const bestMatch = allItemKeys
83
+ .filter(key => typeof key === 'string' && pathname.startsWith(key))
84
+ .sort((a, b) => (b as string).length - (a as string).length)[0] as
85
+ | string
86
+ | undefined;
87
+
88
+ if (bestMatch) setSelectedKey(bestMatch);
89
+
90
+ const parentKey = bestMatch ? childToParentKey.get(bestMatch) : undefined;
91
+ // Auto open parent submenu when landing on a child route
92
+ if (parentKey) setOpenKeys([parentKey]);
93
+ else if (
94
+ // If landing on a parent path that has children, open it too
95
+ fullpageItems.some(p => {
96
+ const isSameKey = 'key' in p && p.key === bestMatch;
97
+ const hasChildren =
98
+ 'children' in p &&
99
+ Array.isArray(p.children) &&
100
+ (p.children as TMenuItemType[]).length > 0;
101
+ return isSameKey && hasChildren;
102
+ })
103
+ ) {
104
+ setOpenKeys([bestMatch as string]);
105
+ }
106
+ }, [location, allItemKeys, childToParentKey, fullpageItems]);
31
107
 
32
108
  const onClick: IMenuProps['onClick'] = e => {
33
- if (!e.key.includes('external')) {
34
- setOpenKeys([]);
109
+ const key = e.key;
110
+
111
+ // Toggle parent submenu open/close when clicking its title
112
+ if (parentKeys.has(key)) {
113
+ setOpenKeys(prev => (prev.includes(key) ? [] : [key]));
114
+ setSelectedKey(key);
115
+ return;
35
116
  }
36
117
 
37
- navigate(e.key);
118
+ const parentKey = childToParentKey.get(key);
119
+ if (parentKey) setOpenKeys([parentKey]);
120
+
121
+ if (!key.includes('external')) setOpenKeys([]);
122
+
123
+ navigate(key);
38
124
  };
39
125
 
40
- const onOpenChange = (openKeys: string[]) => {
41
- setOpenKeys([openKeys.pop() || '']);
126
+ const onOpenChange = (nextOpenKeys: string[]) => {
127
+ // Keep at most one submenu open; allow closing to none
128
+ if (nextOpenKeys.length === 0) {
129
+ setOpenKeys([]);
130
+ return;
131
+ }
132
+ setOpenKeys([nextOpenKeys[nextOpenKeys.length - 1]]);
42
133
  };
43
134
 
44
135
  return (
@@ -46,8 +137,9 @@ export const Navigation: React.FC = () => {
46
137
  <Menu
47
138
  mode="inline"
48
139
  selectedKeys={[selectedKey]}
49
- items={[...items, ...fullpageItems, ...pluginItems, ...itemsBottom]}
140
+ items={navigationItems}
50
141
  onClick={onClick}
142
+ triggerSubMenuAction="click"
51
143
  theme="dark"
52
144
  openKeys={openKeys}
53
145
  onOpenChange={onOpenChange}
@@ -2,14 +2,28 @@ import './pages.scss';
2
2
 
3
3
  import { AppRenderer } from '@akinon/app-shell';
4
4
  import React from 'react';
5
+ import { useLocation } from 'react-router-dom';
5
6
 
6
7
  export const PageExternal: React.FC<{ id: number; path?: string }> = ({
7
8
  id,
8
9
  path
9
10
  }) => {
11
+ const location = useLocation();
12
+ // Compute splat (unmatched) path after the configured base `path`.
13
+ // Example: base "/keywords" and current pathname "/external/app/keywords/5/edit" -> splat "/5/edit".
14
+ const basePath = path ?? '';
15
+ const pathname = location.pathname;
16
+ const indexOfBase = pathname.indexOf(basePath);
17
+ const splat =
18
+ indexOfBase >= 0 ? pathname.slice(indexOfBase + basePath.length) : '';
19
+ const resolvedPath = `${basePath}${splat}` || undefined;
20
+ const resolvedPathWithQuery = resolvedPath
21
+ ? `${resolvedPath}${location.search ?? ''}${location.hash ?? ''}`
22
+ : undefined;
23
+
10
24
  return (
11
25
  <div className="page-plugin-external">
12
- <AppRenderer id={id} path={path} />
26
+ <AppRenderer id={id} path={resolvedPathWithQuery} />
13
27
  </div>
14
28
  );
15
29
  };
package/src/routes.tsx CHANGED
@@ -16,25 +16,28 @@ export const Routes = () => {
16
16
  <Route key={app.id} path={`/external/${app.slug}`}>
17
17
  <Route index element={<PageExternal id={app.id} path={app.path} />} />
18
18
 
19
- {(configs.get(app.id) as unknown as { menu: unknown[] })?.menu?.map(
20
- sub => {
21
- const { path } = sub as { path: string };
22
- return (
23
- path !== '/' && (
24
- <Route
25
- key={path}
26
- path={`/external/${app.slug}${path}`}
27
- element={
28
- <PageExternal
29
- id={app.id}
30
- path={app.subSlug ? `${app.subSlug}${path}` : path}
31
- />
32
- }
33
- />
34
- )
35
- );
36
- }
37
- )}
19
+ {(
20
+ configs.get(app.id) as
21
+ | { menu?: Array<{ path: string }> }
22
+ | undefined
23
+ )?.menu?.map((sub: { path: string }) => {
24
+ return (
25
+ sub.path !== '/' && (
26
+ <Route
27
+ key={sub.path}
28
+ path={`/external/${app.slug}${sub.path}/*`}
29
+ element={
30
+ <PageExternal
31
+ id={app.id}
32
+ path={
33
+ app.subSlug ? `${app.subSlug}${sub.path}` : sub.path
34
+ }
35
+ />
36
+ }
37
+ />
38
+ )
39
+ );
40
+ })}
38
41
  </Route>
39
42
  ))}
40
43