Skip to content

Commit 23c6ba4

Browse files
committed
#709 Implement Design System Header
1 parent 636c0cb commit 23c6ba4

9 files changed

Lines changed: 172 additions & 213 deletions

File tree

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"pane-registry": "$pane-registry"
8686
},
8787
"devDependencies": {
88+
"@iconify/json": "2.2.494",
8889
"@testing-library/dom": "^10.4.1",
8990
"@testing-library/jest-dom": "^6.9.1",
9091
"@types/node": "^22.20.0",
@@ -96,6 +97,7 @@
9697
"neostandard": "^0.13.0",
9798
"solidos-toolkit": "dev",
9899
"typescript": "^6.0.3",
100+
"unplugin-icons": "^23.0.1",
99101
"vite": "^8.0.16",
100102
"vitest": "^4.0.18"
101103
},
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
@import "solid-ui/breakpoints.css";
2+
3+
:host {
4+
display: flex;
5+
flex-direction: row;
6+
align-items: center;
7+
justify-content: space-between;
8+
background: var(--solid-ui-color-slate-800);
9+
color: var(--solid-ui-color-white);
10+
padding: 7px 20px;
11+
gap: 10px;
12+
13+
a.logo {
14+
height: 40px;
15+
16+
solid-ui-solid-emblem {
17+
height: 100%;
18+
}
19+
}
20+
21+
.spacer {
22+
flex: 1;
23+
}
24+
25+
.separator {
26+
background: var(--solid-ui-color-body-grey);
27+
width: 1px;
28+
align-self: stretch;
29+
}
30+
31+
@media (--solid-ui-mobile) {
32+
a.logo,
33+
solid-ui-menu,
34+
.separator {
35+
display: none;
36+
}
37+
}
38+
}

src/components/header/Header.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { customElement, WebComponent } from 'solid-ui'
2+
import { html } from 'lit'
3+
import { property } from 'lit/decorators.js'
4+
import type { AccountMenuItem } from 'solid-ui/components/account'
5+
6+
import 'solid-ui/components/account'
7+
import 'solid-ui/components/button'
8+
import 'solid-ui/components/menu-item'
9+
import 'solid-ui/components/menu'
10+
import 'solid-ui/components/solid-emblem'
11+
import '~icons/lucide/help-circle'
12+
13+
import styles from './Header.styles.css'
14+
15+
@customElement('solid-panes-header')
16+
export default class Header extends WebComponent {
17+
static styles = styles
18+
19+
@property({ type: Array })
20+
accessor menuItems: AccountMenuItem[] = []
21+
22+
render () {
23+
return html`
24+
<a href="/" class="logo">
25+
<span class="sr-only">Home</span>
26+
<solid-ui-solid-emblem></solid-ui-solid-emblem>
27+
</a>
28+
<div class="spacer"></div>
29+
<solid-ui-account .menuItems=${this.menuItems}></solid-ui-account>
30+
<span class="separator"></span>
31+
<solid-ui-menu placement="bottom-end" distance="5">
32+
<solid-ui-button slot="trigger" variant="ghost" title="Open help">
33+
<span class="sr-only">Open help</span>
34+
<icon-lucide-help-circle slot="icon"></icon-lucide-help-circle>
35+
</solid-ui-button>
36+
37+
<solid-ui-menu-item href="https://solidos.github.io/userguide/">User guide</solid-ui-menu-item>
38+
<solid-ui-menu-item href="https://github.com/solidos/solidos/issues">Report a problem</solid-ui-menu-item>
39+
</solid-ui-menu>
40+
`
41+
}
42+
}

src/components/header/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Header from './Header'
2+
3+
export { Header }
4+
export default Header

src/mainPage/header.ts

