Skip to content

Commit 464f5f5

Browse files
add: Contacts app
Address book demo. Bespoke contacts-view-pane.js (responsive card grid with avatars, name + handle/email) + Inline edit auto-derived from urn:solid:AddressBook. Sample data: an address book of three webfolks.
1 parent acd9cae commit 464f5f5

6 files changed

Lines changed: 281 additions & 0 deletions

File tree

contacts/app.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "Contacts",
3+
"description": "An address book — collection of contacts as a card grid. Bespoke Contacts view (avatar grid with name + nick/email) plus Inline edit, Edit, View, Source — all schema-driven from urn:solid:AddressBook.",
4+
"entry": "./index.html",
5+
"types": ["urn:solid:AddressBook"],
6+
"icon": "\ud83d\udcd2",
7+
"author": "Melvin Carvalho",
8+
"status": "stable",
9+
"added": "2026-04-19"
10+
}

contacts/contacts-view-pane.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* Contacts view pane — renders a urn:solid:AddressBook as an address-book grid.
3+
*
4+
* Reads AddressBook.title and AddressBook.hasMember (an array of Person
5+
* references — either inlined objects with name/img/email or bare URI
6+
* references). Renders each member as a card with avatar, name, and contact
7+
* pills. Empty state when no members.
8+
*
9+
* Same data, different lens. Drop in alongside ui-pane (Inline) and
10+
* schema-pane (Edit) — LOSOS shows them all as tabs.
11+
*
12+
* AGPL-3.0 — part of solid-apps
13+
*/
14+
15+
import { html, render } from 'https://losos.org/losos/html.js'
16+
17+
const initial = (s) => (s || '?').trim().charAt(0).toUpperCase()
18+
const niceHost = (u) => { try { return new URL(u).host.replace(/^www\./, '') } catch { return u } }
19+
20+
function asMember(m) {
21+
if (typeof m === 'string') return { id: m }
22+
if (m && typeof m === 'object') {
23+
return {
24+
id: m['@id'] || '',
25+
name: m.name,
26+
nick: m.nick,
27+
email: m.email,
28+
img: m.img,
29+
homepage: m.homepage,
30+
}
31+
}
32+
return null
33+
}
34+
35+
export default {
36+
label: 'Contacts',
37+
icon: '\ud83d\udcd2',
38+
39+
canHandle(subject, store) {
40+
const node = store.get(subject.value)
41+
if (!node) return false
42+
const t = store.type(node)
43+
if (!t) return false
44+
return Array.isArray(t)
45+
? t.some(x => /AddressBook/i.test(x))
46+
: /AddressBook/i.test(t)
47+
},
48+
49+
render(subject, lionStore, container, rawData) {
50+
let data = rawData
51+
if (!data) {
52+
const dataEl = document.querySelector('script[type="application/ld+json"]')
53+
try { data = JSON.parse(dataEl.textContent) } catch { return }
54+
}
55+
56+
const title = data.title || 'Contacts'
57+
const description = data.description
58+
const rawMembers = Array.isArray(data.hasMember) ? data.hasMember : (data.hasMember ? [data.hasMember] : [])
59+
const members = rawMembers.map(asMember).filter(Boolean)
60+
61+
const displayName = (m) => m.name || m.nick || (m.id ? niceHost(m.id) : 'Unknown')
62+
63+
render(container, html`
64+
<style>
65+
.cv-bg {
66+
min-height: 100vh;
67+
background:
68+
radial-gradient(circle at 80% 0%, rgba(99,102,241,0.06) 0%, transparent 40%),
69+
radial-gradient(circle at 20% 100%, rgba(236,72,153,0.05) 0%, transparent 40%),
70+
linear-gradient(180deg, #fafaf8 0%, #f0efeb 100%);
71+
padding: 56px 24px 96px;
72+
}
73+
.cv-wrap { max-width: 880px; margin: 0 auto; }
74+
.cv-head {
75+
text-align: center;
76+
margin-bottom: 40px;
77+
}
78+
.cv-title {
79+
font: italic 400 44px/1.1 Georgia, serif;
80+
color: #1a1a1a; margin: 0 0 8px;
81+
letter-spacing: -0.8px;
82+
}
83+
.cv-desc {
84+
font: 400 15px/1.5 'Inter', -apple-system, sans-serif;
85+
color: #888; margin: 0 0 6px;
86+
}
87+
.cv-count {
88+
font: 600 11px/1 'Inter', -apple-system, sans-serif;
89+
letter-spacing: 0.12em; text-transform: uppercase;
90+
color: #999;
91+
}
92+
.cv-grid {
93+
display: grid;
94+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
95+
gap: 16px;
96+
}
97+
.cv-card {
98+
background: #fff;
99+
border: 1px solid #e5e3de;
100+
border-radius: 12px;
101+
padding: 20px;
102+
display: flex; gap: 14px; align-items: center;
103+
text-decoration: none;
104+
color: inherit;
105+
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
106+
}
107+
.cv-card:hover {
108+
transform: translateY(-2px);
109+
box-shadow: 0 8px 24px rgba(0,0,0,0.06);
110+
border-color: #6366f1;
111+
}
112+
.cv-avatar {
113+
width: 56px; height: 56px; border-radius: 50%;
114+
object-fit: cover; flex: 0 0 auto;
115+
background: #f5f4f0;
116+
}
117+
.cv-avatar-fallback {
118+
width: 56px; height: 56px; border-radius: 50%;
119+
background: linear-gradient(135deg, #6366f1, #8b5cf6);
120+
color: #fff; font: 600 22px/56px Georgia, serif;
121+
text-align: center; flex: 0 0 auto;
122+
}
123+
.cv-meta { min-width: 0; flex: 1; }
124+
.cv-name {
125+
font: 600 16px/1.2 'Inter', -apple-system, sans-serif;
126+
color: #1a1a1a; margin: 0 0 3px;
127+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
128+
}
129+
.cv-sub {
130+
font: 400 13px/1.4 'Inter', -apple-system, sans-serif;
131+
color: #888;
132+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
133+
}
134+
.cv-empty {
135+
background: #fff;
136+
border: 1px dashed #d4d2cd;
137+
border-radius: 12px;
138+
padding: 60px 20px;
139+
text-align: center;
140+
color: #999;
141+
font: 400 15px/1.5 'Inter', -apple-system, sans-serif;
142+
}
143+
</style>
144+
145+
<div class="cv-bg">
146+
<div class="cv-wrap">
147+
<div class="cv-head">
148+
<h1 class="cv-title">${title}</h1>
149+
${description ? html`<p class="cv-desc">${description}</p>` : null}
150+
<div class="cv-count">${members.length} ${members.length === 1 ? 'contact' : 'contacts'}</div>
151+
</div>
152+
153+
${members.length === 0
154+
? html`<div class="cv-empty">No contacts yet.</div>`
155+
: html`
156+
<div class="cv-grid">
157+
${members.map(function(m) {
158+
const name = displayName(m)
159+
const sub = m.nick ? '@' + m.nick : (m.email || (m.id ? niceHost(m.id) : ''))
160+
const href = m.id || m.homepage || '#'
161+
return html`
162+
<a class="cv-card" href="${href}" target="_blank" rel="noopener">
163+
${m.img
164+
? html`<img class="cv-avatar" src="${m.img}" alt="${name}" onerror="${function(e) { const f = document.createElement('div'); f.className = 'cv-avatar-fallback'; f.textContent = initial(name); e.target.replaceWith(f) }}" />`
165+
: html`<div class="cv-avatar-fallback">${initial(name)}</div>`
166+
}
167+
<div class="cv-meta">
168+
<div class="cv-name">${name}</div>
169+
${sub ? html`<div class="cv-sub">${sub}</div>` : null}
170+
</div>
171+
</a>
172+
`
173+
})}
174+
</div>
175+
`
176+
}
177+
</div>
178+
</div>
179+
`)
180+
}
181+
}

