Skip to content

Commit 22d55d2

Browse files
Add Profile app — beautiful schema-aware Person page
A second app at solid-apps.github.io/profile/. Showcases what a custom view pane can do that the generic schema-view can't. profile/profile-view-pane.js (~150 lines): - Hero photo (200x200, circular, drop shadow, white border ring) - Fallback to gradient + initial when no img - Large italic Georgia name (56px, -1.2px tracking) - @nick handle in Inter - Optional WebID/IRI badge - Contact pills (email mailto, homepage link) — hover lift + indigo border - "Knows" network as pill list, clickable WebIDs - Subtle indigo+pink radial gradient background Same Person data, four lenses: Profile — bespoke beautiful view (this pane, default tab) Edit — schema-pane auto-form View — schema-view generic display Source — raw JSON-LD Demonstrates the pattern: when generic schema-view isn't pretty enough, ship a bespoke per-type pane in your app dir. Schema-driven panes still work alongside; users get both. Sample data: Sir Tim Berners-Lee (same Person used by LOSOS vcard demo) through urn:solid:Person rather than vcard:Individual — keeps the cross-app interop story coherent (the urn:solid resolver in LOSOS could auto-canonicalise vcard:Individual → urn:solid:Person if shipped). xlogin already wired (Nostr + Solid one-tag drop-in). Open with ?uri=<your-webid> to render any Solid profile against this layout.
1 parent 88e04cd commit 22d55d2

6 files changed

Lines changed: 257 additions & 0 deletions

File tree

corpus.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
{"name":"Profile","description":"Your card on the web. Beautiful schema-driven Person page with bespoke Profile view, schema-driven Edit, and Source. Open ?uri=<your-webid> to view any Solid profile.","entry":"./index.html","types":["urn:solid:Person"],"icon":"👤","author":"Melvin Carvalho","status":"stable","added":"2026-04-19"}
12
{"name":"Todos","description":"A list of tasks. Built on LOSOS's bespoke todo-pane via the urn:solid:Tracker manifest.","entry":"./index.html","types":["urn:solid:Tracker","urn:solid:Vtodo"],"icon":"","author":"Melvin Carvalho","status":"stable","added":"2026-04-19"}

