@ossy/app 1.40.3 → 3.0.2
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/README.md +6 -0
- package/cli/get-platform-files.task.js +4 -6
- package/cli/manifest-plugin.js +111 -36
- package/package.json +23 -14
- package/runtime/merge-shell-slots.js +148 -0
- package/runtime/page-runtime.js +32 -16
- package/runtime/resolve-app-slots.js +48 -11
- package/src/en.translations.json +8 -0
- package/src/manifest/build-actions-schema.js +1 -36
- package/src/manifest/build-capabilities.js +1 -190
- package/src/manifest/build-manifest-summary.js +1 -121
- package/src/manifest/discover-package-definitions.js +1 -100
- package/src/manifest/resolve-page-layout.js +51 -0
- package/src/manifest/serialize-package-definition.js +1 -30
- package/src/shell/App.jsx +13 -7
- package/src/shell/AppSettings.jsx +6 -0
- package/src/shell/DevPagesPanel.jsx +1 -1
- package/src/shell/HeaderAuthActions.jsx +49 -0
- package/src/shell/LanguageList.jsx +83 -0
- package/src/shell/LanguageSelect.jsx +73 -0
- package/src/shell/ThemeEditor.jsx +2 -2
- package/src/shell/ThemeSelect.jsx +101 -0
- package/src/shell/buildSidebarNav.js +86 -0
- package/src/shell/index.js +7 -0
- package/src/shell/languageCode.js +31 -0
- package/src/shell/resolvePackageHomePageId.js +14 -0
- package/src/shell/useCompactShellLayout.js +24 -0
- package/src/shell-registry/blank.layout.jsx +8 -0
- package/src/shell-registry/default.layout.jsx +101 -0
- package/src/shell-registry/footer-default.component.jsx +39 -0
- package/src/shell-registry/head-default.component.jsx +17 -0
- package/src/shell-registry/header-default.component.jsx +50 -0
- package/src/shell-registry/logo-logomark.component.jsx +8 -0
- package/src/shell-registry/logo-logotype.component.jsx +15 -0
- package/src/shell-registry/minimal.layout.jsx +58 -0
- package/src/shell-registry/sidebar-default.component.jsx +281 -0
- package/src/sv.translations.json +8 -0
- package/src/manifest/action-id-to-tool-name.js +0 -29
- package/src/manifest/build-action-input-schema.js +0 -220
- package/src/manifest/template-to-json-schema.js +0 -103
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
PageSection,
|
|
4
|
+
View,
|
|
5
|
+
Slot,
|
|
6
|
+
} from '@ossy/design-system'
|
|
7
|
+
import { useCompactShellLayout, shellSlotUnset } from '@ossy/app/shell'
|
|
8
|
+
|
|
9
|
+
export const metadata = { id: '@ossy/app/layout/default' }
|
|
10
|
+
|
|
11
|
+
export const slots = {
|
|
12
|
+
'app:head': '@ossy/app/component/head/default',
|
|
13
|
+
'app:header': '@ossy/app/component/header/default',
|
|
14
|
+
'app:sidebar': '@ossy/app/component/sidebar/default',
|
|
15
|
+
'app:footer': '@ossy/app/component/footer/default',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function DefaultLayout ({
|
|
19
|
+
shellSlots = {},
|
|
20
|
+
maxWidth = 'xl',
|
|
21
|
+
}) {
|
|
22
|
+
const compact = useCompactShellLayout()
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
{!shellSlotUnset(shellSlots['app:head']) && <Slot view="app:head" />}
|
|
27
|
+
<View
|
|
28
|
+
surface="base"
|
|
29
|
+
data-ossy-app-shell
|
|
30
|
+
style={{
|
|
31
|
+
boxSizing: 'border-box',
|
|
32
|
+
height: '100%',
|
|
33
|
+
minWidth: 0,
|
|
34
|
+
padding: compact ? 0 : 'var(--space-m)',
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<PageSection
|
|
38
|
+
maxWidth={maxWidth}
|
|
39
|
+
style={{ height: '100%', minWidth: 0, width: '100%', boxSizing: 'border-box' }}
|
|
40
|
+
>
|
|
41
|
+
{!shellSlotUnset(shellSlots['app:system-messages']) && (
|
|
42
|
+
<Slot view="app:system-messages" />
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
<View
|
|
46
|
+
layout="sidebar"
|
|
47
|
+
surface="primary"
|
|
48
|
+
roundness="m"
|
|
49
|
+
inset={compact ? 'none' : 's'}
|
|
50
|
+
gap="xs"
|
|
51
|
+
style={{
|
|
52
|
+
height: '100%',
|
|
53
|
+
minWidth: 0,
|
|
54
|
+
minHeight: 0,
|
|
55
|
+
...(compact ? { gap: '4px' } : {}),
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{!shellSlotUnset(shellSlots['app:sidebar']) && (
|
|
59
|
+
<Slot region="sidebar-primary" view="app:sidebar" />
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{!shellSlotUnset(shellSlots['app:header']) && (
|
|
63
|
+
<Slot region="header" view="app:header" />
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
<View
|
|
67
|
+
data-region="content"
|
|
68
|
+
data-scroll
|
|
69
|
+
gap={compact ? 'xxs' : 'xs'}
|
|
70
|
+
style={{
|
|
71
|
+
minHeight: 0,
|
|
72
|
+
minWidth: 0,
|
|
73
|
+
overflowX: 'hidden',
|
|
74
|
+
overflowY: 'auto',
|
|
75
|
+
display: 'flex',
|
|
76
|
+
flexDirection: 'column',
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<main
|
|
80
|
+
style={{
|
|
81
|
+
flex: '1 0 auto',
|
|
82
|
+
minHeight: '100%',
|
|
83
|
+
minWidth: 0,
|
|
84
|
+
width: '100%',
|
|
85
|
+
boxSizing: 'border-box',
|
|
86
|
+
display: 'flex',
|
|
87
|
+
flexDirection: 'column',
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<Slot view="app:content" />
|
|
91
|
+
</main>
|
|
92
|
+
{!shellSlotUnset(shellSlots['app:footer']) && (
|
|
93
|
+
<Slot view="app:footer" style={{ flexShrink: 0 }} />
|
|
94
|
+
)}
|
|
95
|
+
</View>
|
|
96
|
+
</View>
|
|
97
|
+
</PageSection>
|
|
98
|
+
</View>
|
|
99
|
+
</>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Slot } from '@ossy/design-system'
|
|
3
|
+
import { LanguageList } from '../shell/LanguageList.jsx'
|
|
4
|
+
|
|
5
|
+
export const metadata = { id: '@ossy/app/component/footer/default' }
|
|
6
|
+
|
|
7
|
+
/** TL/TR/BR/BL — smallest corners except BR, which matches the shell outer bottom-right (`--space-l`). */
|
|
8
|
+
const FOOTER_BORDER_RADIUS =
|
|
9
|
+
'var(--space-xs) var(--space-xs) var(--space-l) var(--space-xs)'
|
|
10
|
+
|
|
11
|
+
export default function DefaultFooter ({ surface, style, ...rest }) {
|
|
12
|
+
return (
|
|
13
|
+
<View
|
|
14
|
+
as="footer"
|
|
15
|
+
surface={surface ?? 'primary'}
|
|
16
|
+
inset="m"
|
|
17
|
+
style={{
|
|
18
|
+
flexShrink: 0,
|
|
19
|
+
minHeight: 'auto',
|
|
20
|
+
borderRadius: FOOTER_BORDER_RADIUS,
|
|
21
|
+
...style,
|
|
22
|
+
}}
|
|
23
|
+
{...rest}
|
|
24
|
+
>
|
|
25
|
+
<View
|
|
26
|
+
layout="row"
|
|
27
|
+
gap="l"
|
|
28
|
+
alignItems="flex-start"
|
|
29
|
+
justifyContent="flex-start"
|
|
30
|
+
style={{ minWidth: 0, width: '100%' }}
|
|
31
|
+
>
|
|
32
|
+
<View layout="column" style={{ minWidth: 0 }}>
|
|
33
|
+
<Slot view="@ossy/app/component/logo/logotype" />
|
|
34
|
+
</View>
|
|
35
|
+
<LanguageList />
|
|
36
|
+
</View>
|
|
37
|
+
</View>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default document head assets for platform shell layouts (React 19 hoisting).
|
|
5
|
+
*/
|
|
6
|
+
export function SiteDocumentHead () {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<link rel="apple-touch-icon" href="/logo192x192.png" />
|
|
10
|
+
<link rel="manifest" href="/manifest.json" />
|
|
11
|
+
</>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const metadata = { id: '@ossy/app/component/head/default' }
|
|
16
|
+
|
|
17
|
+
export default SiteDocumentHead
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { AppHeader, View } from '@ossy/design-system'
|
|
3
|
+
import { useCompactShellLayout } from '../shell/useCompactShellLayout.js'
|
|
4
|
+
import { HeaderAuthActions } from '../shell/HeaderAuthActions.jsx'
|
|
5
|
+
import { LanguageSelect } from '../shell/LanguageSelect.jsx'
|
|
6
|
+
import { ThemeSelect } from '../shell/ThemeSelect.jsx'
|
|
7
|
+
|
|
8
|
+
export const metadata = { id: '@ossy/app/component/header/default' }
|
|
9
|
+
|
|
10
|
+
/** TL/TR/BR/BL — smallest corners except TR, which matches sidebar outer top-left (`--space-l`). */
|
|
11
|
+
const HEADER_BORDER_RADIUS =
|
|
12
|
+
'var(--space-xs) var(--space-l) var(--space-xs) var(--space-xs)'
|
|
13
|
+
|
|
14
|
+
export default function DefaultHeader ({
|
|
15
|
+
compact: compactProp,
|
|
16
|
+
surface,
|
|
17
|
+
style,
|
|
18
|
+
inset,
|
|
19
|
+
...rest
|
|
20
|
+
}) {
|
|
21
|
+
const compactViewport = useCompactShellLayout()
|
|
22
|
+
const compact = compactProp ?? compactViewport
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<AppHeader
|
|
26
|
+
compact={compact}
|
|
27
|
+
inset={inset ?? 's'}
|
|
28
|
+
surface={surface ?? 'primary'}
|
|
29
|
+
style={{
|
|
30
|
+
borderRadius: HEADER_BORDER_RADIUS,
|
|
31
|
+
...style,
|
|
32
|
+
}}
|
|
33
|
+
{...rest}
|
|
34
|
+
far={(
|
|
35
|
+
<View
|
|
36
|
+
layout="row"
|
|
37
|
+
style={{
|
|
38
|
+
flexShrink: 0,
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
gap: compact ? 'var(--space-xs)' : 'var(--space-s)',
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<LanguageSelect />
|
|
44
|
+
<ThemeSelect compact={compact} />
|
|
45
|
+
<HeaderAuthActions compact={compact} />
|
|
46
|
+
</View>
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Icon } from '@ossy/design-system'
|
|
3
|
+
|
|
4
|
+
export const metadata = { id: '@ossy/app/component/logo/logomark' }
|
|
5
|
+
|
|
6
|
+
export default function DefaultLogoLogomark (props) {
|
|
7
|
+
return <Icon name="components" variant="components" size="m" {...props} />
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Icon, Text, View } from '@ossy/design-system'
|
|
3
|
+
|
|
4
|
+
export const metadata = { id: '@ossy/app/component/logo/logotype' }
|
|
5
|
+
|
|
6
|
+
export default function DefaultLogoLogotype ({ style, ...rest }) {
|
|
7
|
+
return (
|
|
8
|
+
<View layout="row" gap="xs" alignItems="center" style={style} {...rest}>
|
|
9
|
+
<Icon name="components" variant="components" size="m" />
|
|
10
|
+
<Text variant="heading-tertiary" as="span">
|
|
11
|
+
Demo
|
|
12
|
+
</Text>
|
|
13
|
+
</View>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { PageSection, View, Slot } from '@ossy/design-system'
|
|
3
|
+
import { shellSlotUnset } from '@ossy/app/shell'
|
|
4
|
+
|
|
5
|
+
export const metadata = { id: '@ossy/app/layout/minimal' }
|
|
6
|
+
|
|
7
|
+
export const slots = {
|
|
8
|
+
'app:head': '@ossy/app/component/head/default',
|
|
9
|
+
'app:header': '@ossy/app/component/header/default',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function MinimalLayout ({ shellSlots = {} }) {
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
{!shellSlotUnset(shellSlots['app:head']) && <Slot view="app:head" />}
|
|
16
|
+
{!shellSlotUnset(shellSlots['app:system-messages']) && (
|
|
17
|
+
<Slot view="app:system-messages" />
|
|
18
|
+
)}
|
|
19
|
+
<View
|
|
20
|
+
surface="base"
|
|
21
|
+
data-ossy-app-shell
|
|
22
|
+
style={{
|
|
23
|
+
boxSizing: 'border-box',
|
|
24
|
+
height: '100%',
|
|
25
|
+
minWidth: 0,
|
|
26
|
+
padding: 'var(--space-s)',
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<PageSection maxWidth="m" style={{ height: '100%', minWidth: 0, width: '100%' }}>
|
|
30
|
+
<View
|
|
31
|
+
stack
|
|
32
|
+
surface="primary"
|
|
33
|
+
roundness="m"
|
|
34
|
+
inset="s"
|
|
35
|
+
gap="xs"
|
|
36
|
+
style={{ height: '100%', minHeight: 0 }}
|
|
37
|
+
>
|
|
38
|
+
{!shellSlotUnset(shellSlots['app:header']) && (
|
|
39
|
+
<Slot view="app:header" />
|
|
40
|
+
)}
|
|
41
|
+
<main
|
|
42
|
+
style={{
|
|
43
|
+
flex: '1 1 auto',
|
|
44
|
+
minHeight: 0,
|
|
45
|
+
minWidth: 0,
|
|
46
|
+
overflow: 'hidden',
|
|
47
|
+
display: 'flex',
|
|
48
|
+
flexDirection: 'column',
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<Slot view="app:content" />
|
|
52
|
+
</main>
|
|
53
|
+
</View>
|
|
54
|
+
</PageSection>
|
|
55
|
+
</View>
|
|
56
|
+
</>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
|
2
|
+
import { Button, View, Slot, useLocale } from '@ossy/design-system'
|
|
3
|
+
import { useRouter } from '@ossy/router-react'
|
|
4
|
+
import { useSdk } from '@ossy/sdk-react'
|
|
5
|
+
import { GetWorkspace } from '@ossy/workspaces'
|
|
6
|
+
import { useApp, buildSidebarNav, patchUserAppSettings, useCompactShellLayout } from '@ossy/app/shell'
|
|
7
|
+
|
|
8
|
+
export const metadata = { id: '@ossy/app/component/sidebar/default' }
|
|
9
|
+
|
|
10
|
+
const SIDEBAR_LOGO_BLOCK_MIN_PX = 44
|
|
11
|
+
const RAIL_EDGE_HIT_LEFT_PX = 14
|
|
12
|
+
const RAIL_EDGE_HIT_RIGHT_PX = 18
|
|
13
|
+
const RAIL_COLLAPSE_DELAY_MS = 220
|
|
14
|
+
const LOGO_FADE_MS = 180
|
|
15
|
+
const RAIL_INNER_WIDTH_PX = 56
|
|
16
|
+
const RAIL_EXPANDED_INNER_PX = 220
|
|
17
|
+
|
|
18
|
+
function pathnameOf (href) {
|
|
19
|
+
if (typeof href !== 'string' || !href) return ''
|
|
20
|
+
try {
|
|
21
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(href)) {
|
|
22
|
+
return new URL(href).pathname
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// fall through
|
|
26
|
+
}
|
|
27
|
+
return href.split('?')[0].split('#')[0]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isNavSelected ({ href, pageId, itemPageId, routerHref }) {
|
|
31
|
+
const current = pathnameOf(routerHref)
|
|
32
|
+
const target = pathnameOf(href)
|
|
33
|
+
if (current && target && current === target) return true
|
|
34
|
+
if (itemPageId && pageId === itemPageId) return true
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function navLabelForItem (item, showLabels, t) {
|
|
39
|
+
if (!showLabels) return ''
|
|
40
|
+
return item.labelKey ? t(item.labelKey) : item.label
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function DefaultSidebar ({
|
|
44
|
+
small: smallProp = false,
|
|
45
|
+
...rest
|
|
46
|
+
}) {
|
|
47
|
+
const [hoverRailExpanded, setHoverRailExpanded] = useState(false)
|
|
48
|
+
const collapseTimerRef = useRef(null)
|
|
49
|
+
const app = useApp()
|
|
50
|
+
const pageId = app?.pageId
|
|
51
|
+
const compactViewport = useCompactShellLayout()
|
|
52
|
+
const initialSidebarCollapsed = app?.sidebarPrimaryCollapsed === true
|
|
53
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(initialSidebarCollapsed)
|
|
54
|
+
|
|
55
|
+
const onToggleCollapsed = useCallback(() => {
|
|
56
|
+
setSidebarCollapsed((collapsed) => {
|
|
57
|
+
const next = !collapsed
|
|
58
|
+
patchUserAppSettings({ sidebarPrimaryCollapsed: next }).catch(() => {})
|
|
59
|
+
return next
|
|
60
|
+
})
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const small = compactViewport || sidebarCollapsed || smallProp
|
|
64
|
+
const sdk = useSdk()
|
|
65
|
+
const { data: workspace } = sdk.read(GetWorkspace)
|
|
66
|
+
const router = useRouter()
|
|
67
|
+
const { t } = useLocale()
|
|
68
|
+
|
|
69
|
+
const desktopHoverRail = small && !compactViewport
|
|
70
|
+
const showLabels = !small || (desktopHoverRail && hoverRailExpanded)
|
|
71
|
+
const iconOnly = !showLabels
|
|
72
|
+
const hoverExpandedDesktop = desktopHoverRail && hoverRailExpanded
|
|
73
|
+
const narrowChrome = small
|
|
74
|
+
|
|
75
|
+
const { shellItems, packageItems } = useMemo(
|
|
76
|
+
() => buildSidebarNav({
|
|
77
|
+
manifestSummary: app?.manifestSummary,
|
|
78
|
+
workspaceServices: workspace?.services,
|
|
79
|
+
definitions: app?.manifestSummary?.definitions,
|
|
80
|
+
isAuthenticated: app?.isAuthenticated,
|
|
81
|
+
workspaceId: app?.workspaceId || workspace?.id,
|
|
82
|
+
devMode: app?.devMode,
|
|
83
|
+
devEntitlements: app?.devEntitlements,
|
|
84
|
+
}),
|
|
85
|
+
[
|
|
86
|
+
app?.manifestSummary,
|
|
87
|
+
app?.devEntitlements,
|
|
88
|
+
app?.devMode,
|
|
89
|
+
app?.isAuthenticated,
|
|
90
|
+
app?.workspaceId,
|
|
91
|
+
workspace?.services,
|
|
92
|
+
workspace?.id,
|
|
93
|
+
],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const shellButtons = shellItems.map((item) => {
|
|
97
|
+
const href = router.getHref(item.id)
|
|
98
|
+
return {
|
|
99
|
+
key: item.id,
|
|
100
|
+
href,
|
|
101
|
+
selected: isNavSelected({ href, pageId, itemPageId: item.id, routerHref: router.href }),
|
|
102
|
+
prefix: iconOnly ? { name: item.prefix, size: 's', style: { flexShrink: 0 } } : item.prefix,
|
|
103
|
+
label: navLabelForItem(item, showLabels, t),
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const packageButtons = packageItems.map((item) => {
|
|
108
|
+
const href = router.getHref(item.pageId)
|
|
109
|
+
return {
|
|
110
|
+
key: item.slug,
|
|
111
|
+
href,
|
|
112
|
+
selected: isNavSelected({ href, pageId, itemPageId: item.pageId, routerHref: router.href }),
|
|
113
|
+
prefix: iconOnly ? { name: item.icon, size: 's', style: { flexShrink: 0 } } : item.icon,
|
|
114
|
+
label: iconOnly ? '' : item.label,
|
|
115
|
+
title: iconOnly ? item.label : undefined,
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const menuNear = [...shellButtons, ...packageButtons]
|
|
120
|
+
|
|
121
|
+
const clearCollapseTimer = useCallback(() => {
|
|
122
|
+
if (!collapseTimerRef.current) return
|
|
123
|
+
clearTimeout(collapseTimerRef.current)
|
|
124
|
+
collapseTimerRef.current = null
|
|
125
|
+
}, [])
|
|
126
|
+
|
|
127
|
+
const openHoverRail = useCallback(() => {
|
|
128
|
+
clearCollapseTimer()
|
|
129
|
+
setHoverRailExpanded(true)
|
|
130
|
+
}, [clearCollapseTimer])
|
|
131
|
+
|
|
132
|
+
const closeHoverRailWithDelay = useCallback(() => {
|
|
133
|
+
clearCollapseTimer()
|
|
134
|
+
collapseTimerRef.current = setTimeout(() => {
|
|
135
|
+
setHoverRailExpanded(false)
|
|
136
|
+
collapseTimerRef.current = null
|
|
137
|
+
}, RAIL_COLLAPSE_DELAY_MS)
|
|
138
|
+
}, [clearCollapseTimer])
|
|
139
|
+
|
|
140
|
+
useEffect(() => () => clearCollapseTimer(), [clearCollapseTimer])
|
|
141
|
+
|
|
142
|
+
const logoFadeStyle = {
|
|
143
|
+
transition: `opacity ${LOGO_FADE_MS}ms ease`,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const renderLogo = () => {
|
|
147
|
+
if (desktopHoverRail) {
|
|
148
|
+
return (
|
|
149
|
+
<div style={{ position: 'relative', height: 28, width: '100%', display: 'flex', justifyContent: 'center' }}>
|
|
150
|
+
<div style={{ ...logoFadeStyle, opacity: hoverRailExpanded ? 0 : 1 }}>
|
|
151
|
+
<Slot view="@ossy/app/component/logo/logomark" />
|
|
152
|
+
</div>
|
|
153
|
+
<div
|
|
154
|
+
style={{
|
|
155
|
+
position: 'absolute',
|
|
156
|
+
inset: 0,
|
|
157
|
+
display: 'flex',
|
|
158
|
+
justifyContent: 'center',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
...logoFadeStyle,
|
|
161
|
+
opacity: hoverRailExpanded ? 1 : 0,
|
|
162
|
+
pointerEvents: 'none',
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<Slot view="@ossy/app/component/logo/logotype" />
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Slot view={showLabels ? '@ossy/app/component/logo/logotype' : '@ossy/app/component/logo/logomark'} />
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const renderButton = (item) => (
|
|
177
|
+
<Button
|
|
178
|
+
key={item.key ?? item.href}
|
|
179
|
+
href={item.href}
|
|
180
|
+
variant={item.selected ? 'cta' : 'link'}
|
|
181
|
+
prefix={item.prefix}
|
|
182
|
+
title={item.title}
|
|
183
|
+
aria-current={item.selected ? 'page' : undefined}
|
|
184
|
+
style={{
|
|
185
|
+
justifyContent: iconOnly ? 'center' : 'flex-start',
|
|
186
|
+
padding: hoverExpandedDesktop ? '8px 24px' : (narrowChrome ? 'var(--space-s)' : '8px 24px'),
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{item.label}
|
|
190
|
+
</Button>
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
const inner = (
|
|
194
|
+
<View
|
|
195
|
+
{...(!desktopHoverRail ? rest : {})}
|
|
196
|
+
surface="primary"
|
|
197
|
+
gap={narrowChrome ? 's' : 'm'}
|
|
198
|
+
style={{
|
|
199
|
+
height: '100%',
|
|
200
|
+
width: desktopHoverRail ? '100%' : 'auto',
|
|
201
|
+
minWidth: small ? 0 : undefined,
|
|
202
|
+
maxHeight: small ? '100%' : undefined,
|
|
203
|
+
padding: hoverExpandedDesktop
|
|
204
|
+
? 'var(--space-m) var(--space-l) var(--space-m) var(--space-m)'
|
|
205
|
+
: narrowChrome
|
|
206
|
+
? 'var(--space-s) var(--space-xs)'
|
|
207
|
+
: 'var(--space-m) var(--space-l) var(--space-m) var(--space-m)',
|
|
208
|
+
borderRadius: 'var(--space-l) var(--space-s) var(--space-s) var(--space-l)',
|
|
209
|
+
flexShrink: 0,
|
|
210
|
+
boxSizing: 'border-box',
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
<View
|
|
214
|
+
layout="row"
|
|
215
|
+
style={{
|
|
216
|
+
justifyContent: 'center',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
marginBottom: 'var(--space-m)',
|
|
219
|
+
minHeight: SIDEBAR_LOGO_BLOCK_MIN_PX,
|
|
220
|
+
flexShrink: 0,
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
{renderLogo()}
|
|
224
|
+
</View>
|
|
225
|
+
|
|
226
|
+
<View gap="s" style={{ flexGrow: 1, overflowY: 'auto', minHeight: 0 }} data-scroll>
|
|
227
|
+
<View style={{ flexShrink: 0, height: 'var(--space-l)', minHeight: 'var(--space-l)' }} />
|
|
228
|
+
{menuNear.map(renderButton)}
|
|
229
|
+
</View>
|
|
230
|
+
|
|
231
|
+
{onToggleCollapsed && !compactViewport && (
|
|
232
|
+
<View style={{ flexShrink: 0, marginTop: 'var(--space-s)' }}>
|
|
233
|
+
<Button
|
|
234
|
+
variant="link"
|
|
235
|
+
prefix={small ? 'arrow-long-right' : 'arrow-long-left'}
|
|
236
|
+
onClick={(e) => {
|
|
237
|
+
e.stopPropagation()
|
|
238
|
+
onToggleCollapsed()
|
|
239
|
+
}}
|
|
240
|
+
aria-label={small
|
|
241
|
+
? (t('app.shell.sidebar.expand') || 'Expand sidebar')
|
|
242
|
+
: (t('app.shell.sidebar.collapse') || 'Collapse sidebar')}
|
|
243
|
+
style={{
|
|
244
|
+
justifyContent: 'flex-start',
|
|
245
|
+
width: '100%',
|
|
246
|
+
padding: '8px 0 8px 12px',
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
</View>
|
|
250
|
+
)}
|
|
251
|
+
</View>
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if (!desktopHoverRail) {
|
|
255
|
+
return inner
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div
|
|
260
|
+
{...rest}
|
|
261
|
+
data-ossy-sidebar-rail
|
|
262
|
+
style={{
|
|
263
|
+
alignSelf: 'stretch',
|
|
264
|
+
flexShrink: 0,
|
|
265
|
+
boxSizing: 'content-box',
|
|
266
|
+
width: hoverRailExpanded ? RAIL_EXPANDED_INNER_PX : RAIL_INNER_WIDTH_PX,
|
|
267
|
+
paddingLeft: RAIL_EDGE_HIT_LEFT_PX,
|
|
268
|
+
marginLeft: -RAIL_EDGE_HIT_LEFT_PX,
|
|
269
|
+
paddingRight: RAIL_EDGE_HIT_RIGHT_PX,
|
|
270
|
+
marginRight: -RAIL_EDGE_HIT_RIGHT_PX,
|
|
271
|
+
transition: 'width 0.2s ease',
|
|
272
|
+
zIndex: hoverRailExpanded ? 25 : 2,
|
|
273
|
+
position: 'relative',
|
|
274
|
+
}}
|
|
275
|
+
onMouseEnter={openHoverRail}
|
|
276
|
+
onMouseLeave={closeHoverRailWithDelay}
|
|
277
|
+
>
|
|
278
|
+
{inner}
|
|
279
|
+
</div>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app.shell.sidebar.expand": "Expandera sidopanel",
|
|
3
|
+
"app.shell.sidebar.collapse": "Smal sidopanel med ikoner",
|
|
4
|
+
"app.shell.header.languagePicker": "Språk",
|
|
5
|
+
"app.shell.footer.languages": "Språk",
|
|
6
|
+
"app.shell.header.themeSwitch": "{theme}-tema. Byt tema.",
|
|
7
|
+
"app.shell.header.profile": "Profil"
|
|
8
|
+
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Derive MCP tool name from platform action id.
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* actionIdToToolName('@ossy/resources/actions/list') // 'ossy_resources_list'
|
|
6
|
-
*
|
|
7
|
-
* @param {string} actionId
|
|
8
|
-
* @returns {string}
|
|
9
|
-
*/
|
|
10
|
-
export function actionIdToToolName (actionId) {
|
|
11
|
-
const parts = actionId.split('/')
|
|
12
|
-
if (parts.length >= 4 && parts[0].startsWith('@') && parts[2] === 'actions') {
|
|
13
|
-
const provider = parts[0].slice(1)
|
|
14
|
-
return `${provider}_${parts[1]}_${parts[3]}`.replace(/-/g, '_')
|
|
15
|
-
}
|
|
16
|
-
return `ossy_${actionId.replace(/^@/, '').replace(/\//g, '_').replace(/-/g, '_')}`
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @param {string} actionId
|
|
21
|
-
* @returns {string}
|
|
22
|
-
*/
|
|
23
|
-
export function humanizeActionId (actionId) {
|
|
24
|
-
const intent = actionId.split('/').pop() || actionId
|
|
25
|
-
return intent
|
|
26
|
-
.split('-')
|
|
27
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
28
|
-
.join(' ')
|
|
29
|
-
}
|