contacts/index.html

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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>Contacts — 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: an address book of well-known web folks. Inlined Person
17+
objects so the demo is self-contained — in production each member
18+
would typically be a bare URI reference to an external Person resource. -->
19+
<script type="application/ld+json">
20+
{
21+
"@context": { "@vocab": "urn:solid:" },
22+
"@id": "#this",
23+
"@type": "AddressBook",
24+
"title": "Web Heroes",
25+
"description": "A few people who shaped the web.",
26+
"hasMember": [
27+
{
28+
"@id": "https://www.w3.org/People/Berners-Lee/card#i",
29+
"@type": "Person",
30+
"name": "Sir Tim Berners-Lee",
31+
"nick": "timbl",
32+
"email": "timbl@w3.org",
33+
"homepage": "https://www.w3.org/People/Berners-Lee/",
34+
"img": "https://www.w3.org/People/Berners-Lee/Press/stock-photos/tim-berners-lee-inrupt-2024.jpg"
35+
},
36+
{
37+
"@id": "https://melvincarvalho.com/#me",
38+
"@type": "Person",
39+
"name": "Melvin Carvalho",
40+
"nick": "melvincarvalho",
41+
"homepage": "https://melvincarvalho.com/"
42+
},
43+
{
44+
"@id": "https://ruben.verborgh.org/profile/#me",
45+
"@type": "Person",
46+
"name": "Ruben Verborgh",
47+
"nick": "rubenverborgh",
48+
"homepage": "https://ruben.verborgh.org/"
49+
}
50+
]
51+
}
52+
</script>
53+
54+
<!-- Login: Nostr + Solid in one tag. -->
55+
<script src="https://unpkg.com/xlogin"></script>
56+
57+
<!-- Panes — bespoke Contacts grid first, then schema-driven. -->
58+
<script type="module" data-pane src="./contacts-view-pane.js"></script>
59+
<script type="module" data-pane src="https://solid-ui.github.io/ui-pane.js"></script>
60+
<script type="module" data-pane src="https://losos.org/panes/schema-pane.js"></script>
61+
<script type="module" data-pane src="https://solid-panes.github.io/schema-view.js"></script>
62+
<script type="module" data-pane src="https://losos.org/panes/source-pane.js"></script>
63+
64+
<div id="losos"></div>
65+
66+
<!-- Boot: autoSchema patches $schema, then shell -->
67+
<script type="module">
68+
import { autoSchema } from 'https://solid-panes.github.io/auto-schema.js'
69+
const result = await autoSchema()
70+
console.log('[contacts] autoSchema patched', result.patched, 'island(s),', result.skipped, 'skipped')
71+
await import('https://losos.org/losos/shell.js')
72+
</script>
73+
74+
</body>
75+
</html>

