@ossy/app 1.40.2 → 1.40.3
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 +43 -12
- package/cli/build.task.js +49 -10
- package/cli/get-platform-files.task.js +17 -9
- package/cli/manifest-plugin.js +250 -25
- package/package.json +15 -11
- package/runtime/page-runtime.js +5 -3
- package/runtime/resolve-app-slots.js +89 -0
- package/src/manifest/action-id-to-tool-name.js +29 -0
- package/src/manifest/build-action-input-schema.js +220 -0
- package/src/manifest/build-actions-schema.js +36 -0
- package/src/manifest/build-capabilities.js +190 -0
- package/src/manifest/build-manifest-summary.js +11 -5
- package/src/manifest/extract-task-catalog.js +1 -0
- package/src/manifest/template-to-json-schema.js +103 -0
- package/src/shell/App.jsx +6 -3
- package/src/shell/AppSettings.jsx +11 -0
- package/src/shell/DevPagesPanel.jsx +17 -13
- package/src/shell/ThemeEditor.jsx +18 -33
- package/src/shell/WorkspaceAppSettingsSync.jsx +39 -0
- package/src/shell/index.js +5 -0
- package/src/shell/patchUserAppSettings.js +10 -0
- package/src/shell/resolveEndpoints.js +43 -0
- package/src/shell/resolveWorkspaceServices.js +20 -0
- package/src/shell/themeEditorStyles.js +49 -0
- package/src/shell/useShellWorkspace.js +40 -0
- package/runtime/resolve-shell-slots.js +0 -112
|
@@ -1,30 +1,9 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react'
|
|
2
2
|
import { updateResourceContent } from '@ossy/resources'
|
|
3
3
|
import { useSdk } from '@ossy/sdk-react'
|
|
4
4
|
import { Overlay, Button, useTheme, View, Text } from '@ossy/design-system'
|
|
5
5
|
import { DevPagesPanel } from './DevPagesPanel.jsx'
|
|
6
|
-
|
|
7
|
-
const fabStyles = {
|
|
8
|
-
boxShadow: '2px 2px 5px hsla(0, 0%, 0%, .2)',
|
|
9
|
-
borderRadius: '999px',
|
|
10
|
-
position: 'fixed',
|
|
11
|
-
right: 'var(--space-m)',
|
|
12
|
-
bottom: 'var(--space-m)',
|
|
13
|
-
cursor: 'pointer',
|
|
14
|
-
transition: 'transform .5s',
|
|
15
|
-
zIndex: 101,
|
|
16
|
-
padding: 'var(--space-m)',
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const modalPanelStyles = {
|
|
20
|
-
width: 'min(720px, 80vw)',
|
|
21
|
-
minWidth: 'min(480px, 100%)',
|
|
22
|
-
maxHeight: '85vh',
|
|
23
|
-
overflowY: 'auto',
|
|
24
|
-
overflowX: 'hidden',
|
|
25
|
-
margin: '0 auto',
|
|
26
|
-
boxShadow: '2px 2px 5px hsla(0, 0%, 0%, .2)',
|
|
27
|
-
}
|
|
6
|
+
import { themeEditorStyles } from './themeEditorStyles.js'
|
|
28
7
|
|
|
29
8
|
const ThemeSwitcher = () => {
|
|
30
9
|
const { activeTheme, setTheme, themes } = useTheme()
|
|
@@ -47,10 +26,6 @@ export const ThemeEditor = () => {
|
|
|
47
26
|
// const [theme, temporarilyUpdateTheme] = useTheme()
|
|
48
27
|
const theme = {}
|
|
49
28
|
const temporarilyUpdateTheme = () => {}
|
|
50
|
-
|
|
51
|
-
const toggleStyles = useMemo(() => !isEditorOpen
|
|
52
|
-
? fabStyles
|
|
53
|
-
: { ...fabStyles, transform: 'rotate(-45deg)' }, [isEditorOpen])
|
|
54
29
|
|
|
55
30
|
const onToggle = useCallback(() => {
|
|
56
31
|
setIsEditorOpen(!isEditorOpen)
|
|
@@ -85,18 +60,22 @@ export const ThemeEditor = () => {
|
|
|
85
60
|
|
|
86
61
|
return (
|
|
87
62
|
<>
|
|
63
|
+
<style href="@ossy/app/theme-editor" precedence="high">
|
|
64
|
+
{themeEditorStyles}
|
|
65
|
+
</style>
|
|
66
|
+
|
|
88
67
|
{
|
|
89
68
|
isEditorOpen && (
|
|
90
69
|
<Overlay isVisible={true} onClose={onToggle}>
|
|
91
|
-
<View layout="off-center"
|
|
92
|
-
<View slot="content"
|
|
70
|
+
<View layout="off-center" data-component="theme-editor-overlay">
|
|
71
|
+
<View slot="content" data-component="theme-editor-panel">
|
|
93
72
|
<View surface="primary" roundness="m" gap="m" inset="l">
|
|
94
73
|
<DevPagesPanel />
|
|
95
74
|
<View as="form" gap="s" onSubmit={onSaveTheme} onChange={onThemeChange}>
|
|
96
75
|
{Object.entries(theme).map(([name, value]) => (
|
|
97
|
-
<div
|
|
98
|
-
<label
|
|
99
|
-
<input value={value} data-name={name}
|
|
76
|
+
<div key={name} data-component="theme-editor-field">
|
|
77
|
+
<label>{name}</label>
|
|
78
|
+
<input value={value} data-name={name} />
|
|
100
79
|
</div>
|
|
101
80
|
))}
|
|
102
81
|
<Button type="submit" variant="cta">
|
|
@@ -112,7 +91,13 @@ export const ThemeEditor = () => {
|
|
|
112
91
|
)
|
|
113
92
|
}
|
|
114
93
|
|
|
115
|
-
<Button
|
|
94
|
+
<Button
|
|
95
|
+
variant="cta"
|
|
96
|
+
prefix="math-plus"
|
|
97
|
+
data-component="theme-editor-fab"
|
|
98
|
+
data-open={isEditorOpen ? 'true' : undefined}
|
|
99
|
+
onClick={onToggle}
|
|
100
|
+
/>
|
|
116
101
|
</>
|
|
117
102
|
)
|
|
118
103
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { GetWorkspace, ListWorkspaces } from '@ossy/workspaces'
|
|
3
|
+
import { useSdk } from '@ossy/sdk-react'
|
|
4
|
+
import { patchUserAppSettings } from './patchUserAppSettings.js'
|
|
5
|
+
|
|
6
|
+
function buildPatch (workspace, workspaces) {
|
|
7
|
+
const patch = {}
|
|
8
|
+
if (workspace?.name) patch.workspaceName = workspace.name
|
|
9
|
+
if (workspace?.services) patch.workspaceServices = workspace.services
|
|
10
|
+
if (workspaces?.length) {
|
|
11
|
+
patch.workspaces = workspaces.map(({ id, name }) => ({ id, name }))
|
|
12
|
+
}
|
|
13
|
+
return Object.keys(patch).length ? patch : null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Persists workspace shell snapshot to the signed user-app-settings cookie
|
|
18
|
+
* (via ProxyInternal PATCH /@ossy/users/me/app-settings) so SSR bootstrap
|
|
19
|
+
* props stay stable across full-page navigations.
|
|
20
|
+
*/
|
|
21
|
+
export function WorkspaceAppSettingsSync () {
|
|
22
|
+
const sdk = useSdk()
|
|
23
|
+
const { data: workspace } = sdk.read(GetWorkspace)
|
|
24
|
+
const { data: workspaces } = sdk.read(ListWorkspaces)
|
|
25
|
+
const lastPatchRef = useRef('')
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const patch = buildPatch(workspace, workspaces)
|
|
29
|
+
if (!patch) return
|
|
30
|
+
|
|
31
|
+
const serialized = JSON.stringify(patch)
|
|
32
|
+
if (serialized === lastPatchRef.current) return
|
|
33
|
+
lastPatchRef.current = serialized
|
|
34
|
+
|
|
35
|
+
patchUserAppSettings(patch).catch(() => {})
|
|
36
|
+
}, [workspace, workspaces])
|
|
37
|
+
|
|
38
|
+
return null
|
|
39
|
+
}
|
package/src/shell/index.js
CHANGED
|
@@ -2,3 +2,8 @@ export * from './App.jsx'
|
|
|
2
2
|
export * from './AppSettings.jsx'
|
|
3
3
|
export { useApp, useAppSettings, AppContext } from './AppContext.js'
|
|
4
4
|
export * from './ThemeEditor.jsx'
|
|
5
|
+
export { patchUserAppSettings } from './patchUserAppSettings.js'
|
|
6
|
+
export { useShellWorkspace } from './useShellWorkspace.js'
|
|
7
|
+
export { WorkspaceAppSettingsSync } from './WorkspaceAppSettingsSync.jsx'
|
|
8
|
+
export { resolveEndpoints } from './resolveEndpoints.js'
|
|
9
|
+
export { resolveWorkspaceServices } from './resolveWorkspaceServices.js'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve integration endpoint URLs from app context and browser origin.
|
|
3
|
+
*
|
|
4
|
+
* @param {{ apiUrl?: string, workspaceId?: string } | null | undefined} app
|
|
5
|
+
* @returns {{
|
|
6
|
+
* apiUrl: string,
|
|
7
|
+
* appOrigin: string,
|
|
8
|
+
* actionsUrl: string,
|
|
9
|
+
* mcpUrl: string,
|
|
10
|
+
* capabilitiesUrl: string,
|
|
11
|
+
* workspaceId: string | undefined,
|
|
12
|
+
* }}
|
|
13
|
+
*/
|
|
14
|
+
export function resolveEndpoints (app) {
|
|
15
|
+
const apiUrl = app?.apiUrl || ''
|
|
16
|
+
const appOrigin =
|
|
17
|
+
typeof window !== 'undefined' && window.location?.origin
|
|
18
|
+
? window.location.origin
|
|
19
|
+
: ''
|
|
20
|
+
|
|
21
|
+
const normalizedApi = apiUrl.replace(/\/$/, '')
|
|
22
|
+
let actionsUrl = '/actions'
|
|
23
|
+
|
|
24
|
+
if (normalizedApi.startsWith('http://') || normalizedApi.startsWith('https://')) {
|
|
25
|
+
actionsUrl = `${normalizedApi}/actions`
|
|
26
|
+
} else if (normalizedApi.startsWith('/')) {
|
|
27
|
+
actionsUrl = appOrigin ? `${appOrigin}${normalizedApi}/actions` : `${normalizedApi}/actions`
|
|
28
|
+
} else if (appOrigin) {
|
|
29
|
+
actionsUrl = `${appOrigin}/actions`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const mcpUrl = appOrigin ? `${appOrigin}/mcp` : '/mcp'
|
|
33
|
+
const capabilitiesUrl = appOrigin ? `${appOrigin}/capabilities.json` : '/capabilities.json'
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
apiUrl: normalizedApi,
|
|
37
|
+
appOrigin,
|
|
38
|
+
actionsUrl,
|
|
39
|
+
mcpUrl,
|
|
40
|
+
capabilitiesUrl,
|
|
41
|
+
workspaceId: app?.workspaceId,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge live workspace services with dev-mode entitlement overrides.
|
|
3
|
+
*
|
|
4
|
+
* @param {{
|
|
5
|
+
* devMode?: boolean
|
|
6
|
+
* devEntitlements?: Record<string, { enabled?: boolean }>
|
|
7
|
+
* workspaceServices?: Record<string, { enabled?: boolean }>
|
|
8
|
+
* }} options
|
|
9
|
+
* @returns {Record<string, { enabled?: boolean }>}
|
|
10
|
+
*/
|
|
11
|
+
export function resolveWorkspaceServices ({
|
|
12
|
+
devMode = false,
|
|
13
|
+
devEntitlements,
|
|
14
|
+
workspaceServices,
|
|
15
|
+
}) {
|
|
16
|
+
if (devMode && devEntitlements) {
|
|
17
|
+
return { ...(workspaceServices || {}), ...devEntitlements }
|
|
18
|
+
}
|
|
19
|
+
return workspaceServices || {}
|
|
20
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Dev theme editor — FAB and modal panel using theme tokens. */
|
|
2
|
+
export const themeEditorStyles = `
|
|
3
|
+
[data-component="theme-editor-fab"] {
|
|
4
|
+
box-shadow: 2px 2px 5px color-mix(in srgb, var(--foreground) 20%, transparent);
|
|
5
|
+
border-radius: var(--space-xl);
|
|
6
|
+
position: fixed;
|
|
7
|
+
right: var(--space-m);
|
|
8
|
+
bottom: var(--space-m);
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
transition: transform 0.5s;
|
|
11
|
+
z-index: 101;
|
|
12
|
+
padding: var(--space-m);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[data-component="theme-editor-fab"][data-open="true"] {
|
|
16
|
+
transform: rotate(-45deg);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[data-component="theme-editor-panel"] {
|
|
20
|
+
width: min(720px, 80vw);
|
|
21
|
+
min-width: min(480px, 100%);
|
|
22
|
+
max-height: 85vh;
|
|
23
|
+
overflow-y: auto;
|
|
24
|
+
overflow-x: hidden;
|
|
25
|
+
margin: 0 auto;
|
|
26
|
+
box-shadow: 2px 2px 5px color-mix(in srgb, var(--foreground) 20%, transparent);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[data-component="theme-editor-overlay"] {
|
|
30
|
+
height: 100%;
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
[data-component="theme-editor-field"] {
|
|
35
|
+
margin-bottom: var(--space-m);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-component="theme-editor-field"] label {
|
|
39
|
+
display: block;
|
|
40
|
+
font-family: var(--text-default-font-family, sans-serif);
|
|
41
|
+
margin-bottom: var(--space-xs);
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[data-component="theme-editor-field"] input {
|
|
46
|
+
width: 100%;
|
|
47
|
+
padding: var(--space-xs);
|
|
48
|
+
}
|
|
49
|
+
`
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { GetWorkspace, ListWorkspaces } from '@ossy/workspaces'
|
|
3
|
+
import { useSdk } from '@ossy/sdk-react'
|
|
4
|
+
import { useApp } from './AppContext.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Workspace list + active workspace for shell chrome.
|
|
8
|
+
* SSR and the first client paint use the signed user-app-settings cookie
|
|
9
|
+
* (merged into bootstrap props); live SDK reads refresh in the background.
|
|
10
|
+
*/
|
|
11
|
+
export function useShellWorkspace () {
|
|
12
|
+
const app = useApp()
|
|
13
|
+
const sdk = useSdk()
|
|
14
|
+
const { data: workspaceFromSdk } = sdk.read(GetWorkspace)
|
|
15
|
+
const { data: workspacesFromSdk } = sdk.read(ListWorkspaces)
|
|
16
|
+
|
|
17
|
+
const workspace = useMemo(() => {
|
|
18
|
+
if (workspaceFromSdk) return workspaceFromSdk
|
|
19
|
+
if (app?.workspaceName || app?.workspaceServices || app?.workspaceId) {
|
|
20
|
+
return {
|
|
21
|
+
id: app.workspaceId,
|
|
22
|
+
name: app.workspaceName,
|
|
23
|
+
services: app.workspaceServices,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined
|
|
27
|
+
}, [
|
|
28
|
+
workspaceFromSdk,
|
|
29
|
+
app?.workspaceId,
|
|
30
|
+
app?.workspaceName,
|
|
31
|
+
app?.workspaceServices,
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const workspaces = useMemo(() => {
|
|
35
|
+
if (workspacesFromSdk?.length) return workspacesFromSdk
|
|
36
|
+
return app?.workspaces ?? []
|
|
37
|
+
}, [workspacesFromSdk, app?.workspaces])
|
|
38
|
+
|
|
39
|
+
return { workspace, workspaces }
|
|
40
|
+
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/** Canonical shell slot names (namespaced). App layout maps chrome only — not content. */
|
|
2
|
-
export const SHELL_SLOT_NAMES = [
|
|
3
|
-
'shell:header',
|
|
4
|
-
'shell:sidebar',
|
|
5
|
-
'shell:toolbar',
|
|
6
|
-
'shell:notifications',
|
|
7
|
-
'shell:system-messages',
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
/** Platform-owned slot filled with the current route page component. */
|
|
11
|
-
export const CONTENT_SLOT_NAME = 'shell:content'
|
|
12
|
-
|
|
13
|
-
/** Bare shell region → namespaced key (legacy fallback for app components only). */
|
|
14
|
-
const BARE_SHELL_REGION = {
|
|
15
|
-
header: 'shell:header',
|
|
16
|
-
sidebar: 'shell:sidebar',
|
|
17
|
-
toolbar: 'shell:toolbar',
|
|
18
|
-
notifications: 'shell:notifications',
|
|
19
|
-
'system-messages': 'shell:system-messages',
|
|
20
|
-
content: CONTENT_SLOT_NAME,
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Normalize a slot map key to the canonical namespaced form when it is a bare shell region.
|
|
25
|
-
*
|
|
26
|
-
* @param {string} slotName
|
|
27
|
-
* @returns {string}
|
|
28
|
-
*/
|
|
29
|
-
export function normalizeShellSlotName (slotName) {
|
|
30
|
-
const key = typeof slotName === 'string' ? slotName.trim() : ''
|
|
31
|
-
return BARE_SHELL_REGION[key] ?? key
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Build `Record<slotName, Component>` from the app layout's static `slots` map
|
|
36
|
-
* (`slotName → componentId`) and components loaded by `metadata.id`.
|
|
37
|
-
*
|
|
38
|
-
* One component per slot. Optional fallback (app components only): if the layout
|
|
39
|
-
* map omits a slot, use a component whose id equals the slot name (e.g. `shell:header`)
|
|
40
|
-
* or the bare region name (e.g. `header`).
|
|
41
|
-
*
|
|
42
|
-
* Does not resolve `shell:content` — use {@link resolvePageSlots}.
|
|
43
|
-
*
|
|
44
|
-
* @param {Record<string, string> | null | undefined} layoutSlotsMap
|
|
45
|
-
* @param {Record<string, import('react').ComponentType>} componentsById
|
|
46
|
-
* @returns {Record<string, import('react').ComponentType>}
|
|
47
|
-
*/
|
|
48
|
-
export function resolveShellSlots (layoutSlotsMap, componentsById) {
|
|
49
|
-
/** @type {Record<string, import('react').ComponentType>} */
|
|
50
|
-
const resolved = {}
|
|
51
|
-
|
|
52
|
-
const map = layoutSlotsMap && typeof layoutSlotsMap === 'object' ? layoutSlotsMap : {}
|
|
53
|
-
for (const [rawSlot, componentId] of Object.entries(map)) {
|
|
54
|
-
const slotName = normalizeShellSlotName(rawSlot)
|
|
55
|
-
const id = typeof componentId === 'string' ? componentId.trim() : ''
|
|
56
|
-
if (!slotName || !id || slotName === CONTENT_SLOT_NAME) continue
|
|
57
|
-
const Component = componentsById[id]
|
|
58
|
-
if (Component) resolved[slotName] = Component
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for (const slotName of SHELL_SLOT_NAMES) {
|
|
62
|
-
if (resolved[slotName]) continue
|
|
63
|
-
if (componentsById[slotName]) {
|
|
64
|
-
resolved[slotName] = componentsById[slotName]
|
|
65
|
-
continue
|
|
66
|
-
}
|
|
67
|
-
const bare = slotName.slice('shell:'.length)
|
|
68
|
-
if (componentsById[bare]) resolved[slotName] = componentsById[bare]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return resolved
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const RESOURCE_SLOT_PREFIX = 'resource:'
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Map manifest components whose `metadata.id` is a `resource:{type}/{view}` key.
|
|
78
|
-
*
|
|
79
|
-
* @param {Record<string, import('react').ComponentType>} componentsById
|
|
80
|
-
* @returns {Record<string, import('react').ComponentType>}
|
|
81
|
-
*/
|
|
82
|
-
export function resolveResourceSlots (componentsById) {
|
|
83
|
-
/** @type {Record<string, import('react').ComponentType>} */
|
|
84
|
-
const resolved = {}
|
|
85
|
-
for (const [id, Component] of Object.entries(componentsById || {})) {
|
|
86
|
-
if (typeof id === 'string' && id.startsWith(RESOURCE_SLOT_PREFIX) && Component) {
|
|
87
|
-
resolved[id] = Component
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return resolved
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Full provider slot map for a page request: shell chrome, resource views, and page content.
|
|
95
|
-
*
|
|
96
|
-
* @param {{
|
|
97
|
-
* layoutSlots?: Record<string, string> | null,
|
|
98
|
-
* componentsById?: Record<string, import('react').ComponentType>,
|
|
99
|
-
* pageComponent?: import('react').ComponentType | null,
|
|
100
|
-
* }} options
|
|
101
|
-
* @returns {Record<string, import('react').ComponentType>}
|
|
102
|
-
*/
|
|
103
|
-
export function resolvePageSlots ({ layoutSlots, componentsById = {}, pageComponent = null } = {}) {
|
|
104
|
-
const shellSlots = resolveShellSlots(layoutSlots, componentsById)
|
|
105
|
-
const resourceSlots = resolveResourceSlots(componentsById)
|
|
106
|
-
/** @type {Record<string, import('react').ComponentType>} */
|
|
107
|
-
const resolved = { ...componentsById, ...shellSlots, ...resourceSlots }
|
|
108
|
-
if (pageComponent) {
|
|
109
|
-
resolved[CONTENT_SLOT_NAME] = pageComponent
|
|
110
|
-
}
|
|
111
|
-
return resolved
|
|
112
|
-
}
|