Lines changed: 36 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,224 +1,53 @@
1-
import { authSession, authn, store } from 'solid-logic'
2-
import { widgets, utils } from 'solid-ui'
3-
import 'solid-ui/components/header'
4-
import { Header } from 'solid-ui/components/header'
5-
import type { HeaderMenuItem, HeaderAccountMenuItem, HeaderAuthState } from 'solid-ui/components/header'
6-
import { OutlineManager } from '../outline/manager'
7-
import { LiveStore } from 'rdflib'
8-
import helpIconSvg from '../icons/help.svg?raw'
9-
import loginIconSvg from '../icons/person.svg?raw'
10-
import signOutIconSvg from '../icons/signOut.svg?raw'
11-
import defaultAvatarIconSvg from '../icons/personInCircle.svg?raw'
12-
import downArrowIconSvg from '../icons/downArrow.svg?raw'
13-
import signupIconSvg from '../icons/signUp.svg?raw'
14-
import { createUiIcon } from '../icons/iconHelper'
15-
import { setActiveMenuPane } from './menu'
16-
/**
17-
* menu icons
18-
*/
19-
const HELP_MENU_ICON = createUiIcon(helpIconSvg, 'Help Icon', '#ffffff')
20-
const LOGIN_ICON = createUiIcon(loginIconSvg, 'LogIn Icon', '#ffffff')
21-
const SIGNUP_ICON = createUiIcon(signupIconSvg, 'SignUp Icon', '#ffffff')
22-
const LOGOUT_ICON = createUiIcon(signOutIconSvg, 'LogOut Icon', '#000000')
23-
const DEFAULT_AVATAR_ICON = createUiIcon(defaultAvatarIconSvg, 'Default Avatar Icon', '#6A7282')
24-
const DOWN_ARROW_ICON = createUiIcon(downArrowIconSvg, 'Down Arrow Icon', '#ffffff')
25-
const SOLID_ICON_URL = 'https://solidproject.org/assets/img/solid-emblem.svg'
26-
/**
27-
* menu elements
28-
*/
29-
const SIGN_IN_MENU_ITEM = 'Log In'
30-
const SIGN_OUT_MENU_ITEM = 'Log Out'
31-
const SIGN_UP_MENU_ITEM = 'Sign Up'
32-
const SIGN_UP__MENU_LINK = 'https://solidproject.org/get_a_pod'
33-
34-
// data structure extracted for solid-ui-header binding
35-
export const HELP_MENU_LIST = [
36-
{ label: 'User guide', url: 'https://solidos.github.io/userguide/', target: '_blank' },
37-
{ label: 'Report a problem', url: 'https://github.com/solidos/solidos/issues', target: '_blank' }
38-
]
1+
import { authn } from 'solid-logic'
2+
import { html, render } from 'lit-html'
3+
import type { AccountMenuItem } from 'solid-ui/components/account'
394

40-
const HEADER_MOBILE_STYLE_ID = 'solid-ui-header-mobile-style'
5+
import { setActiveMenuPane } from './menu'
6+
import type { OutlineManager } from '../outline/manager'
417

42-
type ManagedHeader = Header & {
43-
__solidPanesListenersAttached?: boolean
44-
__solidPanesOutliner?: OutlineManager
45-
}
8+
import '~icons/lucide/user'
9+
import '../components/header'
4610

