diff --git a/README.md b/README.md
index ef5f4287..8afbc0e0 100644
--- a/README.md
+++ b/README.md
@@ -78,3 +78,30 @@ contacts (A VCARD Address Book, Group, Individual, Organization) can be handled
pane. Any other pane which wants to deal with contacts can just use the pane within its own user interface.

+
+
+## Generative AI usage
+The SolidOS team is using GitHub Copilot integrated in Visual Studio Code.
+We have added comments in the code to make it explicit which parts are 100% written by AI.
+
+### Prompt usage hitory:
+* Model Claude Opus 4.6: Initially solid-panes is loaded into an HTML shell form mashlib that looks like ... Also, an iFrame is rendered inside the `
` for “isolated pane rendering”. Analyze the solid-panes code for what it uses from this HTML and suggest a new HTML structure which is mobile and accessibility friendly. Let's go ahead and make changes in this code as suggested to accommodate the new databrowser HTML.
+
+* Raptor mini: take a look at how I wired the environment from mashlib into solid-panes. It is not quite right, can you suggest fixes?
+
+* Raptor mini: Update the code to use the new solid-ui-header component. Keep in mind the log in and sign up are wired in specific ways.
+
+* Auto: change the menu to fill up the menu items like in the code: 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)
+}
+})
+}
+
+* Auto: each #sym:MenuItem has an icon which i want displayed on the left side of each menu item when rendered
+
+* Auto: don't add each menu item in a button looking border. Simply list them.
+Upon hover apply background color e6dcff and selected or active to be background color: cbb9ff
\ No newline at end of file
diff --git a/dev/dev-mash.css b/dev/dev-mash.css
index 8aa75274..8b36eea0 100644
--- a/dev/dev-mash.css
+++ b/dev/dev-mash.css
@@ -8,74 +8,46 @@
@import url("./dev-light.css");
@import url("./dev-mash-utilities.css");
-/* I couldn't find the code for the collapse image. this is a quick work around
-to make the collapsing easier to use ( the triangles dont jump 20 pixels). ~cm2
-*/
-img[title="Hide details."] {
- float: left;
-}
-
-html {
- height: 100%;
- line-height: 1.15;
-}
-body {
- height: 100%;
- background-color: var(--color-background);
- color: var(--color-text);
+html, body {
+ margin: 0;
+ padding: 0;
font-family: var(--font-family-base);
-}
-
-/* Improved heading hierarchy */
-h1, h2, h3, h4, h5, h6 {
- color: var(--color-primary);
- font-weight: 600;
- line-height: var(--line-height-tight);
- margin-top: 0;
- margin-bottom: var(--spacing-sm);
-}
-
-h1 { font-size: 2em; } /* 32px */
-h2 { font-size: 1.5em; } /* 24px */
-h3 { font-size: 1.25em; } /* 20px */
-h4 { font-size: 1.125em; }/* 18px */
-h5, h6 { font-size: 1em; }/* 16px */
-
-/* Better paragraph spacing */
-p {
- margin-bottom: var(--spacing-md);
+ font-size: var(--font-size-base);
line-height: var(--line-height-base);
- max-width: 65ch; /* Optimal reading width */
-}
-
-/* Improved link accessibility */
-a {
- color: var(--color-primary);
- text-decoration: underline;
- text-underline-offset: 0.125em;
- text-decoration-thickness: 0.0625em;
-}
-
-a:hover, a:focus {
- text-decoration-thickness: 0.125em;
+ background: var(--color-background);
+ color: var(--color-text);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
}
-
-/* Main page elements in databrowser.html */
-#PageBody {
+/* ── App layout ── */
+body {
display: flex;
flex-direction: column;
+ min-height: 100dvh; /* dvh = dynamic viewport for mobile chrome */
}
-#DummyUUID {
- flex: 1 0 auto;
-}
+
#PageHeader {
+ position: sticky;
+ top: 0;
+ z-index: 100;
flex-shrink: 0;
}
+
+#MainContent {
+ flex: 1 1 auto;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch; /* smooth iOS scroll */
+ padding: clamp(0.5rem, 2vw, 1.5rem); /* responsive gutter */
+ container-type: inline-size; /* enable @container queries */
+}
+
#PageFooter {
flex-shrink: 0;
}
+
.warning {
color: var(--color-warning);
}
@@ -83,6 +55,7 @@ a:hover, a:focus {
background-color: var(--color-selected-bg);
}
+/* used in profile-pane as success button */
.licOkay {
background-color: var(--color-success-bg);
}
@@ -1409,3 +1382,56 @@ End of styles for tableViewPane
-moz-box-shadow: $x-axis $y-axis $blur $color;
-o-box-shadow: $x-axis $y-axis $blur $color;
}
+
+/* Generated by AI */
+@media screen and (max-width: 768px) {
+ #PageHeader,
+ #PageFooter {
+ width: 100%;
+ }
+
+ #MainContent {
+ padding-top: 0.75rem;
+ padding-right: max(0.75rem, env(safe-area-inset-right));
+ padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
+ padding-left: max(0.75rem, env(safe-area-inset-left));
+ }
+
+ .TabulatorOutline,
+ .outline-view {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .TabulatorOutline table,
+ .outline-view table {
+ min-width: 100%;
+ }
+
+ img.outlineImage,
+ img.pic,
+ img.foafPic {
+ height: auto;
+ max-width: 100%;
+ }
+
+ div.description,
+ div.premises,
+ div.justification,
+ div.mildNotice {
+ box-sizing: border-box;
+ margin-left: 0;
+ margin-right: 0;
+ width: 100%;
+ }
+}
+
+html[data-layout="mobile"] #PageBody {
+ min-height: var(--app-height, 100dvh);
+}
+
+html[data-layout="mobile"] #MainContent {
+ overscroll-behavior-x: contain;
+}
+
+/* END of Generated by AI */
\ No newline at end of file
diff --git a/dev/loader.ts b/dev/loader.ts
index 207e35db..eb9f3106 100644
--- a/dev/loader.ts
+++ b/dev/loader.ts
@@ -5,6 +5,7 @@ import { solidLogicSingleton, store, authSession } from 'solid-logic'
import { getOutliner } from '../src'
import Pane from 'profile-pane'
import './dev-mash.css'
+import { DataBrowserContext, RenderEnvironment } from 'pane-registry'
// Add custom properties to the Window interface for TypeScript
declare global {
@@ -27,7 +28,15 @@ async function renderPane (uri: string) {
await new Promise((resolve, reject) => {
store.fetcher.load(doc).then(resolve, reject)
})
- const context = {
+
+ const devEnvironment : RenderEnvironment = {
+ layout: 'desktop', // or 'mobile'
+ layoutPreference: 'desktop', // or 'mobile' or 'auto'
+ inputMode: 'pointer', // or 'touch'
+ theme: 'light', // or 'dark'
+ viewport: { width: 800, height: 480 } // this is the default viewport for the browser window
+ }
+ const context : DataBrowserContext = {
// see https://github.com/solidos/solid-panes/blob/005f90295d83e499fd626bd84aeb3df10135d5c1/src/index.ts#L30-L34
dom: document,
getOutliner,
@@ -35,7 +44,8 @@ async function renderPane (uri: string) {
store: store,
paneRegistry,
logic: solidLogicSingleton
- }
+ },
+ environment: devEnvironment
}
console.log(subject, context)
diff --git a/package.json b/package.json
index 6cd14a2f..5c53477d 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,9 @@
"lint": "eslint",
"lint-fix": "eslint --fix",
"typecheck": "tsc --noEmit",
+ "watch-js": "babel src --out-dir dist --source-maps --extensions '.ts,.js' --watch",
+ "watch-dist": "webpack --watch",
+ "watch-types": "tsc --emitDeclarationOnly --watch --preserveWatchOutput",
"typecheck-test": "tsc --noEmit -p tsconfig.test.json",
"test": "jest --no-coverage",
"test-watch": "npm run lint && jest --onlyChanged --watch",
@@ -30,7 +33,7 @@
"prepublishOnly": "npm run build && npm run lint && npm test",
"preversion": "npm run lint && npm run typecheck && npm test",
"postversion": "git push origin main --follow-tags",
- "watch": "npm run build-version && babel src -d dist --source-maps --extensions '.ts,.js' --watch",
+ "watch": "npm run build-version && concurrently -k -n babel,types,webpack \"npm:watch-js\" \"npm:watch-types\" \"npm:watch-dist\"",
"start": "webpack serve --config webpack.dev.config.mjs --open"
},
"repository": {
@@ -76,7 +79,8 @@
"overrides": {
"rdflib": "$rdflib",
"solid-logic": "$solid-logic",
- "solid-ui": "$solid-ui"
+ "solid-ui": "$solid-ui",
+ "pane-registry": "$pane-registry"
},
"devDependencies": {
"@babel/cli": "^7.28.6",
@@ -93,6 +97,7 @@
"babel-loader": "^10.0.0",
"babel-plugin-inline-import": "^3.0.0",
"buffer": "^6.0.3",
+ "concurrently": "^9.2.1",
"copy-webpack-plugin": "^14.0.0",
"css-loader": "^7.1.4",
"eslint": "^9.39.3",
diff --git a/src/dashboard/basicPreferences.ts b/src/dashboard/basicPreferences.ts
index 5523d220..2b40e1b1 100644
--- a/src/dashboard/basicPreferences.ts
+++ b/src/dashboard/basicPreferences.ts
@@ -15,6 +15,7 @@ export const basicPreferencesPane: PaneDefinition = {
// The subject should be the logged in user.
render: (subject, context) => {
const dom = context.dom
+ // @ts-ignore
const store = context.session.store as Store
function complainIfBad (ok: Boolean, mess: any) {
diff --git a/src/dashboard/dashboardPane.ts b/src/dashboard/dashboardPane.ts
index 385de0c4..ab41aa7c 100644
--- a/src/dashboard/dashboardPane.ts
+++ b/src/dashboard/dashboardPane.ts
@@ -7,12 +7,7 @@ import { DataBrowserContext, PaneDefinition } from 'pane-registry'
export const dashboardPane: PaneDefinition = {
icon: icons.iconBase + 'noun_547570.svg',
name: 'dashboard',
- label: subject => {
- if (subject.termType === 'NamedNode' && subject.uri === subject.site().uri) {
- return 'Dashboard'
- }
- return null
- },
+ label: () => { return 'Dashboard' }, // we do not care if it is a WebID or not, not yet
render: (subject, context) => {
console.log('Dashboard Pane Render')
const dom = context.dom
@@ -46,14 +41,24 @@ function buildPage (
container: HTMLElement,
webId: NamedNode | null,
context: DataBrowserContext,
- subject: NamedNode
+ subject: NamedNode | null
) {
- // if uri then SolidOS is a browse.html web app
const uri = (new URL(window.location.href)).searchParams.get('uri')
- if (webId && (uri || webId.site().uri === subject.site().uri)) {
+
+ if (uri && webId) {
+ return buildDashboard(container, context)
+ }
+
+ if (!uri && subject) {
+ return buildHomePage(container, subject)
+ }
+
+ if (!uri && !subject && webId) {
return buildDashboard(container, context)
}
- return buildHomePage(container, subject)
+
+ const fallbackSubject = subject || webId || store.sym(window.location.href)
+ return buildHomePage(container, fallbackSubject)
}
function buildDashboard (container: HTMLElement, context: DataBrowserContext) {
diff --git a/src/dashboard/homepage.ts b/src/dashboard/homepage.ts
index 360477f0..a7fb8e5f 100644
--- a/src/dashboard/homepage.ts
+++ b/src/dashboard/homepage.ts
@@ -1,12 +1,12 @@
-import { Fetcher, IndexedFormula, NamedNode, sym } from 'rdflib'
-import { ns } from 'solid-ui'
+import { Fetcher, IndexedFormula, NamedNode } from 'rdflib'
+import { loadProfileFromURI, getName } from '../profileUtils/ownerProfile'
export async function generateHomepage (
- subject: NamedNode,
+ uri: NamedNode,
store: IndexedFormula,
fetcher: Fetcher
): Promise
{
- const ownersProfile = await loadProfile(subject, store, fetcher)
+ const ownersProfile = await loadProfileFromURI(uri, store, fetcher)
const name = getName(store, ownersProfile)
const wrapper = document.createElement('div')
@@ -51,45 +51,3 @@ function createTitle (uri: string, name: string): HTMLElement {
return title
}
-
-async function loadProfile (
- subject: NamedNode,
- store: IndexedFormula,
- fetcher: Fetcher
-): Promise {
- const pod = subject.site().uri
- // TODO: This is a hack - we cannot assume that the profile is at this document, but we will live with it for now
- const webId = sym(`${pod}profile/card#me`)
- try {
- await fetcher.load(webId)
- return webId
- } catch (err) {
- // Fall back to pod root and any discovered profile links.
- }
-
- try {
- await fetcher.load(subject)
- } catch (err) {
- return subject
- }
-
- const primaryTopic = store.any(subject, ns.foaf('primaryTopic'), null, subject.doc())
- if (primaryTopic && primaryTopic.termType === 'NamedNode') {
- try {
- await fetcher.load(primaryTopic as NamedNode)
- return primaryTopic as NamedNode
- } catch (err) {
- return subject
- }
- }
-
- return subject
-}
-
-function getName (store: IndexedFormula, ownersProfile: NamedNode): string {
- return (
- store.anyValue(ownersProfile, ns.vcard('fn'), null, ownersProfile.doc()) ||
- store.anyValue(ownersProfile, ns.foaf('name'), null, ownersProfile.doc()) ||
- new URL(ownersProfile.uri).host.split('.')[0]
- )
-}
diff --git a/src/home/homePane.ts b/src/home/homePane.ts
index 92d17fe0..b1837f89 100644
--- a/src/home/homePane.ts
+++ b/src/home/homePane.ts
@@ -8,7 +8,7 @@
**
*/
-import { PaneDefinition } from 'pane-registry'
+import { DataBrowserContext, PaneDefinition } from 'pane-registry'
import { NamedNode } from 'rdflib'
import { authn } from 'solid-logic'
import { create, icons, login } from 'solid-ui'
@@ -29,7 +29,7 @@ const HomePaneSource: PaneDefinition = {
return 'home'
},
- render: function (subject, context) {
+ render: function (subject, context: DataBrowserContext) {
const dom = context.dom
const showContent = async function () {
const homePaneContext = { div, dom, statusArea: div, me }
@@ -54,6 +54,7 @@ const HomePaneSource: PaneDefinition = {
const relevantPanes = await login.filterAvailablePanes(
context.session.paneRegistry.list
)
+ // @ts-ignore
create.newThingUI(creationContext, context, relevantPanes) // newUI Have to pass panes down
login.registrationList(homePaneContext, {}).then(function () {})
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/internal/internalPane.ts b/src/internal/internalPane.ts
index b041634a..a73aa584 100644
--- a/src/internal/internalPane.ts
+++ b/src/internal/internalPane.ts
@@ -20,6 +20,7 @@ const pane: PaneDefinition = {
render: function (subject, context) {
const dom = context.dom
+ // @ts-ignore
const store = context.session.store as Store
const canonizedSubject = store.canon(subject) as BlankNode | NamedNode | Variable
const types = store.findTypeURIs(canonizedSubject)
diff --git a/src/mainPage/footer.ts b/src/mainPage/footer.ts
index 77045023..2dbf90d4 100644
--- a/src/mainPage/footer.ts
+++ b/src/mainPage/footer.ts
@@ -8,6 +8,7 @@ const SOLID_PROJECT_URL = 'https://solidproject.org'
const SOLID_PROJECT_NAME = 'solidproject.org'
export function createFooter (store: LiveStore) {
+ // @ts-ignore
initFooter(store, setFooterOptions())
}
diff --git a/src/mainPage/header.ts b/src/mainPage/header.ts
index 61aa0170..12abe7d9 100644
--- a/src/mainPage/header.ts
+++ b/src/mainPage/header.ts
@@ -1,80 +1,201 @@
-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()
+ // Do not auto-open the profile pane after login.
+ // outliner.showDashboard({ pane: 'profile' })
}
})
-}
-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') {
+ await refreshCurrentHeader()
+ // Do not navigate to the profile after logout.
+ } else if (detail?.action === 'show-profile') {
+ // TODO see if this can be consolidated
+ if (authn.currentUser()) {
+ 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
}
diff --git a/src/mainPage/index.ts b/src/mainPage/index.ts
index 8c8b5345..da6df391 100644
--- a/src/mainPage/index.ts
+++ b/src/mainPage/index.ts
@@ -4,17 +4,46 @@
*/
import { LiveStore, NamedNode } from 'rdflib'
-import { getOutliner } from '../index'
-import { createHeader } from './header'
+import { getOutliner, OutlineManager } from '../index'
+import { createHeader, refreshHeader } 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'
+export { refreshHeader } from './header'
+
+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)
+ const subject: NamedNode = typeof uri === 'string' ? store.sym(uri) : uri
+ console.log('-----initMainPage GotoSubject ', subject)
outliner.GotoSubject(subject, true, undefined, true, undefined)
+
const header = await createHeader(store, outliner)
+ const menu = createLeftSideMenu(subject, outliner)
const footer = createFooter(store)
- return Promise.all([header, footer])
+ return Promise.all([header, menu, footer])
+}
+
+export async function refreshUI (outliner: OutlineManager) {
+ await refreshHeader(outliner)
+ refreshMenu(outliner.context.environment?.layout === 'mobile' ? 'mobile' : 'desktop')
}
diff --git a/src/mainPage/menu.css b/src/mainPage/menu.css
new file mode 100644
index 00000000..43ea8d22
--- /dev/null
+++ b/src/mainPage/menu.css
@@ -0,0 +1,84 @@
+.menu-content {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.menu-item {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ border: 0;
+ border-radius: var(--border-radius-sm, 0.2rem);
+ padding: 0.5rem 0.75rem;
+ background: transparent;
+ color: var(--color-text-heading, #000000);
+ 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: var(--color-header-menu-item-hover, #e6dcff);
+}
+
+.menu-item-active,
+.menu-item-active:hover,
+.menu-item-active:focus {
+ background: var(--color-header-menu-item-selected, #cbb9ff);
+ color: var(--color-header-menu-item-text-selected, #7c4cff);
+}
+
+.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: var(--border-radius-sm, 0.2rem);
+ 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..cca659ca
--- /dev/null
+++ b/src/mainPage/menu.ts
@@ -0,0 +1,249 @@
+import './menu.css'
+import { OutlineManager } from '../outline/manager'
+import { authSession, authn } from 'solid-logic'
+import { NamedNode } from 'rdflib'
+import { loadProfileFromURI } from '../profileUtils/ownerProfile'
+
+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 (subject: NamedNode, outliner: any): Promise