@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.
Files changed (40) hide show
  1. package/README.md +6 -0
  2. package/cli/get-platform-files.task.js +4 -6
  3. package/cli/manifest-plugin.js +111 -36
  4. package/package.json +23 -14
  5. package/runtime/merge-shell-slots.js +148 -0
  6. package/runtime/page-runtime.js +32 -16
  7. package/runtime/resolve-app-slots.js +48 -11
  8. package/src/en.translations.json +8 -0
  9. package/src/manifest/build-actions-schema.js +1 -36
  10. package/src/manifest/build-capabilities.js +1 -190
  11. package/src/manifest/build-manifest-summary.js +1 -121
  12. package/src/manifest/discover-package-definitions.js +1 -100
  13. package/src/manifest/resolve-page-layout.js +51 -0
  14. package/src/manifest/serialize-package-definition.js +1 -30
  15. package/src/shell/App.jsx +13 -7
  16. package/src/shell/AppSettings.jsx +6 -0
  17. package/src/shell/DevPagesPanel.jsx +1 -1
  18. package/src/shell/HeaderAuthActions.jsx +49 -0
  19. package/src/shell/LanguageList.jsx +83 -0
  20. package/src/shell/LanguageSelect.jsx +73 -0
  21. package/src/shell/ThemeEditor.jsx +2 -2
  22. package/src/shell/ThemeSelect.jsx +101 -0
  23. package/src/shell/buildSidebarNav.js +86 -0
  24. package/src/shell/index.js +7 -0
  25. package/src/shell/languageCode.js +31 -0
  26. package/src/shell/resolvePackageHomePageId.js +14 -0
  27. package/src/shell/useCompactShellLayout.js +24 -0
  28. package/src/shell-registry/blank.layout.jsx +8 -0
  29. package/src/shell-registry/default.layout.jsx +101 -0
  30. package/src/shell-registry/footer-default.component.jsx +39 -0
  31. package/src/shell-registry/head-default.component.jsx +17 -0
  32. package/src/shell-registry/header-default.component.jsx +50 -0
  33. package/src/shell-registry/logo-logomark.component.jsx +8 -0
  34. package/src/shell-registry/logo-logotype.component.jsx +15 -0
  35. package/src/shell-registry/minimal.layout.jsx +58 -0
  36. package/src/shell-registry/sidebar-default.component.jsx +281 -0
  37. package/src/sv.translations.json +8 -0
  38. package/src/manifest/action-id-to-tool-name.js +0 -29
  39. package/src/manifest/build-action-input-schema.js +0 -220
  40. 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
- }