Skip to content

Commit 8777d2e

Browse files
committed
improved style and added usage of icons
1 parent 981dbc0 commit 8777d2e

10 files changed

Lines changed: 228 additions & 28 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ solid-ui ships self-contained Lit-based custom elements as subpath exports. Each
266266
267267
### solid-ui-header
268268

269-
A header bar with branding, auth state (logged-out / logged-in), an account dropdown, and a desktop-only help menu.
269+
A header bar with branding, auth state (logged-out / logged-in), an account dropdown, an optional logout icon, and a desktop-only help menu.
270270

271271
**Subpath export:** `solid-ui/components/header`
272272

@@ -294,7 +294,7 @@ import { LoginButton } from 'solid-ui/components/login-button'
294294
```
295295

296296
```html
297-
<solid-ui-login-button label="Log in" issuer-url="https://solidcommunity.net"></solid-ui-login-button>
297+
<solid-ui-login-button label="Log in" issuer-url="https://solidcommunity.net" icon="https://example.com/login-icon.svg"></solid-ui-login-button>
298298
```
299299

300300
```typescript
@@ -315,7 +315,7 @@ import { SignupButton } from 'solid-ui/components/signup-button'
315315
```
316316

317317
```html
318-
<solid-ui-signup-button label="Get a Pod" signup-url="https://solidproject.org/get_a_pod"></solid-ui-signup-button>
318+
<solid-ui-signup-button label="Get a Pod" signup-url="https://solidproject.org/get_a_pod" icon="https://example.com/icon.svg"></solid-ui-signup-button>
319319
```
320320

321321
### Component build pipeline

src/v2/components/header/Header.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import '../loginButton/index'
55
import '../signupButton/index'
66
import { ifDefined } from 'lit/directives/if-defined.js'
77

8-
const DEFAULT_HELP_MENU_ICON = icons.iconBase + 'noun_help.svg'
8+
const DEFAULT_HELP_MENU_ICON = ''
99
const DEFAULT_SOLID_ICON_URL = 'https://solidproject.org/assets/img/solid-emblem.svg'
1010
const DEFAULT_SIGNUP_URL = 'https://solidproject.org/get_a_pod'
1111
const DEFAULT_LOGGEDIN_MENU_BUTTON_AVATAR = icons.iconBase + 'emptyProfileAvatar.png'
@@ -17,6 +17,7 @@ export type HeaderMenuItem = {
1717
url?: string
1818
target?: string
1919
action?: string
20+
icon?: string
2021
}
2122

2223
export type HeaderAccountMenuItem = HeaderMenuItem & {
@@ -36,8 +37,12 @@ export class Header extends LitElement {
3637
signUpAction: { type: Object, attribute: false },
3738
accountMenu: { type: Array, attribute: false },
3839
logoutLabel: { type: String, attribute: 'logout-label', reflect: true },
40+
logoutIcon: { type: String, attribute: 'logout-icon', reflect: true },
3941
accountLabel: { type: String, attribute: 'account-label', reflect: true },
4042
accountAvatar: { type: String, attribute: 'account-avatar', reflect: true },
43+
accountAvatarFallback: { type: String, attribute: 'account-avatar-fallback', reflect: true },
44+
loginIcon: { type: String, attribute: 'login-icon', reflect: true },
45+
signUpIcon: { type: String, attribute: 'sign-up-icon', reflect: true },
4146
helpMenuList: { type: Array },
4247
accountMenuOpen: { state: true },
4348
helpMenuOpen: { state: true },
@@ -62,7 +67,6 @@ export class Header extends LitElement {
6267
--header-button-bg: var(--color-menu-bg, #ffffff);
6368
--header-button-text: var(--color-header-button-text, #0F172B);
6469
--header-button-detail-text: var(--color-header-button-detail-text, #99A1AF);
65-
--header-icon-filter: invert(1) brightness(1.3); /* special way to invert SVG color of icons, from back to white */
6670
--header-shadow: var(--color-header-shadow, 2px 6px 10px 0 rgba(0, 0, 0, 0.4), 2px 8px 24px 0 rgba(0, 0, 0, 0.19));
6771
font-family: var(--font-family-base, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif);
6872
}
@@ -84,7 +88,7 @@ export class Header extends LitElement {
8488
--header-button-bg: var(--color-menu-bg, #ffffff);
8589
--header-button-text: var(--color-header-button-text, #0f172a);
8690
--header-button-detail-text: var(--color-header-button-detail-text, #878192);
87-
--header-icon-filter: invert(1) brightness(1.3); /* special way to invert SVG color of icons, from back to white */
91+
--header-icon-filter: invert(1) brightness(1.3); /* special way to invert SVG color of icons, from white to black */
8892
--header-shadow: var(--color-header-shadow, 2px 6px 10px 0 rgba(0, 0, 0, 0.4), 2px 8px 24px 0 rgba(0, 0, 0, 0.19));
8993
font-family: var(--font-family-base, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif);
9094
}
@@ -265,6 +269,7 @@ export class Header extends LitElement {
265269
height: 1.75rem;
266270
border-radius: 999px;
267271
object-fit: cover;
272+
background-color: var(--header-border);
268273
}
269274
270275
.account-dropdown {
@@ -462,11 +467,23 @@ export class Header extends LitElement {
462467
}
463468
464469
.help-icon {
465-
width: 24px;
466-
height: 24px;
467-
filter: var(--header-icon-filter);
470+
width: 35px;
471+
height: 35px;
468472
cursor: pointer;
469473
}
474+
475+
.help-text {
476+
color: var(--header-text, #ffffff);
477+
font: inherit;
478+
}
479+
480+
.logout-action-icon {
481+
width: 16px;
482+
height: 16px;
483+
display: inline-block;
484+
object-fit: contain;
485+
margin-right: 0.5rem;
486+
}
470487
`
471488