index.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
{
2+
"profile": {
3+
"name": "Profile",
4+
"description": "Your card on the web. Beautiful schema-driven Person page with bespoke Profile view, schema-driven Edit, and Source. Open ?uri=<your-webid> to view any Solid profile.",
5+
"entry": "/profile/index.html",
6+
"types": [
7+
"urn:solid:Person"
8+
],
9+
"icon": "👤",
10+
"status": "stable",
11+
"manifest": "/profile/app.json"
12+
},
213
"todos": {
314
"name": "Todos",
415
"description": "A list of tasks. Built on LOSOS's bespoke todo-pane via the urn:solid:Tracker manifest.",

profile/app.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "Profile",
3+
"description": "Your card on the web. Beautiful schema-driven Person page with bespoke Profile view, schema-driven Edit, and Source. Open ?uri=<your-webid> to view any Solid profile.",
4+
"entry": "./index.html",
5+
"types": ["urn:solid:Person"],
6+
"icon": "\ud83d\udc64",
7+
"author": "Melvin Carvalho",
8+
"status": "stable",
9+
"added": "2026-04-19"
10+
}

profile/index.html

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Profile — solid-apps</title>
7+
<style>
8+
* { margin: 0; box-sizing: border-box; }
9+
body { font-family: 'Inter', -apple-system, sans-serif; background: #fafaf8; }
10+
#losos { max-width: 100% !important; }
11+
#losos > div { max-width: 100% !important; width: 100% !important; }
12+
</style>
13+
</head>
14+
<body>
15+
16+
<!-- Sample data: Sir Tim Berners-Lee. Same person used by the LOSOS vcard demo,
17+
here through a urn:solid:Person lens to keep the cross-app interop story coherent. -->
18+
<script type="application/ld+json">
19+
{
20+
"@context": { "@vocab": "urn:solid:" },
21+
"@id": "https://www.w3.org/People/Berners-Lee/card#i",
22+
"@type": "Person",
23+
"name": "Sir Tim Berners-Lee",
24+
"nick": "timbl",
25+
"email": "timbl@w3.org",
26+
"homepage": "https://www.w3.org/People/Berners-Lee/",
27+
"img": "https://www.w3.org/People/Berners-Lee/Press/stock-photos/tim-berners-lee-inrupt-2024.jpg",
28+
"knows": [
29+
"https://csarven.ca/#i",
30+
"https://melvincarvalho.com/#me",
31+
"https://ruben.verborgh.org/profile/#me"
32+
]
33+
}
34+
</script>
35+
36+
<!-- Login: Nostr + Solid in one tag. -->
37+
<script src="https://unpkg.com/xlogin"></script>
38+
39+
<!-- Panes — order matters for tab order. Profile (bespoke) first, then schema-driven. -->
40+
<script type="module" data-pane src="./profile-view-pane.js"></script>
41+
<script type="module" data-pane src="https://losos.org/panes/schema-pane.js"></script>
42+
<script type="module" data-pane src="https://solid-panes.github.io/schema-view.js"></script>
43+
<script type="module" data-pane src="https://losos.org/panes/source-pane.js"></script>
44+
45+
<div id="losos"></div>
46+
47+
<!-- Boot: autoSchema patches $schema (so schema-pane + schema-view fire), then shell -->
48+
<script type="module">
49+
import { autoSchema } from 'https://solid-panes.github.io/auto-schema.js'
50+
const result = await autoSchema()
51+
console.log('[profile] autoSchema patched', result.patched, 'island(s),', result.skipped, 'skipped')
52+
await import('https://losos.org/losos/shell.js')
53+
</script>
54+
55+
</body>
56+
</html>

profile/profile-view-pane.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/**
2+
* Profile view pane — renders a urn:solid:Person as a beautiful "this is me" page.
3+
*
4+
* Schema-aware (reads Person fields: name, nick, email, homepage, img, knows)
5+
* but bespoke layout — hero photo, large italic name, contact pills, knows
6+
* network. The custom counterpart to the generic schema-view.
7+
*
8+
* Same data, different lens. Drop in alongside schema-pane (Edit) and
9+
* schema-view (generic View) — LOSOS shows all three as tabs.
10+
*
11+
* AGPL-3.0 — part of solid-apps
12+
*/
13+
14+
import { html, render } from 'https://losos.org/losos/html.js'
15+
16+
const initial = (s) => (s || '?').trim().charAt(0).toUpperCase()
17+
const stripScheme = (u) => (u || '').replace(/^https?:\/\//, '').replace(/\/$/, '')
18+
const niceLink = (u) => {
19+
try {
20+
const url = new URL(u)
21+
return url.host + (url.pathname === '/' ? '' : url.pathname)
22+
} catch { return u }
23+
}
24+
25+
export default {
26+
label: 'Profile',
27+
icon: '\ud83d\udc64',
28+
29+
canHandle(subject, store) {
30+
const node = store.get(subject.value)
31+
if (!node) return false
32+
const t = store.type(node)
33+
if (!t) return false
34+
return Array.isArray(t)
35+
? t.some(x => /Person/i.test(x))
36+
: /Person/i.test(t)
37+
},
38+
39+
render(subject, lionStore, container, rawData) {
40+
let data = rawData
41+
if (!data) {
42+
const dataEl = document.querySelector('script[type="application/ld+json"]')
43+
try { data = JSON.parse(dataEl.textContent) } catch { return }
44+
}
45+
46+
const name = data.name || data.nick || data['@id'] || 'Unknown'
47+
const nick = data.nick
48+
const email = data.email
49+
const homepage = data.homepage
50+
const img = data.img
51+
const knows = Array.isArray(data.knows) ? data.knows : (data.knows ? [data.knows] : [])
52+
const id = data['@id'] || ''
53+
const showId = id && id !== '#this' && !id.startsWith('#')
54+
55+
render(container, html`
56+
<style>
57+
.pv-bg {
58+
min-height: 100vh;
59+
background:
60+
radial-gradient(circle at 20% 0%, rgba(99,102,241,0.08) 0%, transparent 40%),
61+
radial-gradient(circle at 80% 100%, rgba(236,72,153,0.06) 0%, transparent 40%),
62+
linear-gradient(180deg, #fafaf8 0%, #f0efeb 100%);
63+
padding: 64px 24px 96px;
64+
}
65+
.pv-card {
66+
max-width: 600px; margin: 0 auto; text-align: center;
67+
}
68+
.pv-photo {
69+
width: 200px; height: 200px; border-radius: 50%;
70+
object-fit: cover; margin: 0 auto 32px;
71+
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
72+
border: 4px solid #fff;
73+
display: block;
74+
}
75+
.pv-photo-fallback {
76+
width: 200px; height: 200px; border-radius: 50%;
77+
background: linear-gradient(135deg, #6366f1, #8b5cf6);
78+
color: #fff; font: 600 80px/200px Georgia, serif;
79+
margin: 0 auto 32px;
80+
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
81+
border: 4px solid #fff;
82+
}
83+
.pv-name {
84+
font: italic 400 56px/1.05 Georgia, serif;
85+
color: #1a1a1a; margin: 0 0 6px;
86+
letter-spacing: -1.2px;
87+
}
88+
.pv-nick {
89+
font: 500 16px/1.5 'Inter', -apple-system, sans-serif;
90+
color: #888; margin: 0 0 24px;
91+
}
92+
.pv-id {
93+
display: inline-block;
94+
font: 400 11px/1.5 ui-monospace, SFMono-Regular, monospace;
95+
color: #aaa; margin-bottom: 40px;
96+
word-break: break-all; max-width: 100%;
97+
padding: 4px 10px; border-radius: 4px;
98+
background: rgba(0,0,0,0.03);
99+
}
100+
.pv-contact {
101+
display: flex; gap: 10px; justify-content: center;
102+
flex-wrap: wrap; margin-bottom: 56px;
103+
}
104+
.pv-link {
105+
display: inline-flex; align-items: center; gap: 8px;
106+
padding: 10px 18px; background: #fff;
107+
border: 1px solid #e5e3de; border-radius: 999px;
108+
color: #1a1a1a; text-decoration: none;
109+
font: 500 14px/1 'Inter', -apple-system, sans-serif;
110+
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
111+
}
112+
.pv-link:hover {
113+
transform: translateY(-2px);
114+
box-shadow: 0 6px 20px rgba(0,0,0,0.08);
115+
border-color: #6366f1;
116+
}
117+
.pv-link-icon { font-size: 16px; opacity: 0.85; }
118+
.pv-knows {
119+
padding-top: 40px;
120+
border-top: 1px solid #e5e3de;
121+
}
122+
.pv-knows-title {
123+
font: 600 11px/1 'Inter', -apple-system, sans-serif;
124+
letter-spacing: 0.12em; text-transform: uppercase;
125+
color: #999; margin: 0 0 20px;
126+
}
127+
.pv-knows-list {
128+
list-style: none; padding: 0; margin: 0;
129+
display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;
130+
}
131+
.pv-knows-link {
132+
padding: 7px 14px; background: #fff;
133+
border: 1px solid #e5e3de; border-radius: 999px;
134+
font: 400 13px/1 'Inter', sans-serif;
135+
color: #1a5276; text-decoration: none;
136+
transition: border-color 0.18s, color 0.18s;
137+
}
138+
.pv-knows-link:hover { border-color: #6366f1; color: #6366f1; }
139+
</style>
140+
141+
<div class="pv-bg">
142+
<div class="pv-card">
143+
${img
144+
? html`<img class="pv-photo" src="${img}" alt="${name}" onerror="${function(e) { const f = document.createElement('div'); f.className = 'pv-photo-fallback'; f.textContent = initial(name); e.target.replaceWith(f) }}" />`
145+
: html`<div class="pv-photo-fallback">${initial(name)}</div>`
146+
}
147+
148+
<h1 class="pv-name">${name}</h1>
149+
${nick ? html`<div class="pv-nick">@${nick}</div>` : null}
150+
${showId ? html`<div class="pv-id">${id}</div>` : null}
151+
152+
${(email || homepage) ? html`
153+
<div class="pv-contact">
154+
${email ? html`<a class="pv-link" href="${'mailto:' + email}"><span class="pv-link-icon">\u2709</span>${email}</a>` : null}
155+
${homepage ? html`<a class="pv-link" href="${homepage}" target="_blank" rel="noopener"><span class="pv-link-icon">\ud83c\udf10</span>${niceLink(homepage)}</a>` : null}
156+
</div>
157+
` : null}
158+
159+
${knows.length > 0 ? html`
160+
<div class="pv-knows">
161+
<div class="pv-knows-title">Knows</div>
162+
<ul class="pv-knows-list">
163+
${knows.map(function(k) {
164+
const ref = (typeof k === 'string') ? k : (k && k['@id'])
165+
if (!ref) return null
166+
const label = (typeof k === 'object' && k.name) ? k.name : niceLink(ref)
167+
return html`<li><a class="pv-knows-link" href="${ref}" target="_blank" rel="noopener">${label}</a></li>`
168+
})}
169+
</ul>
170+
</div>
171+
` : null}
172+
</div>
173+
</div>
174+
`)
175+
}
176+
}

reverse-index.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
"urn:solid:Person": [
3+
"/profile/"
4+
],
25
"urn:solid:Tracker": [
36
"/todos/"
47
],

0 commit comments

Comments
 (0)