47-
function ensureMobileHeaderStyles () {
48-
if (document.getElementById(HEADER_MOBILE_STYLE_ID)) return
49-
const style = document.createElement('style')
50-
style.id = HEADER_MOBILE_STYLE_ID
51-
style.textContent = `
52-
solid-ui-header[layout="mobile"]::part(logo) {
53-
display: none !important;
54-
}
55-
`
56-
document.head.appendChild(style)
57-
}
11+
export async function createHeader (outliner: OutlineManager) {
12+
const existingHeader = document.querySelector('solid-panes-header')
5813

59-
export async function createHeader (store: LiveStore, outliner: OutlineManager) {
60-
ensureMobileHeaderStyles()
61-
62-
const header = (document.querySelector('solid-ui-header') || document.createElement('solid-ui-header')) as ManagedHeader
63-
const isNewHeader = !header.isConnected
64-
if (!header.id) {
65-
header.id = 'mainSolidUiHeader'
14+
if (existingHeader) {
15+
return existingHeader
6616
}
67-
header.__solidPanesOutliner = outliner
6817

69-
// ensure it is in DOM (before MainContent for consistency)
7018
const main = document.getElementById('MainContent')
71-
if (!header.isConnected) {
72-
if (main && main.parentNode) {
73-
main.parentNode.insertBefore(header, main)
74-
} else {
75-
document.body.prepend(header)
76-
}
77-
}
78-
79-
await refreshHeader(outliner, header)
80-
81-
if (isNewHeader) {
82-
header.__solidPanesListenersAttached = false
83-
}
84-
85-
attachHeaderListeners(header)
86-
87-
return header
88-
}
89-
90-
function attachHeaderListeners (header: ManagedHeader) {
91-
if (header.__solidPanesListenersAttached) return
92-
93-
const refreshCurrentHeader = async () => {
94-
const outliner = header.__solidPanesOutliner
95-
if (!outliner) return
96-
await refreshHeader(outliner, header)
97-
}
98-
99-
authSession.events.on('login', refreshCurrentHeader)
100-
authSession.events.on('logout', refreshCurrentHeader)
101-
authSession.events.on('sessionRestore', refreshCurrentHeader)
102-
103-
header.addEventListener('auth-action-select', async (e: Event) => {
104-
const outliner = header.__solidPanesOutliner
105-
if (!outliner) return
106-
107-
const detail = (e as CustomEvent).detail
108-
if (detail?.role === 'login') {
109-
await refreshCurrentHeader()
110-
// Do not auto-open the profile pane after login.
111-
// outliner.showDashboard({ pane: 'profile' })
112-
}
113-
})
114-
115-
header.addEventListener('signup-success', async () => {
116-
// do nothing
117-
})
118-
119-
header.addEventListener('account-menu-select', async (e: Event) => {
120-
const outliner = header.__solidPanesOutliner
121-
if (!outliner) return
122-
123-
const detail = (e as CustomEvent).detail
124-
if (detail?.action === 'logout') {
125-
await refreshCurrentHeader()
126-
// Do not navigate to the profile after logout.
127-
} else if (detail?.action === 'show-profile') {
128-
// TODO see if this can be consolidated
129-
const currentUser = authn.currentUser()
130-
if (currentUser) {
131-
outliner.showDashboard(currentUser, { pane: 'profile' })
132-
setActiveMenuPane('profile')
19+
const tmpContainer = document.createElement('div')
20+
const menuItems: AccountMenuItem[] = [
21+
{
22+
label: html`<icon-lucide-user slot="left-icon"></icon-lucide-user> Profile`,
23+
onSelected () {
24+
const currentUser = authn.currentUser()
25+
26+
if (currentUser) {
27+
outliner.showDashboard(currentUser, { pane: 'profile' })
28+
setActiveMenuPane('profile')
29+
}
13330
}
13431
}
135-
})
136-
137-
header.__solidPanesListenersAttached = true
138-
}
139-
140-
export async function refreshHeader (outliner: OutlineManager, headerElement?: Header) {
141-
ensureMobileHeaderStyles()
142-
const headerOptions = setHeaderOptions(outliner)
143-
const header = headerElement || document.querySelector('solid-ui-header') as Header | null
144-
if (!header) return null
145-
146-
header.theme = headerOptions.theme
147-
header.layout = headerOptions.layout
148-
header.brandLink = headerOptions.brandLink
149-
header.logo = headerOptions.layout === 'desktop' ? headerOptions.logo : ''
150-
header.helpIcon = headerOptions.helpIcon
151-
header.helpMenuList = headerOptions.helpMenuList
152-
header.authState = headerOptions.authState
153-
header.loginAction = headerOptions.loginAction
154-
header.signUpAction = headerOptions.signUpAction
155-
header.accountMenu = await setUserMenu()
156-
header.logoutLabel = headerOptions.logoutLabel
157-
header.accountIcon = headerOptions.accountIcon
158-
header.accountAvatar = headerOptions.accountAvatar
159-
header.accountAvatarFallback = headerOptions.accountAvatarFallback
160-
header.loginIcon = headerOptions.loginIcon
161-
header.signUpIcon = headerOptions.signUpIcon
162-
header.logoutIcon = headerOptions.logoutIcon
163-
164-
return header
165-
}
166-
167-
function setHeaderOptions (outliner: OutlineManager) {
168-
const currentUser = authn.currentUser()
169-
const isAuthenticated = !!currentUser
170-
const authState: HeaderAuthState = isAuthenticated ? 'logged-in' : 'logged-out'
171-
const layout: Header['layout'] = outliner.context?.environment?.layout === 'mobile' ? 'mobile' : 'desktop'
172-
const theme: Header['theme'] = outliner.context?.environment?.theme === 'dark' ? 'dark' : 'light'
32+
]
17333

174-
const headerOptions = {
175-
logo: SOLID_ICON_URL,
176-
helpIcon: HELP_MENU_ICON,
177-
helpMenuList: HELP_MENU_LIST,
178-
layout,
179-
theme,
180-
brandLink: '/',
181-
authState,
182-
loginAction: {
183-
label: SIGN_IN_MENU_ITEM
184-
} as HeaderMenuItem,
185-
signUpAction: {
186-
label: SIGN_UP_MENU_ITEM,
187-
url: SIGN_UP__MENU_LINK,
188-
} as HeaderMenuItem,
189-
logoutLabel: SIGN_OUT_MENU_ITEM,
190-
accountIcon: isAuthenticated ? DOWN_ARROW_ICON : '',
191-
accountAvatar: isAuthenticated ? widgets.findImage(currentUser) : undefined,
192-
accountAvatarFallback: DEFAULT_AVATAR_ICON,
193-
loginIcon: LOGIN_ICON,
194-
signUpIcon: SIGNUP_ICON,
195-
logoutIcon: LOGOUT_ICON
196-
}
34+
render(
35+
html`<solid-panes-header .menuItems=${menuItems}></solid-panes-header>`,
36+
tmpContainer
37+
)
19738

198-
return headerOptions
199-
}
39+
const header = tmpContainer.firstElementChild
20040

201-
async function setUserMenu () {
202-
const me = authn.currentUser()
203-
if (!me) {
204-
return []
41+
if (!header) {
42+
throw new Error('Failed to create header')
20543
}
20644

207-
try {
208-
await store.fetcher.load(me.doc())
209-
} catch (err) {
210-
console.error('Unable to load user profile', err)
45+
// ensure it is in DOM (before MainContent for consistency)
46+
if (main && main.parentNode) {
47+
main.parentNode.insertBefore(header, main)
48+
} else {
49+
document.body.prepend(header)
21150
}
21251

213-
const accountMenu: HeaderAccountMenuItem[] = [
214-
{
215-
label: utils.label(me),
216-
avatar: widgets.findImage(me),
217-
webid: me.value,
218-
action: 'show-profile'
219-
},
220-
// TODO add all my available accounts
221-
]
222-
223-
return accountMenu
52+
return header
22453
}

0 commit comments

Comments
 (0)