corpus.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{"name":"Bookmark","description":"A saved web reference. Bespoke card view (favicon, title link, description, tag pills) plus Inline edit, Edit, View, Source — all schema-driven from urn:solid:Bookmark.","entry":"./index.html","types":["urn:solid:Bookmark"],"icon":"🔖","author":"Melvin Carvalho","status":"stable","added":"2026-04-19"}
2+
{"name":"Contacts","description":"An address book — collection of contacts as a card grid. Bespoke Contacts view (avatar grid with name + nick/email) plus Inline edit, Edit, View, Source — all schema-driven from urn:solid:AddressBook.","entry":"./index.html","types":["urn:solid:AddressBook"],"icon":"📒","author":"Melvin Carvalho","status":"stable","added":"2026-04-19"}
23
{"name":"Feed","description":"A fediverse-style timeline. Renders an OrderedCollection of Notes as cards with author chips, relative timestamps, content warnings, and hashtags. Companion to the Profile app.","entry":"./index.html","types":["urn:solid:OrderedCollection","urn:solid:Note"],"icon":"📜","author":"Melvin Carvalho","status":"stable","added":"2026-04-19"}
34
{"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"}
45
{"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
@@ -10,6 +10,17 @@
1010
"status": "stable",
1111
"manifest": "/bookmark/app.json"
1212
},
13+
"contacts": {
14+
"name": "Contacts",
15+
"description": "An address book — collection of contacts as a card grid. Bespoke Contacts view (avatar grid with name + nick/email) plus Inline edit, Edit, View, Source — all schema-driven from urn:solid:AddressBook.",
16+
"entry": "/contacts/index.html",
17+
"types": [
18+
"urn:solid:AddressBook"
19+
],
20+
"icon": "📒",
21+
"status": "stable",
22+
"manifest": "/contacts/app.json"
23+
},
1324
"feed": {
1425
"name": "Feed",
1526
"description": "A fediverse-style timeline. Renders an OrderedCollection of Notes as cards with author chips, relative timestamps, content warnings, and hashtags. Companion to the Profile app.",

reverse-index.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"urn:solid:Bookmark": [
33
"/bookmark/"
44
],
5+
"urn:solid:AddressBook": [
6+
"/contacts/"
7+
],
58
"urn:solid:OrderedCollection": [
69
"/feed/"
710
],

0 commit comments

Comments
 (0)