diff --git a/package-lock.json b/package-lock.json index e47dd8b3..c4a51d20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "solid-panes", - "version": "4.4.2-2", + "version": "4.4.2-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "solid-panes", - "version": "4.4.2-2", + "version": "4.4.2-3", "license": "MIT", "dependencies": { "@solid/better-simple-slideshow": "^0.1.0", @@ -29,6 +29,7 @@ "source-pane": "3.1.1-1" }, "devDependencies": { + "@iconify/json": "2.2.494", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@types/node": "^22.20.0", @@ -40,6 +41,7 @@ "neostandard": "^0.13.0", "solidos-toolkit": "dev", "typescript": "^6.0.3", + "unplugin-icons": "^23.0.1", "vite": "^8.0.16", "vitest": "^4.0.18" } @@ -2548,6 +2550,17 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/json": { + "version": "2.2.494", + "resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.2.494.tgz", + "integrity": "sha512-ssY0bQgG+W2uAd4G+iK/FhjiUu4UG/wdCTIt6dqItBO6DFTtvkHo2nQqWOJTG0orNFl9eDxB+YgVUzXnpvAhkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "*", + "pathe": "^2.0.3" + } + }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", diff --git a/package.json b/package.json index b906fb34..d06dd470 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-panes", - "version": "4.4.2-2", + "version": "4.4.2-3", "description": "Solid-compatible Panes: applets and views for the mashlib and databrowser", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", @@ -85,6 +85,7 @@ "pane-registry": "$pane-registry" }, "devDependencies": { + "@iconify/json": "2.2.494", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@types/node": "^22.20.0", @@ -96,6 +97,7 @@ "neostandard": "^0.13.0", "solidos-toolkit": "dev", "typescript": "^6.0.3", + "unplugin-icons": "^23.0.1", "vite": "^8.0.16", "vitest": "^4.0.18" }, diff --git a/src/components/header/Header.styles.css b/src/components/header/Header.styles.css new file mode 100644 index 00000000..41298c17 --- /dev/null +++ b/src/components/header/Header.styles.css @@ -0,0 +1,38 @@ +@import "solid-ui/breakpoints.css"; + +:host { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background: var(--solid-ui-color-slate-800); + color: var(--solid-ui-color-white); + padding: 7px 20px; + gap: 10px; + + a.logo { + height: 40px; + + solid-ui-solid-emblem { + height: 100%; + } + } + + .spacer { + flex: 1; + } + + .separator { + background: var(--solid-ui-color-body-grey); + width: 1px; + align-self: stretch; + } + + @media (--solid-ui-mobile) { + a.logo, + solid-ui-menu, + .separator { + display: none; + } + } +} diff --git a/src/components/header/Header.ts b/src/components/header/Header.ts new file mode 100644 index 00000000..97e3fe88 --- /dev/null +++ b/src/components/header/Header.ts @@ -0,0 +1,42 @@ +import { customElement, WebComponent } from 'solid-ui' +import { html } from 'lit' +import { property } from 'lit/decorators.js' +import type { AccountMenuItem } from 'solid-ui/components/account' + +import 'solid-ui/components/account' +import 'solid-ui/components/button' +import 'solid-ui/components/menu-item' +import 'solid-ui/components/menu' +import 'solid-ui/components/solid-emblem' +import '~icons/lucide/help-circle' + +import styles from './Header.styles.css' + +@customElement('solid-panes-header') +export default class Header extends WebComponent { + static styles = styles + + @property({ type: Array }) + accessor menuItems: AccountMenuItem[] = [] + + render () { + return html` + +
+ + + + + Open help + + + + User guide + Report a problem + + ` + } +} diff --git a/src/components/header/index.ts b/src/components/header/index.ts new file mode 100644 index 00000000..e0567ca9 --- /dev/null +++ b/src/components/header/index.ts @@ -0,0 +1,4 @@ +import Header from './Header' + +export { Header } +export default Header diff --git a/src/mainPage/header.ts b/src/mainPage/header.ts index f6546e7f..3a661f0b 100644 --- a/src/mainPage/header.ts +++ b/src/mainPage/header.ts @@ -1,224 +1,53 @@ -import { authSession, authn, store } from 'solid-logic' -import { widgets, utils } from 'solid-ui' -import 'solid-ui/components/header' -import { Header } from 'solid-ui/components/header' -import type { HeaderMenuItem, HeaderAccountMenuItem, HeaderAuthState } from 'solid-ui/components/header' -import { OutlineManager } from '../outline/manager' -import { LiveStore } from 'rdflib' -import helpIconSvg from '../icons/help.svg?raw' -import loginIconSvg from '../icons/person.svg?raw' -import signOutIconSvg from '../icons/signOut.svg?raw' -import defaultAvatarIconSvg from '../icons/personInCircle.svg?raw' -import downArrowIconSvg from '../icons/downArrow.svg?raw' -import signupIconSvg from '../icons/signUp.svg?raw' -import { createUiIcon } from '../icons/iconHelper' -import { setActiveMenuPane } from './menu' -/** - * menu icons -*/ -const HELP_MENU_ICON = createUiIcon(helpIconSvg, 'Help Icon', '#ffffff') -const LOGIN_ICON = createUiIcon(loginIconSvg, 'LogIn Icon', '#ffffff') -const SIGNUP_ICON = createUiIcon(signupIconSvg, 'SignUp Icon', '#ffffff') -const LOGOUT_ICON = createUiIcon(signOutIconSvg, 'LogOut Icon', '#000000') -const DEFAULT_AVATAR_ICON = createUiIcon(defaultAvatarIconSvg, 'Default Avatar Icon', '#6A7282') -const DOWN_ARROW_ICON = createUiIcon(downArrowIconSvg, 'Down Arrow Icon', '#ffffff') -const SOLID_ICON_URL = 'https://solidproject.org/assets/img/solid-emblem.svg' -/** - * menu elements -*/ -const SIGN_IN_MENU_ITEM = 'Log In' -const SIGN_OUT_MENU_ITEM = 'Log Out' -const SIGN_UP_MENU_ITEM = 'Sign Up' -const SIGN_UP__MENU_LINK = 'https://solidproject.org/get_a_pod' - -// 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' } -] +import { authn } from 'solid-logic' +import { html, render } from 'lit-html' +import type { AccountMenuItem } from 'solid-ui/components/account' -const HEADER_MOBILE_STYLE_ID = 'solid-ui-header-mobile-style' +import { setActiveMenuPane } from './menu' +import type { OutlineManager } from '../outline/manager' -type ManagedHeader = Header & { - __solidPanesListenersAttached?: boolean - __solidPanesOutliner?: OutlineManager -} +import '~icons/lucide/user' +import '../components/header' -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) -} +export async function createHeader (outliner: OutlineManager) { + const existingHeader = document.querySelector('solid-panes-header') -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' + if (existingHeader) { + return existingHeader } - header.__solidPanesOutliner = outliner - // 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) - } - } - - await refreshHeader(outliner, header) - - if (isNewHeader) { - header.__solidPanesListenersAttached = false - } - - attachHeaderListeners(header) - - return header -} - -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' }) - } - }) - - 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 - const currentUser = authn.currentUser() - if (currentUser) { - outliner.showDashboard(currentUser, { pane: 'profile' }) - setActiveMenuPane('profile') + const tmpContainer = document.createElement('div') + const menuItems: AccountMenuItem[] = [ + { + label: html` Profile`, + onSelected () { + const currentUser = authn.currentUser() + + if (currentUser) { + outliner.showDashboard(currentUser, { pane: 'profile' }) + setActiveMenuPane('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.accountIcon = headerOptions.accountIcon - header.accountAvatar = headerOptions.accountAvatar - header.accountAvatarFallback = headerOptions.accountAvatarFallback - header.loginIcon = headerOptions.loginIcon - header.signUpIcon = headerOptions.signUpIcon - header.logoutIcon = headerOptions.logoutIcon - - 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, - accountIcon: isAuthenticated ? DOWN_ARROW_ICON : '', - accountAvatar: isAuthenticated ? widgets.findImage(currentUser) : undefined, - accountAvatarFallback: DEFAULT_AVATAR_ICON, - loginIcon: LOGIN_ICON, - signUpIcon: SIGNUP_ICON, - logoutIcon: LOGOUT_ICON - } + render( + html``, + tmpContainer + ) - return headerOptions -} + const header = tmpContainer.firstElementChild -async function setUserMenu () { - const me = authn.currentUser() - if (!me) { - return [] + if (!header) { + throw new Error('Failed to create header') } - try { - await store.fetcher.load(me.doc()) - } catch (err) { - console.error('Unable to load user profile', err) + // ensure it is in DOM (before MainContent for consistency) + if (main && main.parentNode) { + main.parentNode.insertBefore(header, main) + } else { + document.body.prepend(header) } - 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 + return header } diff --git a/src/mainPage/index.ts b/src/mainPage/index.ts index 947e2510..7af05d2b 100644 --- a/src/mainPage/index.ts +++ b/src/mainPage/index.ts @@ -6,7 +6,7 @@ import { LiveStore, NamedNode } from 'rdflib' import type { RenderEnvironment } from 'pane-registry' import { getOutliner, OutlineManager } from '../index' -import { createHeader, refreshHeader } from './header' +import { createHeader } from './header' import { createFooter } from './footer' import { createLeftSideMenu, refreshMenu } from './menu' @@ -20,7 +20,6 @@ function renderEnvSignature (env?: RenderEnvironment): string { } export { refreshMenu as updateMenuLayout } from './menu' -export { refreshHeader } from './header' function ensureMainContent () { let main = document.getElementById('MainContent') as HTMLElement | null @@ -47,7 +46,7 @@ export async function initMainPage ( const subject: NamedNode = typeof uri === 'string' ? store.sym(uri) : uri outliner.GotoSubject(subject, true, undefined, true, undefined) - const header = await createHeader(store, outliner) + const header = await createHeader(outliner) const menu = createLeftSideMenu(subject, outliner) const footer = menu.then(() => createFooter(store)) return Promise.all([header, menu, footer]) @@ -71,6 +70,5 @@ export async function refreshUI (outliner: OutlineManager) { ;(outliner as any)[LAST_RENDER_ENV_KEY] = currentSignature } - await refreshHeader(outliner) refreshMenu(outliner.context.environment?.layout === 'mobile' ? 'mobile' : 'desktop') } diff --git a/typings/env.d.ts b/typings/env.d.ts new file mode 100644 index 00000000..2043d9c4 --- /dev/null +++ b/typings/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/vite.config.mts b/vite.config.mts index 50b4ee18..66af8feb 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -1,9 +1,41 @@ +import { isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url' import { solidPane, buildConfig } from 'solidos-toolkit/vite' import { defineConfig } from 'vitest/config' export default defineConfig({ - build: buildConfig({ entry: "src/index.ts" }), + build: buildConfig({ + entry: "src/index.ts", + + // TODO remove these overrides once the following PR from solidos-toolkit has been merged: + // https://github.com/SolidOS/toolkit/pull/6 + overrides: { + rolldownOptions: { + output: [ + { + format: 'es', + preserveModules: true, + preserveModulesRoot: 'src', + entryFileNames: '[name].esm.js', + }, + { + format: 'cjs', + preserveModules: true, + preserveModulesRoot: 'src', + entryFileNames: '[name].cjs.js', + }, + ], + external(id: string) { + return ( + !id.startsWith('~icons/') && + !id.startsWith('@/') && + !id.startsWith('.') && + !isAbsolute(id) + ); + }, + }, + } + }), resolve: { tsconfigPaths: true, @@ -16,7 +48,7 @@ export default defineConfig({ }, }, plugins: solidPane({ - litDecoratorPaths: [], + litDecoratorPaths: ['src/components'], sandbox: { subject: "https://testingsolidos.solidcommunity.net/profile/card#me", entry: "./src/dev.ts",