472489
declare logo: string
@@ -479,8 +496,12 @@ export class Header extends LitElement {
479496
declare signUpAction: HeaderMenuItem
480497
declare accountMenu: HeaderAccountMenuItem[]
481498
declare logoutLabel: string | null
499+
declare logoutIcon: string
482500
declare accountLabel: string
483501
declare accountAvatar: string
502+
declare accountAvatarFallback: string
503+
declare loginIcon: string
504+
declare signUpIcon: string
484505
declare helpMenuList: HeaderMenuItem[]
485506
declare accountMenuOpen: boolean
486507
declare helpMenuOpen: boolean
@@ -499,8 +520,12 @@ export class Header extends LitElement {
499520
this.signUpAction = { label: 'Sign Up', action: 'sign-up', url: DEFAULT_SIGNUP_URL }
500521
this.accountMenu = []
501522
this.logoutLabel = 'Log Out'
523+
this.logoutIcon = ''
502524
this.accountLabel = 'Accounts'
503525
this.accountAvatar = ''
526+
this.accountAvatarFallback = ''
527+
this.loginIcon = ''
528+
this.signUpIcon = ''
504529
this.helpMenuList = []
505530
this.accountMenuOpen = false
506531
this.helpMenuOpen = false
@@ -596,7 +621,7 @@ export class Header extends LitElement {
596621

597622
private renderLoggedInAvatar (avatar?: string, wrapperClass = 'account-avatar') {
598623
const hasAvatar = Boolean(avatar)
599-
const imageSrc = hasAvatar ? avatar : DEFAULT_LOGGEDIN_MENU_BUTTON_AVATAR
624+
const imageSrc = hasAvatar ? avatar : this.accountAvatarFallback || DEFAULT_LOGGEDIN_MENU_BUTTON_AVATAR
600625
const imageAlt = hasAvatar ? 'Profile Avatar' : 'Default Avatar'
601626

602627
if (this.layout === 'mobile' && wrapperClass === 'account-avatar') {
@@ -616,6 +641,7 @@ export class Header extends LitElement {
616641
<slot name="login-action">
617642
<solid-ui-login-button
618643
label="${this.loginAction.label}"
644+
icon=${ifDefined(this.layout !== 'mobile' ? (this.loginIcon || this.loginAction.icon) : undefined)}
619645
theme="${this.theme}"
620646
part="login-action"
621647
@login-success="${() => this.handleLoginSuccess()}"
@@ -625,6 +651,8 @@ export class Header extends LitElement {
625651
<solid-ui-signup-button
626652
label="${this.signUpAction.label}"
627653
signup-url="${ifDefined(this.signUpAction.url)}"
654+
layout="${this.layout}"
655+
.icon=${ifDefined(this.layout !== 'mobile' ? (this.signUpIcon || this.signUpAction.icon) : undefined)}
628656
theme="${this.theme}"
629657
part="sign-up-action"
630658
></solid-ui-signup-button>
@@ -662,7 +690,7 @@ export class Header extends LitElement {
662690
${this.renderLoggedInAvatar(item.avatar, 'account-menu-avatar')}
663691
<span class="account-menu-copy">
664692
<span class="account-menu-label">${item.label}</span>
665-
${item.webid ? html`<span class="account-menu-webid">${item.webid}</span>` : ''}
693+
${item.webid && this.layout !== 'mobile' ? html`<span class="account-menu-webid">${item.webid}</span>` : ''}
666694
</span>
667695
`
668696

@@ -742,6 +770,9 @@ export class Header extends LitElement {
742770
part="logout-action"
743771
role="menuitem"
744772
>
773+
${this.logoutIcon && this.layout !== 'mobile'
774+
? html`<img class="logout-action-icon" src="${this.logoutIcon}" alt="" aria-hidden="true" part="logout-action-icon" />`
775+
: ''}
745776
${this.logoutLabel}
746777
</button>
747778
`
@@ -809,7 +840,9 @@ export class Header extends LitElement {
809840
@click="${(e: MouseEvent) => this.toggleHelpMenu(e)}"
810841
part="help-menu-trigger"
811842
>
812-
<img id="helpIcon" class="help-icon" src="${this.helpIcon}" alt="Help" part="help-icon" />
843+
${this.helpIcon
844+
? html`<img id="helpIcon" class="help-icon" src="${this.helpIcon}" alt="Help" part="help-icon" />`
845+
: html`<span class="help-text" part="help-text">Help</span>`}
813846
</button>
814847
815848
<nav

src/v2/components/header/README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
A Lit-based custom element that renders the Solid application header, including branding, auth actions, account management, and a help menu.
44

5-
When `layout="mobile"`, the header hides the help menu entirely, even if `helpMenuList` items or `help-menu` slotted content are provided.
5+
When `layout="mobile"`, the header hides the help menu entirely, even if `helpMenuList` items or `help-menu` slotted content are provided. In this mode the header also omits icons from the built-in login/sign-up buttons, hides the account webid text in the account dropdown, and hides the logout icon.
66

77
When `auth-state="logged-out"`, the header renders a `<solid-ui-login-button>` as the login action. The login button opens a Solid IDP selection popup and handles the full OIDC login flow via `solid-logic`. On success it emits a `login-success` event and the header transitions to `logged-in` state automatically.
88

@@ -79,17 +79,21 @@ The header automatically imports and registers it — no separate import is need
7979
Properties/attributes:
8080

8181
- `logo`: URL string for the brand image (default: Solid emblem URL).
82-
- `helpIcon`: URL string for the help icon, default from icons asset.
82+
- `helpIcon`: URL string for the help icon, default from icons asset. If `help-icon` is empty or not provided, the help trigger renders the text `Help` instead.
8383
- `brandLink`: URL string for the brand link (default: `#`).
84-
- `layout`: `desktop` or `mobile`. Mobile layout hides the brand logo link and does not render the help menu.
84+
- `layout`: `desktop` or `mobile`. Mobile layout hides the brand logo link, does not render the help menu, omits icons from the built-in login and sign-up buttons, hides the account webid text in the dropdown, and hides the logout icon.
8585
- `theme`: `light` or `dark`.
8686
- `authState`: `logged-out` or `logged-in`.
87-
- `loginAction`: object with a `label` for the login button. When `authState` is `logged-out` this is rendered as a `<solid-ui-login-button>` which handles the full OIDC flow; supplying a `url` instead opts out of the built-in flow and renders a plain link.
88-
- `signUpAction`: object for the logged-out Sign Up action. The `label` field sets the button text and the `url` field (default: `https://solidproject.org/get_a_pod`) is the destination opened in a new tab when the button is clicked.
87+
- `loginAction`: object with a `label` for the login button. When `authState` is `logged-out` this is rendered as a `<solid-ui-login-button>` which handles the full OIDC flow; supplying a `url` instead opts out of the built-in flow and renders a plain link. The optional `icon` field supplies a left-aligned icon URL for the rendered login button, but icons are hidden on mobile layout.
88+
- `signUpAction`: object for the logged-out Sign Up action. The `label` field sets the button text, the `url` field (default: `https://solidproject.org/get_a_pod`) is the destination opened in a new tab when the button is clicked, and the optional `icon` field supplies a left-aligned icon URL for the rendered signup button, but icons are hidden on mobile layout.
8989
- `accountLabel`: label for the logged-in dropdown trigger (default: `Accounts`).
9090
- `accountAvatar`: avatar URL used as the logged-in dropdown icon.
91+
- `accountAvatarFallback`: avatar URL used when `accountAvatar` is not provided. This replaces the internal default profile placeholder image.
92+
- `loginIcon`: optional URL string for a left-aligned icon on the login action button, taking precedence over `loginAction.icon`. Icons are still hidden on mobile layout.
93+
- `signUpIcon`: optional URL string for a left-aligned icon on the sign-up action button, taking precedence over `signUpAction.icon`. Icons are still hidden on mobile layout.
9194
- `accountMenu`: array of account entries for the logged-in dropdown.
9295
- `logoutLabel`: string label for the logout button at the bottom of the logged-in dropdown (default: `Log out`). Set to `null` to hide it.
96+
- `logoutIcon`: URL string for a left-aligned icon displayed in the logout menu item. Hidden on mobile layout.
9397

9498
Slots:
9599

@@ -115,10 +119,14 @@ Use `auth-state="logged-out"` to render the `<solid-ui-login-button>` and a Sign
115119
116120
const header = document.querySelector('solid-ui-header')
117121
header.authState = 'logged-out'
118-
// Optionally override the login button label:
119-
header.loginAction = { label: 'Log In' }
120-
// Optionally override the sign-up destination (opens in a new tab):
121-
header.signUpAction = { label: 'Sign Up', url: 'https://myprovider.example/signup' }
122+
// Optionally add an icon to the login button:
123+
header.loginAction = { label: 'Log In', icon: 'https://example.com/login-icon.svg' }
124+
// Optionally override the sign-up destination and add an icon (opens in a new tab):
125+
header.signUpAction = {
126+
label: 'Sign Up',
127+
url: 'https://myprovider.example/signup',
128+
icon: 'https://example.com/signup-icon.svg'
129+
}
122130
123131
header.addEventListener('auth-action-select', ({ detail }) => {
124132
if (detail.role === 'login') {

src/v2/components/header/header.test.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ describe('SolidUIHeaderElement', () => {
5252
const authActionSelected = jest.fn()
5353

5454
header.authState = 'logged-out'
55-
header.loginAction = { label: 'Log in', action: 'login' }
56-
header.signUpAction = { label: 'Sign Up', url: '/signup' }
55+
header.loginAction = { label: 'Log in', action: 'login', icon: 'https://example.com/login-icon.svg' }
56+
header.signUpAction = { label: 'Sign Up', url: '/signup', icon: 'https://example.com/signup-icon.svg' }
57+
header.loginIcon = 'https://example.com/login-icon-top.svg'
58+
header.signUpIcon = 'https://example.com/signup-icon-top.svg'
5759

5860
header.addEventListener('auth-action-select', (event: Event) => {
5961
authActionSelected((event as CustomEvent).detail)
@@ -69,8 +71,10 @@ describe('SolidUIHeaderElement', () => {
6971
expect(loginButton).not.toBeNull()
7072
expect(signUpLink).not.toBeNull()
7173
expect(loginButton.getAttribute('label')).toBe('Log in')
74+
expect(loginButton.getAttribute('icon')).toBe('https://example.com/login-icon-top.svg')
7275
expect(signUpLink.getAttribute('label')).toBe('Sign Up')
7376
expect(signUpLink.getAttribute('signup-url')).toBe('/signup')
77+
expect(signUpLink.getAttribute('icon')).toBe('https://example.com/signup-icon-top.svg')
7478

7579
loginButton.dispatchEvent(new CustomEvent('login-success', { bubbles: true, composed: true }))
7680

@@ -79,13 +83,51 @@ describe('SolidUIHeaderElement', () => {
7983
})
8084
})
8185

86+
it('does not show login or signup icons on mobile layout', async () => {
87+
const header = new Header()
88+
header.authState = 'logged-out'
89+
header.layout = 'mobile'
90+
header.loginAction = { label: 'Log in', action: 'login', icon: 'https://example.com/login-icon.svg' }
91+
header.signUpAction = { label: 'Sign Up', url: '/signup', icon: 'https://example.com/signup-icon.svg' }
92+
header.loginIcon = 'https://example.com/login-icon-top.svg'
93+
header.signUpIcon = 'https://example.com/signup-icon-top.svg'
94+
95+
document.body.appendChild(header)
96+
await header.updateComplete
97+
98+
const shadow = header.shadowRoot
99+
const loginButton = shadow?.querySelector('solid-ui-login-button') as HTMLElement
100+
const signUpButton = shadow?.querySelector('solid-ui-signup-button') as HTMLElement
101+
102+
expect(loginButton?.shadowRoot?.querySelector('.login-button-icon')).toBeNull()
103+
expect(signUpButton?.shadowRoot?.querySelector('.signup-button-icon')).toBeNull()
104+
})
105+
106+
it('uses a custom fallback avatar when no accountAvatar is configured', async () => {
107+
const header = new Header()
108+
109+
header.authState = 'logged-in'
110+
header.accountAvatar = ''
111+
header.accountAvatarFallback = 'https://example.com/fallback-avatar.png'
112+
113+
document.body.appendChild(header)
114+
await header.updateComplete
115+
116+
const shadow = header.shadowRoot
117+
const avatarImg = shadow?.querySelector('.account-avatar img') as HTMLImageElement
118+
119+
expect(avatarImg).not.toBeNull()
120+
expect(avatarImg.src).toContain('https://example.com/fallback-avatar.png')
121+
})
122+
82123
it('renders an accounts dropdown with avatar when logged in', async () => {
83124
const header = new Header()
84125
const accountMenuSelected = jest.fn()
85126

86127
header.authState = 'logged-in'
87128
header.accountLabel = 'Accounts'
88129
header.accountAvatar = 'https://example.com/avatar.png'
130+
header.logoutIcon = 'https://example.com/logout-icon.svg'
89131
header.accountMenu = [
90132
{ label: 'Personal Pod', webid: 'https://pod.example/profile/card#me', action: 'switch-personal' },
91133
{ label: 'Work Pod', webid: 'https://work.example/profile/card#me', url: '/work' }
@@ -116,6 +158,7 @@ describe('SolidUIHeaderElement', () => {
116158
expect(dropdown.hidden).toBe(false)
117159
expect(firstItem.textContent).toContain('Personal Pod')
118160
expect(lastItem.textContent).toContain('Log Out')
161+
expect((lastItem.querySelector('img.logout-action-icon') as HTMLImageElement)?.src).toContain('https://example.com/logout-icon.svg')
119162

120163
firstItem.click()
121164

@@ -128,6 +171,53 @@ describe('SolidUIHeaderElement', () => {
128171
expect(lastItem.textContent?.trim()).toBe('Log Out')
129172
})
130173

174+
it('does not render the logout icon on mobile layout', async () => {
175+
const header = new Header()
176+
header.layout = 'mobile'
177+
header.authState = 'logged-in'
178+
header.logoutIcon = 'https://example.com/logout-icon.svg'
179+
header.logoutLabel = 'Log Out'
180+
181+
document.body.appendChild(header)
182+
await header.updateComplete
183+
184+
const shadow = header.shadowRoot
185+
const trigger = shadow?.getElementById('accountMenuTrigger') as HTMLButtonElement
186+
expect(trigger).not.toBeNull()
187+
188+
trigger.click()
189+
await header.updateComplete
190+
191+
const lastItem = shadow?.querySelectorAll('.account-menu-item-button')[0] as HTMLButtonElement
192+
expect(lastItem).not.toBeNull()
193+
expect(lastItem.querySelector('img.logout-action-icon')).toBeNull()
194+
expect(lastItem.textContent?.trim()).toBe('Log Out')
195+
})
196+
197+
it('does not render account webid on mobile layout', async () => {
198+
const header = new Header()
199+
header.layout = 'mobile'
200+
header.authState = 'logged-in'
201+
header.accountMenu = [
202+
{ label: 'Personal Pod', webid: 'https://pod.example/profile/card#me', action: 'switch-personal' }
203+
]
204+
205+
document.body.appendChild(header)
206+
await header.updateComplete
207+
208+
const shadow = header.shadowRoot
209+
const trigger = shadow?.getElementById('accountMenuTrigger') as HTMLButtonElement
210+
expect(trigger).not.toBeNull()
211+
212+
trigger.click()
213+
await header.updateComplete
214+
215+
const firstItem = shadow?.querySelector('.account-menu-item-button') as HTMLButtonElement
216+
expect(firstItem).not.toBeNull()
217+
expect(firstItem.querySelector('.account-menu-webid')).toBeNull()
218+
expect(firstItem.textContent?.trim()).toBe('Personal Pod')
219+
})
220+
131221
it('supports theme and layout attributes', async () => {
132222
const header = new Header()
133223
header.setAttribute('theme', 'dark')
@@ -177,6 +267,7 @@ describe('SolidUIHeaderElement', () => {
177267
const helpMenuClicked = jest.fn()
178268

179269
header.authState = 'logged-in'
270+
header.helpIcon = ''
180271
header.helpMenuList = [{ label: 'Docs', url: 'https://example.com/docs', target: '_blank' }]
181272

182273
header.addEventListener('help-menu-select', (event: Event) => {
@@ -190,6 +281,7 @@ describe('SolidUIHeaderElement', () => {
190281
const helpTrigger = shadow?.getElementById('helpMenuTrigger') as HTMLButtonElement
191282

192283
expect(helpTrigger?.disabled).toBe(false)
284+
expect(helpTrigger?.textContent?.trim()).toBe('Help')
193285

194286
helpTrigger?.click()
195287
await header.updateComplete

0 commit comments

Comments
 (0)