diff --git a/README.md b/README.md index 2152b8bab..37d5bafd4 100644 --- a/README.md +++ b/README.md @@ -216,20 +216,6 @@ I want this all to be presented flexible in the component. * Raptor mini: when we are on layout mobile we do not want to display the help menu at all. -* Raptor mini: Create for me a footer Lit Component in tsy style of the components I have and under v2. Take the code from this index.ts to start with. - -* Raptor mini: Good. Now, I want the footer to be a rectangular with round corners, grey background and it should have an adjustable position. - -* Raptor mini: The content of the footer should be different upon loggedin or not. -If not logged in, it should say: -Title Public View -You are viewving this profile as a guest, -And if logged in: -Title: Logged in View -You are logged in as nameOfLoggedIn user. - -* Raptor mini: add a readme to the Footer component with example. - * Claude Sonnet 4.6: Make the drop down as a list under the input field and enlarge the pop up, make it higher, adjustable to fit the drop down. And make the drop down arrow area larger * GPT-5.4 Model: can you wire up the keyboard interactions and aria attributes for Select? diff --git a/src/components/footer/index.ts b/src/components/footer/index.ts deleted file mode 100644 index 3e833d8e5..000000000 --- a/src/components/footer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../../v2/components/layout/footer' diff --git a/src/types/custom-elements.d.ts b/src/types/custom-elements.d.ts index 00ae89ef0..7366bab22 100644 --- a/src/types/custom-elements.d.ts +++ b/src/types/custom-elements.d.ts @@ -14,7 +14,6 @@ import type DialogFooter from '../components/dialog-footer/DialogFooter' import type DialogHeader from '../components/dialog-header/DialogHeader' import type DialogProvider from '../components/dialog-provider/DialogProvider' import type DialogsRoot from '../components/dialogs-root/DialogsRoot' -import type Footer from '../components/footer/Footer' import type Guard from '../components/guard/Guard' import type Header from '../components/header/Header' import type Input from '../components/input/Input' @@ -42,7 +41,6 @@ declare global { 'solid-ui-dialog-header': DialogHeader 'solid-ui-dialog-provider': DialogProvider 'solid-ui-dialogs-root': DialogsRoot - 'solid-ui-footer': Footer 'solid-ui-guard': Guard 'solid-ui-header': Header 'solid-ui-input': Input diff --git a/src/utils/headerHelpers.ts b/src/utils/headerHelpers.ts new file mode 100644 index 000000000..d212f4b25 --- /dev/null +++ b/src/utils/headerHelpers.ts @@ -0,0 +1,126 @@ +/* + Copied from mashlib/src/global/metadata.ts + */ +import { IndexedFormula, LiveStore, NamedNode, parse, sym } from 'rdflib' +import ns from '../lib/ns' + +/* @ts-ignore no-console */ +type ThrottleOptions = { + leading?: boolean; + throttling?: boolean; + trailing?: boolean; +} + +/** + * @ignore exporting this only for the unit test + */ +export function getPod (): NamedNode { + const { origin, pathname } = document.location + const isDatabrowserShell = document.body?.dataset?.appShell === 'databrowser' + const segments = pathname.split('/').filter(Boolean) + const lastSegment = segments[segments.length - 1] || '' + const looksLikeFile = /\.[^/]+$/.test(lastSegment) + + if (isDatabrowserShell && segments.length > 0 && !looksLikeFile) { + return sym(`${origin}/${segments[0]}/`) + } + + // Root-hosted pods and static databrowser pages still use the site root. + return sym(origin).site() +} +/** + */ +export async function getPodOwner (pod: NamedNode, store: LiveStore): Promise { + // This is a massive guess. In future + // const podOwner = sym(`${pod.uri}profile/card#me`) + + try { + // load turtle Container representation + if (!store.any(pod, null, ns.ldp('Container'), pod)) { + const response = await store.fetcher.webOperation('GET', pod.uri, store.fetcher.initFetchOptions(pod.uri, { headers: { accept: 'text/turtle' } })) + const containerTurtle = response.responseText + parse(containerTurtle as string, store, pod.uri, 'text/turtle') + } + } catch (err) { + console.error('Error loading pod ' + pod + ': ' + err) + return null + } + if (!store.holds(pod, ns.rdf('type'), ns.space('Storage'), pod)) { + console.warn('Pod ' + pod + ' does not declare itself as a space:Storage') + return null + } + const podOwner = store.any(pod, ns.solid('owner'), null, pod) || + store.any(null, ns.space('storage'), pod, pod) + if (podOwner) { + try { + await store.fetcher.load((podOwner as NamedNode).doc()) + } catch (_err) { + console.warn('Unable to load profile of pod owner ' + podOwner) + return null + } + if (!store.holds(podOwner, ns.space('storage'), pod, (podOwner as NamedNode).doc())) { + console.warn(`Pod owner ${podOwner} does NOT list pod ${pod} as their storage`) + } + return podOwner as NamedNode// Success! + } else { // pod owner not declared in pod + // @@ TODO: This is given the structure that NSS provides + // This is a massive guess. For old pods which don't have owner link + const guess = sym(`${pod.uri}profile/card#me`) + try { + // @ts-ignore LiveStore always has fetcher + await store.fetcher.load(guess) + } catch (_err) { + console.error('Ooops. Guessed wrong pod owner webid {$guess} : can\'t load it.') + return null + } + if (store.holds(guess, ns.space('storage'), pod, guess.doc())) { + console.warn('Using guessed pod owner webid but it links back.') + return guess + } + return null + } +} +/** + * @ignore exporting this only for the unit test + */ +export function getName (store: IndexedFormula, user: NamedNode): string { + return store.anyValue(user, ns.vcard('fn'), null, user.doc()) || + store.anyValue(user, ns.foaf('name'), null, user.doc()) || + user.uri +} +/** + * @ignore exporting this only for the unit test + */ +export function throttle (func: Function, wait: number, options: ThrottleOptions = {}): (...args: any[]) => any { + let context: any, + args: any, + result: any + let timeout: any = null + let previous = 0 + const later = function () { + previous = !options.leading ? 0 : Date.now() + timeout = null + result = func.apply(context, args) + if (!timeout) context = args = null + } + return function () { + const now = Date.now() + if (!previous && !options.leading) previous = now + const remaining = wait - (now - previous) + // @ts-ignore + context = this + args = arguments + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + previous = now + result = func.apply(context, args) + if (!timeout) context = args = null + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining) + } + return result + } +} diff --git a/src/v2/components/layout/footer/Footer.test.ts b/src/v2/components/layout/footer/Footer.test.ts deleted file mode 100644 index 3c777a508..000000000 --- a/src/v2/components/layout/footer/Footer.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { Footer } from './Footer' -import './index' -import { authn } from 'solid-logic' - -describe('SolidUIFooterElement', () => { - it('is defined as a custom element', () => { - const defined = customElements.get('solid-ui-footer') - expect(defined).toBe(Footer) - }) - - it('renders a public view when not logged in', async () => { - const footer = new Footer() - document.body.appendChild(footer) - await footer.updateComplete - - const shadow = footer.shadowRoot - expect(shadow).not.toBeNull() - expect(shadow?.textContent).toContain('Public View') - expect(shadow?.textContent).toContain('You are viewing this profile as a guest.') - }) - - it('renders a logged in view when the user is authenticated', async () => { - const currentUser = { uri: 'https://alice.example/profile/card#me', equals: vi.fn(() => true) } - const currentUserSpy = vi.spyOn(authn, 'currentUser').mockReturnValue(currentUser as any) - - const footer = new Footer() - document.body.appendChild(footer) - await footer.updateComplete - - const shadow = footer.shadowRoot - expect(shadow).not.toBeNull() - expect(shadow?.textContent).toContain('Logged in View') - expect(shadow?.textContent).toContain('You are logged in as') - const link = shadow?.querySelector('a') - expect(link?.getAttribute('href')).toBe('https://alice.example/profile/card#me') - expect(link?.textContent).toBe('https://alice.example/profile/card#me') - - currentUserSpy.mockRestore() - }) - - it('defaults layout to desktop', async () => { - const footer = new Footer() - document.body.appendChild(footer) - await footer.updateComplete - - expect(footer.layout).toBe('desktop') - expect(footer.getAttribute('layout')).toBe('desktop') - }) - - it('applies mobile layout styles by removing border, box-shadow and border-radius', async () => { - const footer = new Footer() - footer.layout = 'mobile' - document.body.appendChild(footer) - await footer.updateComplete - - const style = footer.shadowRoot?.querySelector('style')?.textContent - expect(style).toContain(':host([layout=\'mobile\'])') - expect(style).toContain('border: none;') - expect(style).toContain('box-shadow: none;') - expect(footer.getAttribute('layout')).toBe('mobile') - }) -}) diff --git a/src/v2/components/layout/footer/Footer.ts b/src/v2/components/layout/footer/Footer.ts deleted file mode 100644 index cdbe3ff71..000000000 --- a/src/v2/components/layout/footer/Footer.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { LitElement, html, css } from 'lit' -import type { LiveStore, NamedNode } from 'rdflib' -import { authSession, authn } from 'solid-logic' -import { getName } from '../../../../utils/headerFooterHelpers' - -export class Footer extends LitElement { - static properties = { - theme: { type: String, reflect: true }, - layout: { type: String, reflect: true }, - position: { type: String, reflect: true }, - top: { type: String, reflect: true }, - right: { type: String, reflect: true }, - bottom: { type: String, reflect: true }, - left: { type: String, reflect: true }, - store: { type: Object, attribute: false }, - _user: { state: true } - } - - static styles = css` - :host { - display: block; - position: var(--footer-position, static); - top: var(--footer-top, auto); - right: var(--footer-right, auto); - bottom: var(--footer-bottom, auto); - left: var(--footer-left, auto); - width: auto; - max-width: var(--footer-max-width, none); - margin: var(--footer-margin, 0); - box-sizing: border-box; - color: var(--footer-text, #4f4f4f); - background: transparent; - border: 1px solid var(--footer-border, rgba(0, 0, 0, 0.12)); - border-radius: var(--footer-border-radius, 1rem); - box-shadow: var(--footer-box-shadow, 0 1px 6px rgba(0, 0, 0, 0.08)); - font-family: var(--font-family-base, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif); - } - - .footer { - display: flex; - justify-content: flex-start; - align-items: flex-start; - gap: 0.25rem; - font-size: 0.75rem; - line-height: 1.5; - text-align: left; - } - - .footer a { - color: var(--footer-link, #4b32a8); - text-decoration: none; - font-weight: 600; - } - - .footer a:hover { - text-decoration: underline; - } - - .footer span { - color: inherit; - } - - .footer div > strong { - display: block; - margin-bottom: 0.5rem; - } - - :host([layout='mobile']) { - border: none; - box-shadow: none; - border-radius: 0; - } - ` - - declare theme: 'light' | 'dark' - declare layout: 'desktop' | 'mobile' - declare position: 'static' | 'absolute' | 'relative' | 'fixed' | 'sticky' - declare top: string - declare right: string - declare bottom: string - declare left: string - declare store: LiveStore | null - declare _user: NamedNode | null - - constructor () { - super() - this.theme = 'light' - this.layout = 'desktop' - this.position = 'static' - this.top = 'auto' - this.right = 'auto' - this.bottom = 'auto' - this.left = 'auto' - this.store = null - this._user = null - this._updateFooter = this._updateFooter.bind(this) - } - - connectedCallback () { - super.connectedCallback() - authSession.events.on('login', this._updateFooter) - authSession.events.on('logout', this._updateFooter) - this._updateFooter() - } - - disconnectedCallback () { - if (typeof authSession.events.off === 'function') { - authSession.events.off('login', this._updateFooter) - authSession.events.off('logout', this._updateFooter) - } - super.disconnectedCallback() - } - - updated (changedProperties: Map) { - if ( - changedProperties.has('position') || - changedProperties.has('top') || - changedProperties.has('right') || - changedProperties.has('bottom') || - changedProperties.has('left') - ) { - this._updatePositionStyles() - } - } - - private _updatePositionStyles () { - this.style.setProperty('--footer-position', this.position) - this.style.setProperty('--footer-top', this.top) - this.style.setProperty('--footer-right', this.right) - this.style.setProperty('--footer-bottom', this.bottom) - this.style.setProperty('--footer-left', this.left) - } - - private _updateFooter () { - this._user = authn.currentUser() - } - - render () { - return html` -
- ${this._renderFooterContent()} -
- ` - } - - private _renderFooterContent () { - if (!this._user) { - return html` -
- Public View -
You are viewing this profile as a guest.
-
- ` - } - - const userName = this.store ? getName(this.store, this._user) : this._user.uri - - return html` -
- Logged in View -
- You are logged in as - ${userName}. -
-
- ` - } -} diff --git a/src/v2/components/layout/footer/README.md b/src/v2/components/layout/footer/README.md deleted file mode 100644 index c868dba69..000000000 --- a/src/v2/components/layout/footer/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# solid-ui-footer component - -A Lit-based custom element that renders a footer panel for Solid applications. It supports a rounded rectangle layout, grey background, and adjustable positioning via attributes. - -## Installation - -```bash -npm install solid-ui -``` - -## Usage in a bundled project (webpack, Vite, Rollup, etc.) - -Import once to register the custom element and get access to the type: - -```javascript -import { Footer } from 'solid-ui/components/footer' -``` - -Then use the element in HTML or in your framework templates: - -```html - - -``` - -If you need the footer to be relative to a container, use `position="relative"` or `position="absolute"` along with `top`, `left`, `right`, and `bottom` as needed. - -## Usage in a plain HTML page (CDN / script tag) - -Load `rdflib` and `solid-logic` first, then import the footer bundle: - -```html - - - - - -``` - -## TypeScript - -Types are included. Import the exported element class: - -```typescript -import { Footer } from 'solid-ui/components/footer' - -const footer = document.querySelector('solid-ui-footer') as Footer -footer.position = 'fixed' -footer.bottom = '1rem' -``` - -## API - -Properties / attributes: - -- `layout`: `desktop` (default) or `mobile`. In mobile layout, the border, box-shadow and border-radius are removed. -- `position`: `static`, `absolute`, `relative`, `fixed`, or `sticky`. -- `top`: CSS offset for the top edge when `position` is not `static`. -- `right`: CSS offset for the right edge when `position` is not `static`. -- `bottom`: CSS offset for the bottom edge when `position` is not `static`. -- `left`: CSS offset for the left edge when `position` is not `static`. -- `store`: an `rdflib` store instance used to resolve the logged-in user name from the current Solid session. - -## Display behavior - -- When no user is logged in, the footer displays a public-view message. -- When a user is logged in, the footer displays a logged-in message and links the current profile name to the user profile URI. - -## Styling - -Customize the footer using CSS variables: - -- `--footer-bg` — background color (default: `#e6e6e6`). -- `--footer-text` — text color (default: `#4f4f4f`). -- `--footer-border-radius` — corner radius (default: `1rem`). -- `--footer-box-shadow` — box shadow. -- `--footer-link` — link color. - -## Example - -```html - -``` - -```typescript -import { Footer } from 'solid-ui/components/footer' -import type { LiveStore } from 'rdflib' - -const footer = document.querySelector('solid-ui-footer') as Footer -footer.position = 'fixed' -footer.bottom = '1rem' -footer.left = '1rem' -footer.right = '1rem' -footer.store = myRdflibStore as LiveStore -``` - -## Testing - -The component is covered by unit tests under `src/v2/components/layout/footer/Footer.test.ts`. diff --git a/src/v2/components/layout/footer/index.ts b/src/v2/components/layout/footer/index.ts deleted file mode 100644 index 7df3d81d0..000000000 --- a/src/v2/components/layout/footer/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Footer } from './Footer' - -export { Footer } - -const FOOTER_TAG_NAME = 'solid-ui-footer' - -if (!customElements.get(FOOTER_TAG_NAME)) { - customElements.define(FOOTER_TAG_NAME, Footer) -}