Skip to content

Commit 066b0ac

Browse files
add: Calendar app
First app scaffolded via npm run new-app — proves the template works. Bespoke calendar-view-pane.js renders an Event as a save-the-date card (red month chip + big day + name/time/location/host). Sample data: a Solid CG meeting.
1 parent 0193835 commit 066b0ac

6 files changed

Lines changed: 259 additions & 0 deletions

File tree

calendar/app.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "Event",
3+
"description": "A calendar entry — schema-driven Event with date/time pickers.",
4+
"entry": "./index.html",
5+
"types": ["urn:solid:Event"],
6+
"icon": "📅",
7+
"author": "Melvin Carvalho",
8+
"status": "stable",
9+
"added": "2026-04-19"
10+
}

calendar/calendar-view-pane.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* Calendar view pane — bespoke renderer for urn:solid:Event.
3+
*
4+
* Reads Event.name, summary, startTime, endTime, location, attributedTo
5+
* and renders a save-the-date card: month/day chip on the left, title +
6+
* time range + location on the right. Empty fields gracefully omitted.
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 MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
17+
const niceHost = (u) => { try { return new URL(u).host.replace(/^www\./, '') } catch { return u } }
18+
19+
function partsOf(iso) {
20+
if (!iso) return null
21+
const d = new Date(iso)
22+
if (isNaN(d)) return null
23+
return {
24+
month: MONTHS[d.getMonth()],
25+
day: d.getDate(),
26+
weekday: d.toLocaleDateString(undefined, { weekday: 'long' }),
27+
time: d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }),
28+
}
29+
}
30+
31+
function timeRange(start, end) {
32+
if (!start) return null
33+
if (!end) return start.time
34+
const sameClock = start.time === end.time
35+
return sameClock ? start.time : `${start.time}${end.time}`
36+
}
37+
38+
export default {
39+
label: 'Calendar',
40+
icon: '\ud83d\udcc5',
41+
42+
canHandle(subject, store) {
43+
const node = store.get(subject.value)
44+
if (!node) return false
45+
const t = store.type(node)
46+
if (!t) return false
47+
return Array.isArray(t)
48+
? t.some(x => /Event/i.test(x))
49+
: /Event/i.test(t)
50+
},
51+
52+
render(subject, lionStore, container, rawData) {
53+
let data = rawData
54+
if (!data) {
55+
const dataEl = document.querySelector('script[type="application/ld+json"]')
56+
try { data = JSON.parse(dataEl.textContent) } catch { return }
57+
}
58+
59+
const name = data.name || data.title || 'Untitled'
60+
const summary = data.summary || data.description
61+
const start = partsOf(data.startTime)
62+
const end = partsOf(data.endTime)
63+
const location = (typeof data.location === 'object' ? data.location['@id'] : data.location) || ''
64+
const attributedTo = data.attributedTo
65+
? (typeof data.attributedTo === 'string' ? data.attributedTo : data.attributedTo['@id'] || data.attributedTo.name)
66+
: null
67+
68+
const range = start ? timeRange(start, end) : null
69+
const dateLine = start ? `${start.weekday}${range ? ' \u00b7 ' + range : ''}` : null
70+
const isUrl = (s) => /^https?:\/\//.test(s)
71+
72+
render(container, html`
73+
<style>
74+
.ev-bg {
75+
min-height: 100vh;
76+
background:
77+
radial-gradient(circle at 20% 0%, rgba(239,68,68,0.06) 0%, transparent 40%),
78+
radial-gradient(circle at 80% 100%, rgba(99,102,241,0.06) 0%, transparent 40%),
79+
linear-gradient(180deg, #fafaf8 0%, #f0efeb 100%);
80+
padding: 64px 24px 96px;
81+
}
82+
.ev-card {
83+
max-width: 640px; margin: 0 auto;
84+
background: #fff;
85+
border: 1px solid #e5e3de;
86+
border-radius: 16px;
87+
padding: 36px;
88+
display: flex; gap: 28px; align-items: flex-start;
89+
box-shadow: 0 8px 32px rgba(0,0,0,0.04);
90+
}
91+
.ev-date {
92+
flex: 0 0 auto;
93+
width: 96px;
94+
border: 1px solid #e5e3de;
95+
border-radius: 12px;
96+
overflow: hidden;
97+
text-align: center;
98+
}
99+
.ev-month {
100+
background: #ef4444; color: #fff;
101+
font: 600 12px/1 'Inter', -apple-system, sans-serif;
102+
padding: 8px; letter-spacing: 0.18em; text-transform: uppercase;
103+
}
104+
.ev-day {
105+
font: 500 44px/1 Georgia, serif;
106+
color: #1a1a1a; padding: 16px 0;
107+
background: #fafaf8;
108+
}
109+
.ev-no-date {
110+
flex: 0 0 auto;
111+
width: 96px; height: 96px;
112+
border-radius: 12px;
113+
background: #f5f4f0;
114+
color: #aaa;
115+
display: flex; align-items: center; justify-content: center;
116+
font: 500 24px/1 Georgia, serif;
117+
}
118+
.ev-meta { flex: 1; min-width: 0; }
119+
.ev-name {
120+
font: 500 26px/1.2 Georgia, serif;
121+
color: #1a1a1a; margin: 0 0 8px;
122+
letter-spacing: -0.4px;
123+
}
124+
.ev-when {
125+
font: 500 13px/1.4 'Inter', -apple-system, sans-serif;
126+
color: #666; margin-bottom: 18px;
127+
}
128+
.ev-summary {
129+
font: 400 15px/1.6 Georgia, serif;
130+
color: #444; margin: 0 0 20px;
131+
}
132+
.ev-row {
133+
display: flex; gap: 8px; align-items: center;
134+
padding-top: 14px; margin-top: 14px;
135+
border-top: 1px solid #e5e3de;
136+
font: 400 13px/1.4 'Inter', -apple-system, sans-serif;
137+
color: #666;
138+
}
139+
.ev-row + .ev-row { border-top: none; padding-top: 0; margin-top: 6px; }
140+
.ev-row .icon { opacity: 0.6; }
141+
.ev-row a { color: #1a5276; text-decoration: none; }
142+
.ev-row a:hover { color: #6366f1; }
143+
</style>
144+
145+
<div class="ev-bg">
146+
<div class="ev-card">
147+
${start
148+
? html`
149+
<div class="ev-date">
150+
<div class="ev-month">${start.month}</div>
151+
<div class="ev-day">${start.day}</div>
152+
</div>`
153+
: html`<div class="ev-no-date">?</div>`
154+
}
155+
156+
<div class="ev-meta">
157+
<h1 class="ev-name">${name}</h1>
158+
${dateLine ? html`<div class="ev-when">${dateLine}</div>` : null}
159+
${summary ? html`<p class="ev-summary">${summary}</p>` : null}
160+
161+
${location ? html`
162+
<div class="ev-row">
163+
<span class="icon">\ud83d\udccd</span>
164+
${isUrl(location)
165+
? html`<a href="${location}" target="_blank" rel="noopener">${niceHost(location)}</a>`
166+
: html`<span>${location}</span>`}
167+
</div>` : null}
168+
169+
${attributedTo ? html`
170+
<div class="ev-row">
171+
<span class="icon">\ud83d\udc64</span>
172+
${isUrl(attributedTo)
173+
? html`<a href="${attributedTo}" target="_blank" rel="noopener">${niceHost(attributedTo)}</a>`
174+
: html`<span>${attributedTo}</span>`}
175+
</div>` : null}
176+
</div>
177+
</div>
178+
</div>
179+
`)
180+
}
181+
}

