Skip to content

Commit acd9cae

Browse files
add: Bookmark app
Single-bookmark demo. Bespoke bookmark-view-pane.js (favicon hero, title link, description, tag pills, saved-on footer) + Inline edit auto-derived from urn:solid:Bookmark schema. Sample data: a bookmark of solidproject.org attributed to TimBL.
1 parent 6189337 commit acd9cae

6 files changed

Lines changed: 240 additions & 0 deletions

File tree

bookmark/app.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "Bookmark",
3+
"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.",
4+
"entry": "./index.html",
5+
"types": ["urn:solid:Bookmark"],
6+
"icon": "\ud83d\udd16",
7+
"author": "Melvin Carvalho",
8+
"status": "stable",
9+
"added": "2026-04-19"
10+
}

bookmark/bookmark-view-pane.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Bookmark view pane — renders a urn:solid:Bookmark as a card.
3+
*
4+
* Schema-aware (reads Bookmark fields: title, recalls, description, hasTopic,
5+
* created, attributedTo) but bespoke layout — favicon hero, big title link,
6+
* description prose, tag pills, saved-on footer.
7+
*
8+
* Same data, different lens. Drop in alongside ui-pane (Inline) and
9+
* schema-pane (Edit) — LOSOS shows them all 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 niceHost = (u) => { try { return new URL(u).host.replace(/^www\./, '') } catch { return u } }
17+
const niceDate = (s) => { try { return new Date(s).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) } catch { return s } }
18+
const faviconFor = (u) => { try { const url = new URL(u); return `https://www.google.com/s2/favicons?domain=${url.host}&sz=128` } catch { return null } }
19+
20+
export default {
21+
label: 'Bookmark',
22+
icon: '\ud83d\udd16',
23+
24+
canHandle(subject, store) {
25+
const node = store.get(subject.value)
26+
if (!node) return false
27+
const t = store.type(node)
28+
if (!t) return false
29+
return Array.isArray(t)
30+
? t.some(x => /Bookmark/i.test(x))
31+
: /Bookmark/i.test(t)
32+
},
33+
34+
render(subject, lionStore, container, rawData) {
35+
let data = rawData
36+
if (!data) {
37+
const dataEl = document.querySelector('script[type="application/ld+json"]')
38+
try { data = JSON.parse(dataEl.textContent) } catch { return }
39+
}
40+
41+
const title = data.title || data['@id'] || 'Untitled'
42+
const recalls = (typeof data.recalls === 'object' ? data.recalls['@id'] : data.recalls) || ''
43+
const description = data.description || ''
44+
const tags = Array.isArray(data.hasTopic) ? data.hasTopic : (data.hasTopic ? [data.hasTopic] : [])
45+
const created = data.created
46+
const attributedTo = data.attributedTo
47+
? (typeof data.attributedTo === 'string' ? data.attributedTo : data.attributedTo['@id'] || data.attributedTo.name)
48+
: null
49+
const favicon = recalls ? faviconFor(recalls) : null
50+
51+
render(container, html`
52+
<style>
53+
.bv-bg {
54+
min-height: 100vh;
55+
background:
56+
radial-gradient(circle at 80% 0%, rgba(34,197,94,0.06) 0%, transparent 40%),
57+
radial-gradient(circle at 20% 100%, rgba(99,102,241,0.06) 0%, transparent 40%),
58+
linear-gradient(180deg, #fafaf8 0%, #f0efeb 100%);
59+
padding: 64px 24px 96px;
60+
}
61+
.bv-card {
62+
max-width: 640px; margin: 0 auto;
63+
background: #fff;
64+
border: 1px solid #e5e3de;
65+
border-radius: 16px;
66+
padding: 40px 36px;
67+
box-shadow: 0 8px 32px rgba(0,0,0,0.04);
68+
}
69+
.bv-head {
70+
display: flex; gap: 18px; align-items: flex-start;
71+
margin-bottom: 24px;
72+
}
73+
.bv-favicon {
74+
width: 56px; height: 56px; border-radius: 12px;
75+
object-fit: contain; flex: 0 0 auto;
76+
background: #f5f4f0;
77+
border: 1px solid #e5e3de;
78+
padding: 8px;
79+
}
80+
.bv-favicon-fallback {
81+
width: 56px; height: 56px; border-radius: 12px;
82+
background: linear-gradient(135deg, #6366f1, #22c55e);
83+
color: #fff; font: 600 26px/56px Georgia, serif;
84+
text-align: center; flex: 0 0 auto;
85+
}
86+
.bv-titles { flex: 1; min-width: 0; }
87+
.bv-title {
88+
font: 500 26px/1.2 Georgia, serif;
89+
color: #1a1a1a; margin: 0 0 6px;
90+
letter-spacing: -0.4px;
91+
}
92+
.bv-title a { color: inherit; text-decoration: none; }
93+
.bv-title a:hover { color: #6366f1; }
94+
.bv-host {
95+
font: 400 13px/1 'Inter', -apple-system, sans-serif;
96+
color: #888;
97+
}
98+
.bv-host a { color: inherit; text-decoration: none; }
99+
.bv-host a:hover { color: #6366f1; }
100+
.bv-desc {
101+
font: 400 15px/1.6 Georgia, serif;
102+
color: #444; margin: 0 0 28px;
103+
}
104+
.bv-tags {
105+
display: flex; gap: 6px; flex-wrap: wrap;
106+
margin-bottom: 28px;
107+
}
108+
.bv-tag {
109+
padding: 5px 11px;
110+
background: #f5f4f0;
111+
border: 1px solid #e5e3de;
112+
border-radius: 999px;
113+
font: 500 12px/1 'Inter', -apple-system, sans-serif;
114+
color: #555;
115+
}
116+
.bv-meta {
117+
padding-top: 20px;
118+
border-top: 1px solid #e5e3de;
119+
font: 400 12px/1.5 'Inter', -apple-system, sans-serif;
120+
color: #999;
121+
display: flex; gap: 14px; flex-wrap: wrap;
122+
}
123+
.bv-meta a { color: #1a5276; text-decoration: none; }
124+
.bv-meta a:hover { color: #6366f1; }
125+
</style>
126+
127+
<div class="bv-bg">
128+
<div class="bv-card">
129+
<div class="bv-head">
130+
${favicon
131+
? html`<img class="bv-favicon" src="${favicon}" alt="" onerror="${function(e) { const f = document.createElement('div'); f.className = 'bv-favicon-fallback'; f.textContent = (title || '?').charAt(0).toUpperCase(); e.target.replaceWith(f) }}" />`
132+
: html`<div class="bv-favicon-fallback">${(title || '?').charAt(0).toUpperCase()}</div>`
133+
}
134+
<div class="bv-titles">
135+
<h1 class="bv-title">
136+
${recalls
137+
? html`<a href="${recalls}" target="_blank" rel="noopener">${title}</a>`
138+
: title}
139+
</h1>
140+
${recalls ? html`<div class="bv-host"><a href="${recalls}" target="_blank" rel="noopener">${niceHost(recalls)}</a></div>` : null}
141+
</div>
142+
</div>
143+
144+
${description ? html`<p class="bv-desc">${description}</p>` : null}
145+
146+
${tags.length > 0 ? html`
147+
<div class="bv-tags">
148+
${tags.map(t => html`<span class="bv-tag">#${t}</span>`)}
149+
</div>
150+
` : null}
151+
152+
${(created || attributedTo) ? html`
153+
<div class="bv-meta">
154+
${created ? html`<span>Saved ${niceDate(created)}</span>` : null}
155+
${attributedTo ? html`<span>by <a href="${attributedTo}" target="_blank" rel="noopener">${niceHost(attributedTo) || attributedTo}</a></span>` : null}
156+
</div>
157+
` : null}
158+
</div>
159+
</div>
160+
`)
161+
}
162+
}

