From ce9d2a02766ad8f10bc532e7de68728fad08c2cd Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Sat, 18 Apr 2026 12:56:05 +0200 Subject: [PATCH] test left-menu --- src/RDFXMLPane.js | 1 + src/dashboard/dashboardPane.ts | 4 +- src/form/pane.js | 2 +- src/humanReadablePane.js | 4 +- src/index.ts | 21 ++- src/mainPage/header.ts | 234 ++++++++++++++++++++++++-------- src/mainPage/index.ts | 36 ++++- src/mainPage/menu.css | 83 ++++++++++++ src/mainPage/menu.ts | 235 +++++++++++++++++++++++++++++++++ src/n3Pane.js | 1 + src/outline/context.ts | 8 +- src/outline/manager.js | 32 +++-- src/pad/padPane.css | 36 +++++ src/pad/padPane.ts | 61 +++++---- src/playlist/playlistPane.js | 8 +- src/registerPanes.js | 4 +- src/slideshow/slideshowPane.js | 2 +- src/socialPane.css | 7 + src/socialPane.js | 26 ++-- tsconfig.json | 2 +- webpack.config.mjs | 4 + 21 files changed, 675 insertions(+), 136 deletions(-) create mode 100644 src/mainPage/menu.css create mode 100644 src/mainPage/menu.ts create mode 100644 src/pad/padPane.css create mode 100644 src/socialPane.css 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 => { + try { + const items = await outliner.getDashboardItems() + return items.map((element) => { + return { + icon: element.icon, + paneName: element.tabName || element.paneName, + label: element.label, + onclick: () => openDashboardPane(outliner, element.tabName || element.paneName) + } + }) + } catch (error) { + console.error('Unable to load navigation menu items', error) + return [] + } +} + +const createMenuButton = (item: MenuItem) => { + const button = document.createElement('button') + button.className = 'menu-item' + button.type = 'button' + if (item.paneName) { + button.dataset.paneName = item.paneName + } + + if (item.icon) { + const icon = document.createElement('img') + icon.className = 'menu-item-icon' + icon.src = item.icon + icon.alt = '' + icon.setAttribute('aria-hidden', 'true') + button.appendChild(icon) + } + + const label = document.createElement('span') + label.className = 'menu-item-label' + label.textContent = item.label + button.appendChild(label) + + if (item.id) { + button.id = item.id + } + + button.addEventListener('click', async () => { + await item.onclick() + }) + + return button +} + +const setActiveMenuItem = (container: HTMLElement, paneName?: string) => { + const menuItems = Array.from(container.querySelectorAll('.menu-item')) + let activeItem: HTMLButtonElement | undefined + + menuItems.forEach((item) => { + const isActive = Boolean(paneName) && item.dataset.paneName === paneName + item.classList.toggle('menu-item-active', isActive) + if (isActive) { + item.setAttribute('aria-current', 'page') + activeItem = item + } else { + item.removeAttribute('aria-current') + } + }) + + if (!activeItem && menuItems[0]) { + menuItems[0].classList.add('menu-item-active') + menuItems[0].setAttribute('aria-current', 'page') + container.dataset.activePaneName = menuItems[0].dataset.paneName || '' + return + } + + if (paneName) { + container.dataset.activePaneName = paneName + } +} + +const renderMenuItems = async (outliner: OutlineManager, container: HTMLElement) => { + const menuItems = await getMenuItems(outliner) + + container.replaceChildren(...menuItems.map(createMenuButton)) + setActiveMenuItem(container, container.dataset.activePaneName) +} + +export const refreshMenu = (layout: 'mobile' | 'desktop') => { + const navMenu = document.getElementById('NavMenu') as HTMLElement | null + const toggle = document.getElementById('MenuToggleBtn') as HTMLButtonElement | null + const overlay = document.getElementById('MenuOverlay') as HTMLElement | null + + if (!navMenu || !toggle || !overlay) return + + if (layout === 'mobile') { + navMenu.classList.add('mobile-hidden') + navMenu.classList.remove('mobile-visible') + toggle.hidden = false + overlay.hidden = true + navMenu.hidden = false + toggle.setAttribute('aria-expanded', 'false') + } else { + navMenu.classList.remove('mobile-hidden', 'mobile-visible') + toggle.hidden = true + overlay.hidden = true + navMenu.hidden = false + toggle.setAttribute('aria-expanded', 'false') + } +} + +export const createLeftSideMenu = async (outliner: OutlineManager) => { + ensureMenuSkeleton() + const navMenu = document.getElementById('NavMenu') as HTMLElement | null + const menuToggle = document.getElementById('MenuToggleBtn') as HTMLElement | null + const menuOverlay = document.getElementById('MenuOverlay') as HTMLElement | null + const navMenuContent = document.getElementById('NavMenuContent') as HTMLElement | null + + const closeMobileMenu = () => { + if (!navMenu || !menuToggle || !menuOverlay) return + navMenu.classList.remove('mobile-visible') + navMenu.classList.add('mobile-hidden') + menuToggle.setAttribute('aria-expanded', 'false') + menuOverlay.hidden = true + } + + const openMobileMenu = () => { + if (!navMenu || !menuToggle || !menuOverlay) return + navMenu.classList.remove('mobile-hidden') + navMenu.classList.add('mobile-visible') + menuToggle.setAttribute('aria-expanded', 'true') + menuOverlay.hidden = false + } + + if (menuToggle) { + menuToggle.hidden = false + menuToggle.addEventListener('click', () => { + if (navMenu?.classList.contains('mobile-visible')) { + closeMobileMenu() + } else { + openMobileMenu() + } + }) + } + + if (menuOverlay) { + menuOverlay.addEventListener('click', closeMobileMenu) + } + + if (navMenuContent) { + await renderMenuItems(outliner, navMenuContent) + + if (!navMenuContent.dataset.authEventsBound) { + const refreshMenuItems = async () => { + await renderMenuItems(outliner, navMenuContent) + } + + authSession.events.on('login', refreshMenuItems) + authSession.events.on('logout', refreshMenuItems) + authSession.events.on('sessionRestore', refreshMenuItems) + navMenuContent.dataset.authEventsBound = 'true' + } + + navMenuContent.addEventListener('click', (event) => { + const item = (event.target as HTMLElement).closest('.menu-item') + if (item instanceof HTMLButtonElement) { + setActiveMenuItem(navMenuContent, item.dataset.paneName) + } + const isMobile = outliner.context?.environment?.layout === 'mobile' + if (item && isMobile) { + closeMobileMenu() + } + }) + } + + refreshMenu(outliner.context?.environment?.layout === 'mobile' ? 'mobile' : 'desktop') +} + +async function openDashboardPane (outliner: any, pane: string): Promise { + outliner.showDashboard({ + pane + }) +} \ No newline at end of file diff --git a/src/n3Pane.js b/src/n3Pane.js index ee8d2c21..d7592005 100644 --- a/src/n3Pane.js +++ b/src/n3Pane.js @@ -51,6 +51,7 @@ export const n3Pane = { sz.setBase(subject.uri) const str = sz.statementsToN3(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/outline/context.ts b/src/outline/context.ts index dffe4f83..fc5f6bf8 100644 --- a/src/outline/context.ts +++ b/src/outline/context.ts @@ -1,4 +1,4 @@ -import { DataBrowserContext, PaneRegistry } from 'pane-registry' +import { DataBrowserContext, PaneRegistry, RenderEnvironment } from 'pane-registry' import { getOutliner } from '../index' import { SolidLogic } from 'solid-logic' import { LiveStore } from 'rdflib' @@ -7,7 +7,8 @@ export function createContext ( dom: HTMLDocument, paneRegistry: PaneRegistry, store: LiveStore, - logic: SolidLogic + logic: SolidLogic, + environment?: RenderEnvironment ): DataBrowserContext { return { dom, @@ -16,6 +17,7 @@ export function createContext ( paneRegistry, store, logic - } + }, + environment } } diff --git a/src/outline/manager.js b/src/outline/manager.js index dee01991..f34af58d 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -321,20 +321,24 @@ export default function (context) { ) } - div.appendChild( - UI.tabs.tabWidget({ - dom, - subject: me, - items, - renderMain, - renderTab, - ordered: true, - orientation: 0, - backgroundColor: '#eeeeee', // black? - selectedTab: options.selectedTab, - onClose: options.onClose - }) - ) + const tabWidgetEl = UI.tabs.tabWidget({ + dom, + subject: me, + items, + renderMain, + renderTab, + ordered: true, + orientation: 0, + backgroundColor: '#eeeeee', // black? + selectedTab: options.selectedTab, + onClose: options.onClose + }) + // Hide the tab strip visually while keeping tabs clickable for programmatic navigation + const tabNav = tabWidgetEl.querySelector('nav') + if (tabNav) { + tabNav.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;' + } + div.appendChild(tabWidgetEl) return div } this.getDashboard = globalAppTabs diff --git a/src/pad/padPane.css b/src/pad/padPane.css new file mode 100644 index 00000000..1339fdda --- /dev/null +++ b/src/pad/padPane.css @@ -0,0 +1,36 @@ +.pad-layout { + background-color: white; + min-width: 94%; + margin-right: 3%; + margin-left: 3%; + min-height: 13em; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +.pad-row { + display: flex; + flex-wrap: wrap; + width: 100%; +} + +.pad-main { + flex: 1 1 100%; + width: 100%; +} + +.pad-controls { + gap: 0.5em; + align-items: center; +} + +.pad-cell { + flex: 1 1 auto; +} + +@media (max-width: 600px) { + .pad-controls .pad-cell { + flex: 1 1 100%; + } +} diff --git a/src/pad/padPane.ts b/src/pad/padPane.ts index 7003d960..a5163e9b 100644 --- a/src/pad/padPane.ts +++ b/src/pad/padPane.ts @@ -2,6 +2,7 @@ import { icons, ns, pad, widgets, login } from 'solid-ui' import { authn, AppDetails } from 'solid-logic' import { graph, log, NamedNode, Namespace, sym, serialize, Store } from 'rdflib' import { PaneDefinition } from 'pane-registry' +import './padPane.css' /* pad Pane ** */ @@ -514,38 +515,42 @@ const paneDef: PaneDefinition = { const div = dom.createElement('div') // Build the DOM - const structure = div.appendChild(dom.createElement('table')) // @@ make responsive style - structure.setAttribute( - 'style', - 'background-color: white; min-width: 94%; margin-right:3% margin-left: 3%; min-height: 13em;' - ) - - const naviLoginoutTR = structure.appendChild(dom.createElement('tr')) - naviLoginoutTR.appendChild(dom.createElement('td')) // naviLoginout1 - naviLoginoutTR.appendChild(dom.createElement('td')) - naviLoginoutTR.appendChild(dom.createElement('td')) - - const naviTop = structure.appendChild(dom.createElement('tr')) // stuff - const naviMain = naviTop.appendChild(dom.createElement('td')) - naviMain.setAttribute('colspan', '3') - - const naviMiddle = structure.appendChild(dom.createElement('tr')) // controls - const naviMiddle1 = naviMiddle.appendChild(dom.createElement('td')) - const naviMiddle2 = naviMiddle.appendChild(dom.createElement('td')) - const naviMiddle3 = naviMiddle.appendChild(dom.createElement('td')) - - const naviStatus = structure.appendChild(dom.createElement('tr')) // status etc + const structure = div.appendChild(dom.createElement('div')) // responsive flex layout + structure.className = 'pad-layout' + + const naviLoginoutTR = structure.appendChild(dom.createElement('div')) + naviLoginoutTR.className = 'pad-row' + naviLoginoutTR.appendChild(dom.createElement('div')) // naviLoginout1 + naviLoginoutTR.appendChild(dom.createElement('div')) + naviLoginoutTR.appendChild(dom.createElement('div')) + + const naviTop = structure.appendChild(dom.createElement('div')) // stuff + naviTop.className = 'pad-row' + const naviMain = naviTop.appendChild(dom.createElement('div')) + naviMain.className = 'pad-main' + + const naviMiddle = structure.appendChild(dom.createElement('div')) // controls + naviMiddle.className = 'pad-row pad-controls' + const naviMiddle1 = naviMiddle.appendChild(dom.createElement('div')) + naviMiddle1.className = 'pad-cell' + const naviMiddle2 = naviMiddle.appendChild(dom.createElement('div')) + naviMiddle2.className = 'pad-cell' + const naviMiddle3 = naviMiddle.appendChild(dom.createElement('div')) + naviMiddle3.className = 'pad-cell' + + const naviStatus = structure.appendChild(dom.createElement('div')) // status etc + naviStatus.className = 'pad-row' const statusArea = naviStatus.appendChild(dom.createElement('div')) - const naviSpawn = structure.appendChild(dom.createElement('tr')) // create new + const naviSpawn = structure.appendChild(dom.createElement('div')) // create new + naviSpawn.className = 'pad-row' const spawnArea = naviSpawn.appendChild(dom.createElement('div')) - const naviMenu = structure.appendChild(dom.createElement('tr')) - naviMenu.setAttribute('class', 'naviMenu') - // naviMenu.setAttribute('style', 'margin-top: 3em;'); - naviMenu.appendChild(dom.createElement('td')) // naviLeft - naviMenu.appendChild(dom.createElement('td')) - naviMenu.appendChild(dom.createElement('td')) + const naviMenu = structure.appendChild(dom.createElement('div')) + naviMenu.setAttribute('class', 'pad-row naviMenu') + naviMenu.appendChild(dom.createElement('div')) // naviLeft + naviMenu.appendChild(dom.createElement('div')) + naviMenu.appendChild(dom.createElement('div')) const options: any = { statusArea, timingArea: naviMiddle1 } diff --git a/src/playlist/playlistPane.js b/src/playlist/playlistPane.js index 99236eff..003d1cff 100644 --- a/src/playlist/playlistPane.js +++ b/src/playlist/playlistPane.js @@ -100,18 +100,14 @@ export default { div.setAttribute('class', 'imageView') img = myDocument.createElement('IFRAME') img.setAttribute('src', uri) - img.setAttribute('width', 560) - img.setAttribute('height', 315) img.setAttribute('frameborder', 0) - img.setAttribute('style', 'max-width: 850px; max-height: 100%;') + img.setAttribute('style', 'width: 100%; aspect-ratio: 16/9; max-width: 100%; border: 0;') img.setAttribute('allowfullscreen', 'true') } else { div.setAttribute('class', 'imageView') img = myDocument.createElement('IMG') img.setAttribute('src', obj.value) - img.setAttribute('width', 560) - img.setAttribute('height', 315) - img.setAttribute('style', 'max-width: 560; max-height: 315;') + img.setAttribute('style', 'max-width: 100%; height: auto;') } let descDiv diff --git a/src/registerPanes.js b/src/registerPanes.js index dff80c37..f43c3444 100644 --- a/src/registerPanes.js +++ b/src/registerPanes.js @@ -51,10 +51,10 @@ export function registerPanes (register) { const editProfileView = profilePane.editor if (!editProfileView) { console.log('@@@ editProfileView', 'profilePane is not providing an editor pane') + } else { + register(editProfileView) // Edit my profile. } - register(editProfileView) // Edit my profile. - register(trustedApplications) // must be registered before basicPreferences register(dashboardPane) register(basicPreferences) diff --git a/src/slideshow/slideshowPane.js b/src/slideshow/slideshowPane.js index 0720bfad..087ad4f6 100644 --- a/src/slideshow/slideshowPane.js +++ b/src/slideshow/slideshowPane.js @@ -64,7 +64,7 @@ export const slideshowPane = { const objectURL = URL.createObjectURL(myBlob) img.setAttribute('src', objectURL) // w640 h480 // }) - img.setAttribute('width', '100%') + img.style.width = '100%' figure.appendChild(dom.createElement('figcaption')) } const options = { dom } diff --git a/src/socialPane.css b/src/socialPane.css new file mode 100644 index 00000000..219aca8c --- /dev/null +++ b/src/socialPane.css @@ -0,0 +1,7 @@ +.socialPane .social-layout { display: flex; flex-wrap: wrap; width: 100%; gap: 0; } +.socialPane .social-nav { flex: 1 1 22%; min-width: 8rem; background-color: #eee; border: 0; padding: 0.5em; box-sizing: border-box; } +.socialPane .social-main { flex: 2 1 50%; min-width: 12rem; background-color: #fff; color: #000; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; padding: 0; box-sizing: border-box; width: 100%; } +.socialPane .social-pic { width: 100%; border: none; margin: 0; padding: 0; } +@media (max-width: 600px) { + .socialPane .social-nav, .socialPane .social-main { flex: 1 1 100%; width: 100%; } +} diff --git a/src/socialPane.js b/src/socialPane.js index ee432446..07d256ce 100644 --- a/src/socialPane.js +++ b/src/socialPane.js @@ -11,6 +11,7 @@ import * as UI from 'solid-ui' import { authn } from 'solid-logic' import * as $rdf from 'rdflib' +import './socialPane.css' export const socialPane = { icon: UI.icons.originalIconBase + 'foaf/foafTiny.gif', @@ -166,25 +167,20 @@ export const socialPane = { const foaf = UI.ns.foaf const vcard = UI.ns.vcard - // extracted from tabbedtab.css 2017-03-21 - const navBlockStyle = - 'background-color: #eee; width: 25%; border: 0; padding: 0.5em; margin: 0;' - const mainBlockStyle = - 'background-color: #fff; color: #000; width: 46%; margin: 0; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; padding: 0;' - const foafPicStyle = ' width: 100% ; border: none; margin: 0; padding: 0;' + const foafPicStyle = 'social-pic' - const structure = div.appendChild(dom.createElement('table')) - const tr = structure.appendChild(dom.createElement('tr')) - const left = tr.appendChild(dom.createElement('td')) - const middle = tr.appendChild(dom.createElement('td')) - const right = tr.appendChild(dom.createElement('td')) + const structure = div.appendChild(dom.createElement('div')) + structure.className = 'social-layout' + const left = structure.appendChild(dom.createElement('div')) + left.className = 'social-nav' + const middle = structure.appendChild(dom.createElement('div')) + const right = structure.appendChild(dom.createElement('div')) + right.className = 'social-nav' const tools = left - tools.style.cssText = navBlockStyle const mainTable = middle.appendChild(dom.createElement('table')) - mainTable.style.cssText = mainBlockStyle + mainTable.className = 'social-main' const tips = right - tips.style.cssText = navBlockStyle // Image top left const src = kb.any(s, foaf('img')) || kb.any(s, foaf('depiction')) @@ -192,7 +188,7 @@ export const socialPane = { const img = dom.createElement('IMG') img.setAttribute('src', src.uri) // w640 h480 // img.className = 'foafPic' - img.style.cssText = foafPicStyle + img.className = foafPicStyle tools.appendChild(img) } const name = kb.anyValue(s, foaf('name')) || '???' diff --git a/tsconfig.json b/tsconfig.json index cce40837..39cf9440 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Basic Options */ "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, - "moduleResolution": "node", + "moduleResolution": "bundler", "lib": [ "DOM", "ES2019" diff --git a/webpack.config.mjs b/webpack.config.mjs index 922b356c..0cba9b35 100644 --- a/webpack.config.mjs +++ b/webpack.config.mjs @@ -21,6 +21,10 @@ const common = { test: /\.css$/, use: ['style-loader', 'css-loader'], }, + { + test: /\.(png|jpe?g|gif|webp|avif|svg)$/i, + type: 'asset/resource' + }, { test: /\.ttl$/i, type: 'asset/source'