@ossy/app 1.39.6 → 1.40.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.
- package/README.md +13 -10
- package/cli/build.task.js +28 -12
- package/cli/discover-translation-files.js +139 -0
- package/cli/get-platform-files.task.js +1 -0
- package/cli/manifest-plugin.js +81 -26
- package/cli/merge-translations.task.js +170 -0
- package/package.json +15 -10
- package/runtime/page-runtime.js +53 -8
- package/runtime/resolve-shell-slots.js +112 -0
- package/src/manifest/build-manifest-summary.js +115 -0
- package/src/manifest/discover-package-definitions.js +100 -0
- package/src/manifest/serialize-package-definition.js +30 -0
- package/src/shell/App.jsx +22 -14
- package/src/shell/AppSettings.jsx +2 -0
- package/src/shell/DevPagesPanel.jsx +128 -0
- package/src/shell/ThemeEditor.jsx +36 -65
- package/src/shell/devPagesUtils.js +53 -0
package/src/shell/App.jsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { SDK } from '@ossy/sdk'
|
|
3
3
|
import { WorkspaceProvider } from '@ossy/sdk-react'
|
|
4
|
-
import { Theme, ComponentSlotsProvider } from '@ossy/design-system'
|
|
4
|
+
import { Theme, ComponentSlotsProvider, LocaleProvider } from '@ossy/design-system'
|
|
5
5
|
import { ThemeEditor } from './ThemeEditor.jsx'
|
|
6
6
|
import { defaultAppSettings } from './AppSettings.jsx'
|
|
7
7
|
import { Router } from '@ossy/router-react'
|
|
8
8
|
import { AppContext } from './AppContext.js'
|
|
9
9
|
|
|
10
|
-
export const App = ({ children, ..._appSettings }) => {
|
|
10
|
+
export const App = ({ children, language, messages, fallbackMessages, ..._appSettings }) => {
|
|
11
11
|
const appSettings = { ...defaultAppSettings(), ..._appSettings }
|
|
12
12
|
|
|
13
13
|
// `components` holds resolved ComponentType values — not JSON-serializable,
|
|
@@ -20,17 +20,25 @@ export const App = ({ children, ..._appSettings }) => {
|
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
<LocaleProvider
|
|
24
|
+
language={language}
|
|
25
|
+
messages={messages}
|
|
26
|
+
fallbackMessages={fallbackMessages}
|
|
27
|
+
defaultLanguage={appSettings.defaultLanguage}
|
|
28
|
+
supportedLanguages={appSettings.supportedLanguages}
|
|
29
|
+
>
|
|
30
|
+
<AppContext.Provider value={contextSettings}>
|
|
31
|
+
<ComponentSlotsProvider slots={components || {}}>
|
|
32
|
+
<Theme theme={appSettings.theme} themes={appSettings.themes}>
|
|
33
|
+
<WorkspaceProvider sdk={sdk}>
|
|
34
|
+
<Router {...appSettings} pages={appSettings.pages || []}>
|
|
35
|
+
{children}
|
|
36
|
+
{appSettings.devMode && <ThemeEditor />}
|
|
37
|
+
</Router>
|
|
38
|
+
</WorkspaceProvider>
|
|
39
|
+
</Theme>
|
|
40
|
+
</ComponentSlotsProvider>
|
|
41
|
+
</AppContext.Provider>
|
|
42
|
+
</LocaleProvider>
|
|
35
43
|
)
|
|
36
44
|
}
|
|
@@ -25,5 +25,7 @@ export function defaultAppSettings() {
|
|
|
25
25
|
faviconHref: undefined,
|
|
26
26
|
/** When true, main app sidebar is collapsed to icons (from server cookie / app-settings). */
|
|
27
27
|
sidebarPrimaryCollapsed: false,
|
|
28
|
+
/** Grouped manifest entries by npm package — for dev tooling. */
|
|
29
|
+
manifestSummary: undefined,
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import { Button, Text, View } from '@ossy/design-system'
|
|
3
|
+
import { useRouter } from '@ossy/router-react'
|
|
4
|
+
import { useApp } from './AppContext.js'
|
|
5
|
+
import { formatPagePaths, groupPagesByFeature } from './devPagesUtils.js'
|
|
6
|
+
import { packageNameToSlug } from '../manifest/build-manifest-summary.js'
|
|
7
|
+
|
|
8
|
+
const monoStyle = {
|
|
9
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
10
|
+
fontSize: '0.8125rem',
|
|
11
|
+
lineHeight: 1.4,
|
|
12
|
+
wordBreak: 'break-all',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const currentPageStyles = {
|
|
16
|
+
padding: 'var(--space-s)',
|
|
17
|
+
borderRadius: 'var(--space-xs)',
|
|
18
|
+
background: 'var(--surface-secondary, hsla(0, 0%, 0%, 0.04))',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DevPagesPanel = () => {
|
|
22
|
+
const app = useApp()
|
|
23
|
+
const router = useRouter()
|
|
24
|
+
const pages = app?.pages || []
|
|
25
|
+
|
|
26
|
+
const groups = useMemo(() => groupPagesByFeature(pages), [pages])
|
|
27
|
+
|
|
28
|
+
const currentPage = useMemo(
|
|
29
|
+
() => pages.find((p) => p.id === app?.pageId),
|
|
30
|
+
[pages, app?.pageId],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const hasPackageViewer = useMemo(
|
|
34
|
+
() => pages.some((p) => p.id === 'packages/detail'),
|
|
35
|
+
[pages],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const packageDetailHref = useMemo(() => {
|
|
39
|
+
if (!hasPackageViewer) return null
|
|
40
|
+
const pkg = currentPage?.package
|
|
41
|
+
if (!pkg) return null
|
|
42
|
+
return router.getHref({
|
|
43
|
+
id: 'packages/detail',
|
|
44
|
+
params: { packageSlug: packageNameToSlug(pkg) },
|
|
45
|
+
})
|
|
46
|
+
}, [currentPage?.package, hasPackageViewer, router])
|
|
47
|
+
|
|
48
|
+
const currentPathLabel = useMemo(() => {
|
|
49
|
+
if (currentPage?.path) return formatPagePaths(currentPage.path)
|
|
50
|
+
const url = app?.url || router.href
|
|
51
|
+
if (!url) return '—'
|
|
52
|
+
const pathname = url.split('?')[0].split('#')[0]
|
|
53
|
+
return pathname || '—'
|
|
54
|
+
}, [currentPage, router.href, app?.url])
|
|
55
|
+
|
|
56
|
+
const currentUrl = app?.url || router.href || '—'
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View gap="m">
|
|
60
|
+
<Text variant="title-tertiary">Dev</Text>
|
|
61
|
+
|
|
62
|
+
<View gap="xs" style={currentPageStyles}>
|
|
63
|
+
<Text variant="title-tertiary" style={{ marginBottom: 'var(--space-xs)' }}>
|
|
64
|
+
Current page
|
|
65
|
+
</Text>
|
|
66
|
+
<Text style={monoStyle}>
|
|
67
|
+
<strong>id:</strong> {app?.pageId || '—'}
|
|
68
|
+
</Text>
|
|
69
|
+
<Text style={monoStyle}>
|
|
70
|
+
<strong>path:</strong> {currentPathLabel}
|
|
71
|
+
</Text>
|
|
72
|
+
<Text style={monoStyle}>
|
|
73
|
+
<strong>url:</strong> {currentUrl}
|
|
74
|
+
</Text>
|
|
75
|
+
{packageDetailHref && currentPage?.package && (
|
|
76
|
+
<Button
|
|
77
|
+
variant="link"
|
|
78
|
+
href={packageDetailHref}
|
|
79
|
+
style={{ alignSelf: 'flex-start', padding: 0, height: 'auto' }}
|
|
80
|
+
>
|
|
81
|
+
View package ({currentPage.package})
|
|
82
|
+
</Button>
|
|
83
|
+
)}
|
|
84
|
+
</View>
|
|
85
|
+
|
|
86
|
+
<View gap="s">
|
|
87
|
+
<Text variant="title-tertiary">
|
|
88
|
+
Pages ({pages.length})
|
|
89
|
+
</Text>
|
|
90
|
+
|
|
91
|
+
{groups.map(({ key, label, pages: groupPages }) => (
|
|
92
|
+
<View key={key} gap="xs">
|
|
93
|
+
<Text style={{ fontWeight: 700, fontSize: '0.875rem' }}>{label}</Text>
|
|
94
|
+
<View gap="xs">
|
|
95
|
+
{groupPages.map((page) => {
|
|
96
|
+
const href = router.getHref(page.id)
|
|
97
|
+
const selected = page.id === app?.pageId
|
|
98
|
+
return (
|
|
99
|
+
<Button
|
|
100
|
+
key={page.id}
|
|
101
|
+
href={href}
|
|
102
|
+
variant={selected ? 'tag-active' : 'link'}
|
|
103
|
+
aria-current={selected ? 'page' : undefined}
|
|
104
|
+
style={{
|
|
105
|
+
display: 'block',
|
|
106
|
+
textAlign: 'left',
|
|
107
|
+
width: '100%',
|
|
108
|
+
padding: 'var(--space-xs) var(--space-s)',
|
|
109
|
+
height: 'auto',
|
|
110
|
+
whiteSpace: 'normal',
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<View gap="2px" style={{ alignItems: 'flex-start' }}>
|
|
114
|
+
<Text style={{ ...monoStyle, fontWeight: 600 }}>{page.id}</Text>
|
|
115
|
+
<Text style={{ ...monoStyle, opacity: 0.85 }}>
|
|
116
|
+
{formatPagePaths(page.path)}
|
|
117
|
+
</Text>
|
|
118
|
+
</View>
|
|
119
|
+
</Button>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
122
|
+
</View>
|
|
123
|
+
</View>
|
|
124
|
+
))}
|
|
125
|
+
</View>
|
|
126
|
+
</View>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
@@ -1,47 +1,28 @@
|
|
|
1
1
|
import React, { useState, useMemo, useCallback, useEffect } from 'react'
|
|
2
2
|
import { useResource } from '@ossy/sdk-react'
|
|
3
3
|
import { Overlay, Button, useTheme, View, Text } from '@ossy/design-system'
|
|
4
|
-
import {
|
|
4
|
+
import { DevPagesPanel } from './DevPagesPanel.jsx'
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
background: 'transparent',
|
|
8
|
-
display: 'content',
|
|
9
|
-
pointerEvents: 'none'
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const blobStyles = {
|
|
6
|
+
const fabStyles = {
|
|
13
7
|
boxShadow: '2px 2px 5px hsla(0, 0%, 0%, .2)',
|
|
14
8
|
borderRadius: '999px',
|
|
15
|
-
position: '
|
|
9
|
+
position: 'fixed',
|
|
16
10
|
right: 'var(--space-m)',
|
|
17
11
|
bottom: 'var(--space-m)',
|
|
18
12
|
cursor: 'pointer',
|
|
19
13
|
transition: 'transform .5s',
|
|
20
|
-
|
|
21
|
-
padding: 'var(--space-m)'
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const editorContainerStyles = {
|
|
25
|
-
position: 'absolute',
|
|
26
|
-
right: '0',
|
|
27
|
-
top: '0',
|
|
28
|
-
height: '100%',
|
|
29
|
-
width: '100%',
|
|
30
|
-
display: 'flex',
|
|
31
|
-
justifyContent: 'flex-end',
|
|
32
|
-
alignItems: 'stretch',
|
|
33
|
-
padding: '8px 32px'
|
|
14
|
+
zIndex: 101,
|
|
15
|
+
padding: 'var(--space-m)',
|
|
34
16
|
}
|
|
35
17
|
|
|
36
|
-
const
|
|
37
|
-
width: '
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
boxShadow: '2px 2px 5px hsla(0, 0%, 0%, .2)',
|
|
41
|
-
padding: '16px 16px 76px 16px',
|
|
42
|
-
overflowX: 'none',
|
|
18
|
+
const modalPanelStyles = {
|
|
19
|
+
width: 'min(720px, 80vw)',
|
|
20
|
+
minWidth: 'min(480px, 100%)',
|
|
21
|
+
maxHeight: '85vh',
|
|
43
22
|
overflowY: 'auto',
|
|
44
|
-
|
|
23
|
+
overflowX: 'hidden',
|
|
24
|
+
margin: '0 auto',
|
|
25
|
+
boxShadow: '2px 2px 5px hsla(0, 0%, 0%, .2)',
|
|
45
26
|
}
|
|
46
27
|
|
|
47
28
|
const ThemeSwitcher = () => {
|
|
@@ -59,7 +40,6 @@ const ThemeSwitcher = () => {
|
|
|
59
40
|
}
|
|
60
41
|
|
|
61
42
|
export const ThemeEditor = () => {
|
|
62
|
-
const app = useApp()
|
|
63
43
|
const { updateResourceContent } = useResource('PCX53TaGviq4_8KvK-VOp')
|
|
64
44
|
const [isEditorOpen, setIsEditorOpen] = useState(false)
|
|
65
45
|
const [viewCount, setViewCount] = useState(0)
|
|
@@ -68,8 +48,8 @@ export const ThemeEditor = () => {
|
|
|
68
48
|
const temporarilyUpdateTheme = () => {}
|
|
69
49
|
|
|
70
50
|
const toggleStyles = useMemo(() => !isEditorOpen
|
|
71
|
-
?
|
|
72
|
-
: { ...
|
|
51
|
+
? fabStyles
|
|
52
|
+
: { ...fabStyles, transform: 'rotate(-45deg)' }, [isEditorOpen])
|
|
73
53
|
|
|
74
54
|
const onToggle = useCallback(() => {
|
|
75
55
|
setIsEditorOpen(!isEditorOpen)
|
|
@@ -103,44 +83,35 @@ export const ThemeEditor = () => {
|
|
|
103
83
|
}, [isEditorOpen])
|
|
104
84
|
|
|
105
85
|
return (
|
|
106
|
-
|
|
107
|
-
|
|
86
|
+
<>
|
|
108
87
|
{
|
|
109
88
|
isEditorOpen && (
|
|
110
|
-
<
|
|
111
|
-
<View
|
|
112
|
-
<View
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
89
|
+
<Overlay isVisible={true} onClose={onToggle}>
|
|
90
|
+
<View layout="off-center" style={{ height: '100%', width: '100%' }}>
|
|
91
|
+
<View slot="content" style={modalPanelStyles}>
|
|
92
|
+
<View surface="primary" roundness="m" gap="m" inset="l">
|
|
93
|
+
<DevPagesPanel />
|
|
94
|
+
<View as="form" gap="s" onSubmit={onSaveTheme} onChange={onThemeChange}>
|
|
95
|
+
{Object.entries(theme).map(([name, value]) => (
|
|
96
|
+
<div style={{ marginBottom: '16px' }}>
|
|
97
|
+
<label style={{ display: 'block', fontFamily: 'sans-serif', marginBottom: '4px', fontWeight: 'bold' }}>{name}</label>
|
|
98
|
+
<input value={value} data-name={name} style={{ width: '100%', padding: '4px' }}/>
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
<Button type="submit" variant="cta">
|
|
102
|
+
Save
|
|
103
|
+
</Button>
|
|
104
|
+
</View>
|
|
105
|
+
<ThemeSwitcher/>
|
|
106
|
+
<Text>Views: {viewCount}</Text>
|
|
107
|
+
</View>
|
|
122
108
|
</View>
|
|
123
|
-
<ThemeSwitcher/>
|
|
124
|
-
<Text>Views: {viewCount}</Text>
|
|
125
109
|
</View>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<View>
|
|
129
|
-
Pages: {app?.pages?.length || 0}
|
|
130
|
-
</View>
|
|
131
|
-
|
|
132
|
-
<View>
|
|
133
|
-
{(app?.pages || []).map(page => (
|
|
134
|
-
<Button>{page.id}</Button>
|
|
135
|
-
))}
|
|
136
|
-
</View>
|
|
137
|
-
</View>
|
|
138
|
-
</div>
|
|
110
|
+
</Overlay>
|
|
139
111
|
)
|
|
140
112
|
}
|
|
141
113
|
|
|
142
114
|
<Button variant="cta" prefix="math-plus" style={toggleStyles} onClick={onToggle} />
|
|
143
|
-
|
|
144
|
-
</Overlay>
|
|
115
|
+
</>
|
|
145
116
|
)
|
|
146
117
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} pageId
|
|
3
|
+
* @returns {string} Group key for sorting and labeling (e.g. `booking`, `app`).
|
|
4
|
+
*/
|
|
5
|
+
export function getPageGroupKey (pageId) {
|
|
6
|
+
const slash = pageId.indexOf('/')
|
|
7
|
+
if (slash === -1) return 'app'
|
|
8
|
+
return pageId.slice(0, slash)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} groupKey
|
|
13
|
+
* @returns {string} Display label (e.g. `@ossy/booking`, `@ossy/app`).
|
|
14
|
+
*/
|
|
15
|
+
export function getPageGroupLabel (groupKey) {
|
|
16
|
+
if (groupKey === 'app') return '@ossy/app'
|
|
17
|
+
return `@ossy/${groupKey}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string | Record<string, string> | undefined} path
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function formatPagePaths (path) {
|
|
25
|
+
if (!path) return '—'
|
|
26
|
+
if (typeof path === 'string') return path
|
|
27
|
+
return Object.entries(path)
|
|
28
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
29
|
+
.map(([lang, p]) => `${lang}: ${p}`)
|
|
30
|
+
.join(' · ')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Array<{ id: string, path?: string | Record<string, string> }>} pages
|
|
35
|
+
* @returns {Array<{ key: string, label: string, pages: typeof pages }>}
|
|
36
|
+
*/
|
|
37
|
+
export function groupPagesByFeature (pages) {
|
|
38
|
+
const byKey = new Map()
|
|
39
|
+
|
|
40
|
+
for (const page of pages) {
|
|
41
|
+
const key = getPageGroupKey(page.id)
|
|
42
|
+
if (!byKey.has(key)) byKey.set(key, [])
|
|
43
|
+
byKey.get(key).push(page)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Array.from(byKey.entries())
|
|
47
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
48
|
+
.map(([key, groupPages]) => ({
|
|
49
|
+
key,
|
|
50
|
+
label: getPageGroupLabel(key),
|
|
51
|
+
pages: [...groupPages].sort((a, b) => a.id.localeCompare(b.id)),
|
|
52
|
+
}))
|
|
53
|
+
}
|