@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/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
- <AppContext.Provider value={contextSettings}>
24
- <ComponentSlotsProvider slots={components || {}}>
25
- <Theme theme={appSettings.theme} themes={appSettings.themes}>
26
- <WorkspaceProvider sdk={sdk}>
27
- <Router {...appSettings} pages={appSettings.pages || []}>
28
- {children}
29
- </Router>
30
- {appSettings.devMode && <ThemeEditor />}
31
- </WorkspaceProvider>
32
- </Theme>
33
- </ComponentSlotsProvider>
34
- </AppContext.Provider>
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 { useApp } from './AppContext.js'
4
+ import { DevPagesPanel } from './DevPagesPanel.jsx'
5
5
 
6
- const overlayStyles = {
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: 'absolute',
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
- pointerEvents: 'auto',
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 editorStyles = {
37
- width: '100%',
38
- maxWidth: '350px',
39
- height: '100%',
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
- pointerEvents: 'auto'
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
- ? blobStyles
72
- : { ...blobStyles, transform: 'rotate(-45deg)' }, [isEditorOpen])
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
- <Overlay isVisible={true} style={overlayStyles}>
107
-
86
+ <>
108
87
  {
109
88
  isEditorOpen && (
110
- <div style={editorContainerStyles}>
111
- <View surface="primary" roundness="m" gap="m" style={editorStyles}>
112
- <View as="form" gap="s" onSubmit={onSaveTheme} onChange={onThemeChange}>
113
- {Object.entries(theme).map(([name, value]) => (
114
- <div style={{ marginBottom: '16px' }}>
115
- <label style={{ display: 'block', fontFamily: 'sans-serif', marginBottom: '4px', fontWeight: 'bold' }}>{name}</label>
116
- <input value={value} data-name={name} style={{ width: '100%', padding: '4px' }}/>
117
- </div>
118
- ))}
119
- <Button type="submit" variant="cta">
120
- Save
121
- </Button>
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
- <View>
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
+ }