calendar/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>Event — 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 real Solid CG meeting. Same shape works for any Event:
17+
name, summary, startTime, endTime, location, attributedTo. -->
18+
<script type="application/ld+json">
19+
{
20+
"@context": { "@vocab": "urn:solid:" },
21+
"@id": "#this",
22+
"@type": "Event",
23+
"name": "Solid Community Group call",
24+
"summary": "Bi-weekly W3C Solid CG sync — agenda, demos, and open issues. Open to anyone.",
25+
"startTime": "2026-04-22T15:00:00Z",
26+
"endTime": "2026-04-22T16:00:00Z",
27+
"location": "https://meet.jit.si/solid",
28+
"attributedTo": "https://www.w3.org/community/solid/"
29+
}
30+
</script>
31+
32+
<!-- Login: Nostr + Solid in one tag. -->
33+
<script src="https://unpkg.com/xlogin"></script>
34+
35+
<!-- Panes — bespoke Event view first, then schema-driven. -->
36+
<script type="module" data-pane src="./calendar-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('[calendar] 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,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":"Event","description":"A calendar entry — schema-driven Event with date/time pickers.","entry":"./index.html","types":["urn:solid:Event"],"icon":"📅","author":"Melvin Carvalho","status":"stable","added":"2026-04-19"}
23
{"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"}
34
{"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"}
45
{"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"}

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+
"calendar": {
14+
"name": "Event",
15+
"description": "A calendar entry — schema-driven Event with date/time pickers.",
16+
"entry": "/calendar/index.html",
17+
"types": [
18+
"urn:solid:Event"
19+
],
20+
"icon": "📅",
21+
"status": "stable",
22+
"manifest": "/calendar/app.json"
23+
},
1324
"contacts": {
1425
"name": "Contacts",
1526
"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.",

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:Event": [
6+
"/calendar/"
7+
],
58
"urn:solid:AddressBook": [
69
"/contacts/"
710
],

0 commit comments

Comments
 (0)