bookmark/index.html

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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>Bookmark — 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: a bookmark of solidproject.org, attributed to TimBL.
17+
The natural self-referential demo. -->
18+
<script type="application/ld+json">
19+
{
20+
"@context": { "@vocab": "urn:solid:" },
21+
"@id": "#this",
22+
"@type": "Bookmark",
23+
"title": "Solid Project",
24+
"recalls": "https://solidproject.org/",
25+
"description": "Solid is a specification that lets people store their data securely in decentralized data stores called Pods. Pods are like secure personal web servers for data.",
26+
"hasTopic": ["solid", "decentralization", "rdf", "linked-data"],
27+
"created": "2026-04-19T12:00:00Z",
28+
"attributedTo": "https://www.w3.org/People/Berners-Lee/card#i"
29+
}
30+
</script>
31+
32+
<!-- Login: Nostr + Solid in one tag. -->
33+
<script src="https://unpkg.com/xlogin"></script>
34+
35+
<!-- Panes — bespoke Bookmark view first, then schema-driven. -->
36+
<script type="module" data-pane src="./bookmark-view-pane.js"></script>
37+
<script type="module" data-pane src="https://solid-ui.github.io/ui-pane.js"></script>
38+
<script type="module" data-pane src="https://losos.org/panes/schema-pane.js"></script>
39+
<script type="module" data-pane src="https://solid-panes.github.io/schema-view.js"></script>
40+
<script type="module" data-pane src="https://losos.org/panes/source-pane.js"></script>
41+
42+
<div id="losos"></div>
43+
44+
<!-- Boot: autoSchema patches $schema, then shell -->
45+
<script type="module">
46+
import { autoSchema } from 'https://solid-panes.github.io/auto-schema.js'
47+
const result = await autoSchema()
48+
console.log('[bookmark] autoSchema patched', result.patched, 'island(s),', result.skipped, 'skipped')
49+
await import('https://losos.org/losos/shell.js')
50+
</script>
51+
52+
</body>
53+
</html>

corpus.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{"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"}
12
{"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"}
23
{"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"}
34
{"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+
"bookmark": {
3+
"name": "Bookmark",
4+
"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.",
5+
"entry": "/bookmark/index.html",
6+
"types": [
7+
"urn:solid:Bookmark"
8+
],
9+
"icon": "🔖",
10+
"status": "stable",
11+
"manifest": "/bookmark/app.json"
12+
},
213
"feed": {
314
"name": "Feed",
415
"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
@@ -1,4 +1,7 @@
11
{
2+
"urn:solid:Bookmark": [
3+
"/bookmark/"
4+
],
25
"urn:solid:OrderedCollection": [
36
"/feed/"
47
],

0 commit comments

Comments
 (0)