diff --git a/src/RDFXMLPane.js b/src/RDFXMLPane.js
index 34fd4a94..877d2ec2 100644
--- a/src/RDFXMLPane.js
+++ b/src/RDFXMLPane.js
@@ -53,6 +53,7 @@ export const RDFXMLPane = {
sz.setBase(subject.uri)
const str = sz.statementsToXML(sts)
const pre = myDocument.createElement('PRE')
+ pre.setAttribute('style', 'overflow-x: auto; max-width: 100%; box-sizing: border-box;')
pre.appendChild(myDocument.createTextNode(str))
div.appendChild(pre)
return div
diff --git a/src/dashboard/dashboardPane.ts b/src/dashboard/dashboardPane.ts
index 385de0c4..c654fd1d 100644
--- a/src/dashboard/dashboardPane.ts
+++ b/src/dashboard/dashboardPane.ts
@@ -60,9 +60,7 @@ function buildDashboard (container: HTMLElement, context: DataBrowserContext) {
// console.log('build dashboard')
// @@ TODO get a proper type
const outliner: any = context.getOutliner(context.dom)
- outliner
- .getDashboard()
- .then((dashboard: HTMLElement) => container.appendChild(dashboard))
+ outliner.showDashboard({})
}
function buildHomePage (container: HTMLElement, subject: NamedNode) {
diff --git a/src/form/pane.js b/src/form/pane.js
index 373b1b7c..9c655615 100644
--- a/src/form/pane.js
+++ b/src/form/pane.js
@@ -103,7 +103,7 @@ export const formPane = {
complainIfBad
)
)
- e.setAttribute('style', 'float: right;')
+ e.setAttribute('style', 'margin-left: auto; display: block;')
}
}
const anchor = dom.createElement('a')
diff --git a/src/humanReadablePane.js b/src/humanReadablePane.js
index f054c9bb..fc951e06 100644
--- a/src/humanReadablePane.js
+++ b/src/humanReadablePane.js
@@ -185,7 +185,7 @@ const humanReadablePane = {
const clean = DOMPurify.sanitize(res)
frame.innerHTML = clean
frame.setAttribute('class', 'doc')
- frame.setAttribute('style', `border: 1px solid; padding: 1em; height: ${lines}em; width: 800px; resize: both; overflow: auto;`)
+ frame.setAttribute('style', `border: 1px solid; padding: 1em; height: ${lines}em; max-width: 100%; width: 100%; box-sizing: border-box; resize: both; overflow: auto;`)
}).catch(error => {
console.error('Error fetching markdown content:', error)
frame.innerHTML = '
Error loading content
'
@@ -195,7 +195,7 @@ const humanReadablePane = {
const setIframeAttributes = (frame, lines) => {
frame.setAttribute('src', subject.uri)
frame.setAttribute('class', 'doc')
- frame.setAttribute('style', `border: 1px solid; padding: 1em; height: ${lines}em; width: 800px; resize: both; overflow: auto;`)
+ frame.setAttribute('style', `border: 1px solid; padding: 1em; height: ${lines}em; max-width: 100%; width: 100%; box-sizing: border-box; resize: both; overflow: auto;`)
}
if (isMarkdown) {
diff --git a/src/index.ts b/src/index.ts
index 19ab9a77..24f2f449 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -21,24 +21,35 @@ import {
paneForIcon,
paneForPredicate,
register,
- byName
+ byName,
+ RenderEnvironment
} from 'pane-registry'
import { createContext } from './outline/context'
-import initMainPage from './mainPage'
+import { initMainPage, refreshUI } from './mainPage'
-function getOutliner (dom) {
+function getOutliner (dom, environment?: RenderEnvironment): OutlineManager {
if (!dom.outlineManager) {
const context = createContext(
dom,
{ list, paneForIcon, paneForPredicate, register, byName },
store as LiveStore,
- solidLogicSingleton
+ solidLogicSingleton,
+ environment
)
dom.outlineManager = new OutlineManager(context)
+ } else if (environment) {
+ dom.outlineManager.context = dom.outlineManager.context || {}
+ dom.outlineManager.context.environment = environment
}
return dom.outlineManager
}
+function updateEnvironment (outliner: OutlineManager, environment: RenderEnvironment) {
+ if (!outliner) return
+ outliner.context = outliner.context || {}
+ outliner.context.environment = environment
+}
+
if (typeof window !== 'undefined') {
getOutliner(window.document)
}
@@ -53,9 +64,11 @@ registerPanes((cjsOrEsModule: any) => register(cjsOrEsModule.default || cjsOrEsM
export {
OutlineManager,
getOutliner,
+ updateEnvironment,
UI,
versionInfo,
initMainPage,
+ refreshUI,
list, // from paneRegistry
paneForIcon, // from paneRegistry
paneForPredicate, // from paneRegistry
diff --git a/src/mainPage/header.ts b/src/mainPage/header.ts
index 61aa0170..0c290b61 100644
--- a/src/mainPage/header.ts
+++ b/src/mainPage/header.ts
@@ -1,80 +1,210 @@
-import { authSession, authn } from 'solid-logic'
-import { icons, initHeader } from 'solid-ui'
+import { authSession, authn, store } from 'solid-logic'
+import { icons, widgets, utils } from 'solid-ui'
+import 'solid-ui/components/header'
+import type { Header, HeaderMenuItem, HeaderAccountMenuItem, HeaderAuthState } from 'solid-ui/components/header'
+import { OutlineManager } from '../outline/manager'
+import { LiveStore } from 'rdflib'
/**
* menu icons
*/
const HELP_MENU_ICON = icons.iconBase + 'noun_help.svg'
const SOLID_ICON_URL = 'https://solidproject.org/assets/img/solid-emblem.svg'
-
/**
* menu elements
*/
-const USER_GUIDE_MENU_ITEM = 'User guide'
-const REPORT_A_PROBLEM_MENU_ITEM = 'Report a problem'
-const SHOW_YOUR_PROFILE_MENU_ITEM = 'Show your profile'
-const LOG_OUT_MENU_ITEM = 'Log out'
+const SIGN_IN_MENU_ITEM = 'Log In'
+const SIGN_OUT_MENU_ITEM = 'Log Out'
+const SIGN_UP_MENU_ITEM = 'Sign Up'
+const ACCOUNT_MENU_LABEL = '▼'
+const SIGN_UP__MENU_LINK = 'https://solidproject.org/get_a_pod'
-type UserMenuItem = { label: string; onclick: () => void }
-/**
- * URLS
- */
-const USER_GUIDE_MENU_URL = 'https://solidos.github.io/userguide/'
-const REPORT_A_PROBLEM_MENU_URL = 'https://github.com/solidos/solidos/issues'
+// data structure extracted for solid-ui-header binding
+export const HELP_MENU_LIST = [
+ { label: 'User guide', url: 'https://solidos.github.io/userguide/', target: '_blank' },
+ { label: 'Report a problem', url: 'https://github.com/solidos/solidos/issues', target: '_blank' }
+]
-export async function createHeader (store, outliner) {
- initHeader(store, await setUserMenu(outliner), setHeaderOptions())
-}
+const HEADER_MOBILE_STYLE_ID = 'solid-ui-header-mobile-style'
-function setHeaderOptions () {
- const helpMenuList = [
- { label: USER_GUIDE_MENU_ITEM, url: USER_GUIDE_MENU_URL, target: '_blank' },
- { label: REPORT_A_PROBLEM_MENU_ITEM, url: REPORT_A_PROBLEM_MENU_URL, target: '_blank' }
- ]
- const headerOptions = { logo: SOLID_ICON_URL, helpIcon: HELP_MENU_ICON, helpMenuList }
+type ManagedHeader = Header & {
+ __solidPanesListenersAttached?: boolean
+ __solidPanesOutliner?: OutlineManager
+}
- return headerOptions
+function ensureMobileHeaderStyles () {
+ if (document.getElementById(HEADER_MOBILE_STYLE_ID)) return
+ const style = document.createElement('style')
+ style.id = HEADER_MOBILE_STYLE_ID
+ style.textContent = `
+ solid-ui-header[layout="mobile"]::part(logo) {
+ display: none !important;
+ }
+ `
+ document.head.appendChild(style)
}
-async function setUserMenu (outliner: any) {
- // @ts-ignore: showProfile is used conditionally
- const showProfile = {
- label: SHOW_YOUR_PROFILE_MENU_ITEM,
- onclick: () => openUserProfile(outliner)
+export async function createHeader (store: LiveStore, outliner: OutlineManager) {
+ ensureMobileHeaderStyles()
+
+ const header = (document.querySelector('solid-ui-header') || document.createElement('solid-ui-header')) as ManagedHeader
+ const isNewHeader = !header.isConnected
+ if (!header.id) {
+ header.id = 'mainSolidUiHeader'
}
+ header.__solidPanesOutliner = outliner
- const logOut: UserMenuItem = {
- label: LOG_OUT_MENU_ITEM,
- onclick: () => {
- authSession.logout()
+ // ensure it is in DOM (before MainContent for consistency)
+ const main = document.getElementById('MainContent')
+ if (!header.isConnected) {
+ if (main && main.parentNode) {
+ main.parentNode.insertBefore(header, main)
+ } else {
+ document.body.prepend(header)
}
}
- // the order of the menu is important here, show profile first and logout last
- let userMenuList:UserMenuItem[] = [] // was [showProfile]
- userMenuList = userMenuList.concat(await getMenuItems(outliner))
- userMenuList.push(logOut)
+ await refreshHeader(outliner, header)
- return userMenuList
-}
+ if (isNewHeader) {
+ header.__solidPanesListenersAttached = false
+ }
-// Does not work to jump to user profile,
-function openUserProfile (outliner: any) {
- outliner.GotoSubject(authn.currentUser(), true, undefined, true, undefined)
- location.reload()
+ attachHeaderListeners(header)
+
+ return header
}
-async function getMenuItems (outliner: any) {
- const items = await outliner.getDashboardItems()
- return items.map((element) => {
- return {
- label: element.label,
- onclick: () => openDashboardPane(outliner, element.tabName || element.paneName)
+function attachHeaderListeners (header: ManagedHeader) {
+ if (header.__solidPanesListenersAttached) return
+
+ const refreshCurrentHeader = async () => {
+ const outliner = header.__solidPanesOutliner
+ if (!outliner) return
+ await refreshHeader(outliner, header)
+ }
+
+ authSession.events.on('login', refreshCurrentHeader)
+ authSession.events.on('logout', refreshCurrentHeader)
+ authSession.events.on('sessionRestore', refreshCurrentHeader)
+
+ header.addEventListener('auth-action-select', async (e: Event) => {
+ const outliner = header.__solidPanesOutliner
+ if (!outliner) return
+
+ const detail = (e as CustomEvent).detail
+ if (detail?.role === 'login') {
+ await refreshCurrentHeader()
+ outliner.showDashboard({ pane: 'profile' }) // upon successful login, we open the dashboard pane
}
})
-}
-async function openDashboardPane (outliner: any, pane: string): Promise {
- outliner.showDashboard({
- pane
+ header.addEventListener('signup-success', async () => {
+ // do nothing
+ })
+
+ header.addEventListener('account-menu-select', async (e: Event) => {
+ const outliner = header.__solidPanesOutliner
+ if (!outliner) return
+
+ const detail = (e as CustomEvent).detail
+ if (detail?.action === 'logout') {
+ console.log('-----Logging out')
+ await refreshCurrentHeader()
+ openUserProfile(outliner)
+ } else if (detail?.action === 'show-profile') {
+ // TODO see if this can be consolidated
+ if (!authn.currentUser()) {
+ openUserProfile(outliner)
+ } else {
+ outliner.showDashboard({ pane: 'profile' })
+ }
+ }
})
+
+ header.__solidPanesListenersAttached = true
+}
+
+export async function refreshHeader (outliner: OutlineManager, headerElement?: Header) {
+ ensureMobileHeaderStyles()
+ const headerOptions = setHeaderOptions(outliner)
+ const header = headerElement || document.querySelector('solid-ui-header') as Header | null
+ if (!header) return null
+
+ header.theme = headerOptions.theme
+ header.layout = headerOptions.layout
+ header.brandLink = headerOptions.brandLink
+ header.logo = headerOptions.layout === 'desktop' ? headerOptions.logo : ''
+ header.helpIcon = headerOptions.helpIcon
+ header.helpMenuList = headerOptions.helpMenuList
+ header.authState = headerOptions.authState
+ header.loginAction = headerOptions.loginAction
+ header.signUpAction = headerOptions.signUpAction
+ header.accountMenu = await setUserMenu()
+ header.logoutLabel = headerOptions.logoutLabel
+ header.accountLabel = headerOptions.accountLabel
+ header.accountAvatar = headerOptions.accountAvatar
+
+ return header
+}
+
+function setHeaderOptions (outliner: OutlineManager) {
+ const currentUser = authn.currentUser()
+ const isAuthenticated = !!currentUser
+ const authState: HeaderAuthState = isAuthenticated ? 'logged-in' : 'logged-out'
+ const layout: Header['layout'] = outliner.context?.environment?.layout === 'mobile' ? 'mobile' : 'desktop'
+ const theme: Header['theme'] = outliner.context?.environment?.theme === 'dark' ? 'dark' : 'light'
+
+ const headerOptions = {
+ logo: SOLID_ICON_URL,
+ helpIcon: HELP_MENU_ICON,
+ helpMenuList: HELP_MENU_LIST,
+ layout,
+ theme,
+ brandLink: '/',
+ authState,
+ loginAction: {
+ label: SIGN_IN_MENU_ITEM
+ } as HeaderMenuItem,
+ signUpAction: {
+ label: SIGN_UP_MENU_ITEM,
+ url: SIGN_UP__MENU_LINK,
+ } as HeaderMenuItem,
+ logoutLabel: SIGN_OUT_MENU_ITEM,
+ accountLabel: isAuthenticated ? ACCOUNT_MENU_LABEL : '',
+ accountAvatar: isAuthenticated ? widgets.findImage(currentUser) : undefined
+ }
+
+ return headerOptions
+}
+
+async function setUserMenu () {
+ const me = authn.currentUser()
+ if (!me) {
+ return []
+ }
+
+ try {
+ await store.fetcher.load(me.doc())
+ } catch (err) {
+ console.error('Unable to load user profile', err)
+ }
+
+ const accountMenu: HeaderAccountMenuItem[] = [
+ {
+ label: utils.label(me),
+ avatar: widgets.findImage(me),
+ webid: me.value,
+ action: 'show-profile'
+ },
+ // TODO add all my available accounts
+ ]
+
+ return accountMenu
+}
+
+// TODO see if these 2 calls can be consolidated
+function openUserProfile (outliner: OutlineManager) {
+ console.log('-----Opening user profile')
+ outliner.GotoSubject(authn.currentUser(), true, undefined, true, undefined)
+ location.reload()
}
diff --git a/src/mainPage/index.ts b/src/mainPage/index.ts
index 8c8b5345..b9ebdddb 100644
--- a/src/mainPage/index.ts
+++ b/src/mainPage/index.ts
@@ -4,17 +4,45 @@
*/
import { LiveStore, NamedNode } from 'rdflib'
-import { getOutliner } from '../index'
+import { getOutliner, OutlineManager } from '../index'
import { createHeader } from './header'
import { createFooter } from './footer'
+import { createLeftSideMenu, refreshMenu } from './menu'
-export default async function initMainPage (store: LiveStore, uri?: string | NamedNode | null) {
- const outliner = getOutliner(document)
+export { refreshMenu as updateMenuLayout } from './menu'
+
+function ensureMainContent () {
+ let main = document.getElementById('MainContent') as HTMLElement | null
+ if (!main) {
+ main = document.createElement('main')
+ main.id = 'MainContent'
+ main.setAttribute('role', 'main')
+ main.setAttribute('tabindex', '-1')
+ main.setAttribute('aria-live', 'polite')
+ document.body.appendChild(main)
+ }
+ return main
+}
+
+export async function initMainPage (
+ store: LiveStore,
+ uri?: string | NamedNode | null,
+ environment?: any
+) {
+ ensureMainContent()
+ const outliner = getOutliner(document, environment)
uri = uri || window.location.href
let subject = uri
if (typeof uri === 'string') subject = store.sym(uri)
outliner.GotoSubject(subject, true, undefined, true, undefined)
const header = await createHeader(store, outliner)
+ const menu = createLeftSideMenu(outliner)
const footer = createFooter(store)
- return Promise.all([header, footer])
+ return Promise.all([header, menu, footer])
}
+
+export function refreshUI (outliner: OutlineManager) {
+ refreshMenu(outliner.context?.environment?.layout === 'mobile' ? 'mobile' : 'desktop')
+}
+
+export default initMainPage
diff --git a/src/mainPage/menu.css b/src/mainPage/menu.css
new file mode 100644
index 00000000..55fe8408
--- /dev/null
+++ b/src/mainPage/menu.css
@@ -0,0 +1,83 @@
+.menu-content {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.menu-item {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ border: 0;
+ border-radius: 0;
+ padding: 0.5rem 0.75rem;
+ background: transparent;
+ color: var(--color-text);
+ text-decoration: none;
+ width: 100%;
+ text-align: left;
+ cursor: pointer;
+}
+
+.menu-item-icon {
+ width: 1rem;
+ height: 1rem;
+ flex: 0 0 1rem;
+ object-fit: contain;
+}
+
+.menu-item-label {
+ min-width: 0;
+}
+
+.menu-item:hover,
+.menu-item:focus {
+ background: #e6dcff;
+}
+
+.menu-item-active,
+.menu-item-active:hover,
+.menu-item-active:focus {
+ background: #cbb9ff;
+}
+
+.menu-toggle {
+ position: fixed;
+ top: 0.5rem;
+ left: 2.75rem;
+ z-index: 300;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.5rem;
+ height: 2.5rem;
+ border: 1px solid var(--color-border);
+ background: var(--color-background);
+ color: var(--color-text);
+ border-radius: 0.4rem;
+ margin: 0;
+}
+
+.menu-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.35);
+ z-index: 100;
+}
+
+.app-nav.mobile-hidden {
+ transform: translateX(-100%);
+ transition: transform 0.25s ease-in-out;
+ position: fixed;
+ left: 0;
+ top: 4.5rem;
+ bottom: 0;
+ z-index: 110;
+ width: min(80vw, 320px);
+}
+
+.app-nav.mobile-visible {
+ transform: translateX(0);
+ display: block;
+ visibility: visible;
+}
\ No newline at end of file
diff --git a/src/mainPage/menu.ts b/src/mainPage/menu.ts
new file mode 100644
index 00000000..0262c2f0
--- /dev/null
+++ b/src/mainPage/menu.ts
@@ -0,0 +1,235 @@
+import './menu.css'
+import { OutlineManager } from '../outline/manager'
+import { authSession } from 'solid-logic'
+
+type MenuItem = {
+ id?: string
+ icon?: string
+ paneName?: string
+ label: string
+ onclick: () => void | Promise
+}
+
+const ensureMenuSkeleton = () => {
+ const root = document.querySelector('[role="main"]') || document.body
+
+ let navMenu = document.getElementById('NavMenu') as HTMLElement | null
+ if (!navMenu) {
+ navMenu = document.createElement('nav')
+ navMenu.id = 'NavMenu'
+ navMenu.className = 'app-nav'
+ navMenu.setAttribute('aria-label', 'App navigation')
+
+ const authStateEl = document.createElement('div')
+ authStateEl.id = 'AuthState'
+ authStateEl.className = 'menu-auth-state'
+ navMenu.appendChild(authStateEl)
+
+ const contentEl = document.createElement('div')
+ contentEl.id = 'NavMenuContent'
+ contentEl.className = 'menu-content'
+ navMenu.appendChild(contentEl)
+
+ root.insertBefore(navMenu, root.firstChild)
+ }
+
+ let toggle = document.getElementById('MenuToggleBtn') as HTMLButtonElement | null
+ if (!toggle) {
+ toggle = document.createElement('button')
+ toggle.id = 'MenuToggleBtn'
+ toggle.className = 'menu-toggle'
+ toggle.type = 'button'
+ toggle.setAttribute('aria-label', 'Toggle navigation menu')
+ toggle.textContent = '\u2630'
+ root.insertBefore(toggle, root.firstChild)
+ }
+
+ let overlay = document.getElementById('MenuOverlay') as HTMLElement | null
+ if (!overlay) {
+ overlay = document.createElement('div')
+ overlay.id = 'MenuOverlay'
+ overlay.className = 'menu-overlay'
+ overlay.hidden = true
+ document.body.appendChild(overlay)
+ }
+}
+
+const getMenuItems = async (outliner: any): Promise