@acms-subir/core 0.3.49 → 0.3.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/docs/calendar.md +64 -0
- package/docs/theme-authoring.md +1 -1
- package/integration.mjs +4 -1
- package/package.json +2 -1
- package/section-library/upcoming-events/UpcomingEvents.astro +3 -1
- package/src/components/display/DisplayPage.astro +192 -0
- package/src/lib/calendar/api.ts +11 -3
- package/src/lib/calendar/cards.ts +3 -3
- package/src/lib/calendar/data.ts +28 -11
- package/src/lib/display/board.ts +216 -0
- package/src/lib/display/http.ts +35 -0
- package/src/lib/display/render.ts +426 -0
- package/src/lib/display/settings.ts +160 -0
- package/src/lib/display/words.ts +52 -0
- package/src/lib/mobile/calendar.ts +3 -1
- package/src/lib/section-items.ts +24 -0
- package/src/lib/sections.ts +4 -15
- package/src/pages/display/calendar.astro +30 -0
- package/src/pages/display/sw.js.ts +71 -0
- package/src/styles/display.css +195 -0
- package/src/theme-sdk/calendar.ts +3 -1
- package/src/theme-sdk/index.ts +1 -1
|
@@ -11,6 +11,7 @@ import { sha256Hex } from '../api-keys';
|
|
|
11
11
|
import type { PublicContext } from './route';
|
|
12
12
|
import { addDaysYmd, describeLink, linkLengths, localMidnight, slotsForDay, ymd } from '../booking/availability';
|
|
13
13
|
import { siteOrigin } from '../seo';
|
|
14
|
+
import { siteLang } from '../dates';
|
|
14
15
|
import { safeUrl } from './content';
|
|
15
16
|
import { pageQuery, isoDay, isoDayOrInstant, queryParams } from './http';
|
|
16
17
|
import { localizeDoc } from '../translations';
|
|
@@ -116,7 +117,8 @@ export function occurrenceDto(o: Occurrence, resources: Map<string, Resource>) {
|
|
|
116
117
|
}
|
|
117
118
|
// e is already localized; publicCalendars limits which calendars are named
|
|
118
119
|
export function publicEventDto(e: CalEvent, origin: string, publicCalendars: Map<string, Calendar>, settings: Settings, locale: string | null, resources?: Map<string, Resource>) {
|
|
119
|
-
|
|
120
|
+
// the repeat line in the request's language, else the site's main one
|
|
121
|
+
const event = eventToJson(e, origin, publicCalendars, settings.timezone, { resources, locale: locale || siteLang(settings) });
|
|
120
122
|
event.url = localizedUrl(event.url, origin, locale);
|
|
121
123
|
// Only configured public event fields; no undeclared imported metadata, no private fields.
|
|
122
124
|
event.fields = publicEventFields(e.fields);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The item shapes section data carries (lib/sections.ts fills them): what a
|
|
2
|
+
// theme's section components and the apps' native sections receive.
|
|
3
|
+
// No imports, so other packages can typecheck against them
|
|
4
|
+
// (packages/native keeps its own copy; test/section-items.test.ts there
|
|
5
|
+
// keeps the two in step).
|
|
6
|
+
|
|
7
|
+
export interface LinkValue { label: string; href: string; external: boolean }
|
|
8
|
+
/** a room this date holds, named only when set to show on public events; color null = none */
|
|
9
|
+
export interface EventItemRoom { slug: string; name: string; color: string | null }
|
|
10
|
+
export interface EventItem {
|
|
11
|
+
title: string; url: string; summary: string; image: string; imageAlt: string; location: string;
|
|
12
|
+
start: string; end: string; allDay: boolean; cancelled: boolean;
|
|
13
|
+
/** this date was moved here: from "Thu, Oct 1 · 6:00 PM" (where it was), with staff's public note; null when not moved */
|
|
14
|
+
moved: { from: string; note: string } | null;
|
|
15
|
+
/** the public rooms this date holds (a moved date's own, when it moved into others) */
|
|
16
|
+
rooms: EventItemRoom[];
|
|
17
|
+
/** "Sat, Oct 3 · 11:00 AM" */ when: string; /** "Saturday, October 3, 2026" */ date: string;
|
|
18
|
+
/** "11:00 AM – 1:00 PM EDT" */ time: string; month: string; day: string; weekday: string;
|
|
19
|
+
cta: { label: string; href: string } | null;
|
|
20
|
+
}
|
|
21
|
+
export interface PostAuthor { name: string; url: string; avatar: string }
|
|
22
|
+
// excerpt: the post's own, else the first ~160 characters of its text
|
|
23
|
+
export interface PostItem { title: string; url: string; excerpt: string; image: string; imageAlt: string; date: string; publishedAt: number | null; tags: string[]; authors: PostAuthor[] }
|
|
24
|
+
export interface BookingItem { title: string; url: string; description: string; resource: string; minutes: number }
|
package/src/lib/sections.ts
CHANGED
|
@@ -28,20 +28,8 @@ type Instance = z.infer<typeof SectionInstanceSchema>;
|
|
|
28
28
|
type Definition = z.infer<typeof SectionDefinition>;
|
|
29
29
|
type FieldDef = Definition['fields'][string];
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
export
|
|
33
|
-
title: string; url: string; summary: string; image: string; imageAlt: string; location: string;
|
|
34
|
-
start: string; end: string; allDay: boolean; cancelled: boolean;
|
|
35
|
-
/** this date was moved here: from "Thu, Oct 1 · 6:00 PM" (where it was), with staff's public note; null when not moved */
|
|
36
|
-
moved: { from: string; note: string } | null;
|
|
37
|
-
/** "Sat, Oct 3 · 11:00 AM" */ when: string; /** "Saturday, October 3, 2026" */ date: string;
|
|
38
|
-
/** "11:00 AM – 1:00 PM EDT" */ time: string; month: string; day: string; weekday: string;
|
|
39
|
-
cta: { label: string; href: string } | null;
|
|
40
|
-
}
|
|
41
|
-
export interface PostAuthor { name: string; url: string; avatar: string }
|
|
42
|
-
// excerpt: the post's own, else the first ~160 characters of its text
|
|
43
|
-
export interface PostItem { title: string; url: string; excerpt: string; image: string; imageAlt: string; date: string; publishedAt: number | null; tags: string[]; authors: PostAuthor[] }
|
|
44
|
-
export interface BookingItem { title: string; url: string; description: string; resource: string; minutes: number }
|
|
31
|
+
import type { BookingItem, EventItem, LinkValue, PostAuthor, PostItem } from './section-items';
|
|
32
|
+
export type { BookingItem, EventItem, EventItemRoom, LinkValue, PostAuthor, PostItem } from './section-items';
|
|
45
33
|
|
|
46
34
|
/** apply a theme migration when the stored instance is older than the definition */
|
|
47
35
|
export function migrateSection(theme: Pick<Theme, 'sections' | 'migrations'>, s: Instance): Instance {
|
|
@@ -148,7 +136,8 @@ async function resolveSource(field: Extract<FieldDef, { type: 'source' }>, value
|
|
|
148
136
|
return {
|
|
149
137
|
title: e.title || 'Untitled event', url: lp(eventUrl(e.slug)), summary: e.excerpt, image: safeUrl(e.cover), imageAlt: e.coverAlt,
|
|
150
138
|
location: locationLine(o.moved?.location ?? e.location), start: new Date(o.start).toISOString(), end: new Date(o.end).toISOString(),
|
|
151
|
-
allDay: o.allDay, cancelled: o.cancelled, moved: was && o.moved ? { from: o.allDay ? was.date : was.range, note: o.moved.note } : null,
|
|
139
|
+
allDay: o.allDay, cancelled: o.cancelled, moved: was && o.moved ? { from: o.allDay ? was.date : was.range, note: o.moved.note } : null,
|
|
140
|
+
rooms: (o.rooms ?? []).map((r) => ({ slug: r.slug, name: r.name, color: r.color || null })), when: f.range, date: f.date, time: f.time, month: f.month, day: f.day, weekday: f.weekday,
|
|
152
141
|
cta: e.cta.label && ctaHref && !o.cancelled ? { label: e.cta.label, href: ctaHref } : null,
|
|
153
142
|
};
|
|
154
143
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DisplayPage from '../../components/display/DisplayPage.astro';
|
|
3
|
+
import { getRepo } from '../../lib/repo';
|
|
4
|
+
import { CALENDAR } from '../../lib/calendar/config';
|
|
5
|
+
import { I18N, localePath, requestLocale } from '../../lib/i18n';
|
|
6
|
+
import { siteOrigin } from '../../lib/seo';
|
|
7
|
+
import { siteLang } from '../../lib/dates';
|
|
8
|
+
import { readDisplaySettings } from '../../lib/display/settings';
|
|
9
|
+
import { loadDisplayBoard } from '../../lib/display/board';
|
|
10
|
+
import { displayHeaders } from '../../lib/display/http';
|
|
11
|
+
|
|
12
|
+
// A TV display from a quick link: everything about it is in the address
|
|
13
|
+
// (/display/calendar?view=week&hours=8-22…, /es/display/calendar for
|
|
14
|
+
// Spanish). Nothing is saved, and whatever the parameters say it shows only
|
|
15
|
+
// what calendar.ics shows. Saved screens with private details live at
|
|
16
|
+
// /display/s/<key> (Admin → Calendar → Screens).
|
|
17
|
+
if (!CALENDAR.enabled) return new Response(null, { status: 404 });
|
|
18
|
+
const repo = await getRepo(Astro.locals);
|
|
19
|
+
const site = await repo.getSettings();
|
|
20
|
+
const settings = readDisplaySettings(Astro.url.searchParams);
|
|
21
|
+
const lang = (I18N.enabled ? requestLocale(Astro.url, Astro.locals) : '') || siteLang(site);
|
|
22
|
+
const board = await loadDisplayBoard(repo, site, {
|
|
23
|
+
settings, privateDetails: false, lang,
|
|
24
|
+
phoneLink: siteOrigin(site, Astro.url) + localePath(CALENDAR.base, lang),
|
|
25
|
+
});
|
|
26
|
+
displayHeaders(Astro.response.headers, { shared: true });
|
|
27
|
+
const prefix = I18N.enabled && lang !== I18N.defaultLocale ? `/${lang}` : '';
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<DisplayPage site={site} board={board} settings={settings} title={settings.title || site.siteTitle} lang={lang} live sw={`${prefix}/display/sw.js`} />
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { CALENDAR } from '../../lib/calendar/config';
|
|
3
|
+
|
|
4
|
+
// The TV display's service worker (registered by DisplayPage.astro, scope
|
|
5
|
+
// /display/, or /es/display/ through the language rewrite). AbleSign reloads
|
|
6
|
+
// a website every time its playlist plays it and keeps no offline copy, so
|
|
7
|
+
// during an outage a reload would show an error page. Network first: each
|
|
8
|
+
// display page that loads (or refreshes its board) is kept, and served when a
|
|
9
|
+
// page load can't reach the site or gets a server error. A page that answers
|
|
10
|
+
// 404 (a rotated or removed screen) loses its copy, so an old link stops
|
|
11
|
+
// showing events. At most 20 pages are kept, none older than a week is served.
|
|
12
|
+
const SCRIPT = `
|
|
13
|
+
var CACHE = 'acms-display-v1';
|
|
14
|
+
var KEEP = 20, MAX_AGE = 7 * 86400000;
|
|
15
|
+
var scope = new URL(self.registration.scope).pathname;
|
|
16
|
+
self.addEventListener('install', function () { self.skipWaiting(); });
|
|
17
|
+
self.addEventListener('activate', function (e) { e.waitUntil(self.clients.claim()); });
|
|
18
|
+
function keep(key, res) {
|
|
19
|
+
return caches.open(CACHE).then(function (c) {
|
|
20
|
+
return c.put(key, res).then(function () { return c.keys(); }).then(function (keys) {
|
|
21
|
+
// the oldest first: at most KEEP pages (a player adding ?t= to each load can't fill the disk)
|
|
22
|
+
return Promise.all(keys.slice(0, Math.max(0, keys.length - KEEP)).map(function (k) { return c.delete(k); }));
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function kept(key) {
|
|
27
|
+
return caches.open(CACHE).then(function (c) { return c.match(key); }).then(function (hit) {
|
|
28
|
+
var at = hit && Date.parse(hit.headers.get('date') || '');
|
|
29
|
+
// a week-old board is worse than an error: it would show the wrong week
|
|
30
|
+
return hit && (!at || Date.now() - at < MAX_AGE) ? hit : null;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
self.addEventListener('fetch', function (e) {
|
|
34
|
+
var req = e.request;
|
|
35
|
+
if (req.method !== 'GET') return;
|
|
36
|
+
var url = new URL(req.url);
|
|
37
|
+
if (url.origin !== location.origin || url.pathname.indexOf(scope) !== 0 || /\\/sw\\.js$/.test(url.pathname)) return;
|
|
38
|
+
var key = url.origin + url.pathname + url.search;
|
|
39
|
+
// only a page load falls back: the page's own refresh must fail, so it
|
|
40
|
+
// keeps its board and says when it was last updated
|
|
41
|
+
var page = req.mode === 'navigate';
|
|
42
|
+
e.respondWith(fetch(req).then(function (res) {
|
|
43
|
+
if (res.ok && res.type === 'basic') {
|
|
44
|
+
e.waitUntil(keep(key, res.clone()));
|
|
45
|
+
return res;
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 404 || res.status === 410) {
|
|
48
|
+
e.waitUntil(caches.open(CACHE).then(function (c) { return c.delete(key); }));
|
|
49
|
+
return res;
|
|
50
|
+
}
|
|
51
|
+
// a deploy, an outage or a rate limit: the last board beats an error page
|
|
52
|
+
if (page && (res.status >= 500 || res.status === 429)) return kept(key).then(function (hit) { return hit || res; });
|
|
53
|
+
return res;
|
|
54
|
+
}, function (err) {
|
|
55
|
+
if (!page) throw err;
|
|
56
|
+
return kept(key).then(function (hit) { if (hit) return hit; throw err; });
|
|
57
|
+
}));
|
|
58
|
+
});
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
export const GET: APIRoute = () => {
|
|
62
|
+
if (!CALENDAR.enabled) return new Response(null, { status: 404 });
|
|
63
|
+
return new Response(SCRIPT, {
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'text/javascript; charset=utf-8',
|
|
66
|
+
// browsers check for a new worker on every registration; never keep an old one
|
|
67
|
+
'Cache-Control': 'no-cache',
|
|
68
|
+
'X-Content-Type-Options': 'nosniff',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* The TV display (/display/…; lib/display/render.ts). Inlined into the page
|
|
2
|
+
before the theme's CSS, so a theme restyles it with equal-specificity rules
|
|
3
|
+
or, better, by setting these variables (core never sets them itself):
|
|
4
|
+
--display-bg, --display-fg, --display-muted, --display-line,
|
|
5
|
+
--display-panel, --display-today, --display-now, --display-event,
|
|
6
|
+
--display-font, --display-heading-font, --display-radius
|
|
7
|
+
e.g. :root { --display-bg: #101820 } or
|
|
8
|
+
.acms-display[data-theme='light'] { --display-bg: #fffaf2 }
|
|
9
|
+
Every length is a multiple of --dsp-u (viewport height × text size), matching
|
|
10
|
+
METRICS in render.ts. Flat blocks only: no accent stripes, no gradients. */
|
|
11
|
+
|
|
12
|
+
html, body { margin: 0; height: 100%; overflow: hidden; }
|
|
13
|
+
body { background: #000; }
|
|
14
|
+
|
|
15
|
+
.acms-display {
|
|
16
|
+
--dsp-u: calc(min(1vh, 1.7778vw) * var(--dsp-scale, 1));
|
|
17
|
+
--d-bg: #0c0e13; --d-fg: #f3f4f7; --d-muted: #9ca3af; --d-line: rgba(255, 255, 255, 0.09);
|
|
18
|
+
--d-panel: rgba(255, 255, 255, 0.06); --d-today: rgba(255, 255, 255, 0.055); --d-now: #ff5a4f; --d-event: #4f6bed;
|
|
19
|
+
--dsp-bg: var(--display-bg, var(--d-bg));
|
|
20
|
+
--dsp-fg: var(--display-fg, var(--d-fg));
|
|
21
|
+
--dsp-muted: var(--display-muted, var(--d-muted));
|
|
22
|
+
--dsp-line: var(--display-line, var(--d-line));
|
|
23
|
+
--dsp-r: var(--display-radius, calc(0.7 * var(--dsp-u)));
|
|
24
|
+
position: fixed;
|
|
25
|
+
inset: 0;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
padding: calc(2.5 * var(--dsp-u)) calc(3 * var(--dsp-u));
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: calc(2 * var(--dsp-u));
|
|
31
|
+
background: var(--dsp-bg);
|
|
32
|
+
color: var(--dsp-fg);
|
|
33
|
+
font-family: var(--display-font, var(--font-body, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif));
|
|
34
|
+
font-size: calc(2.4 * var(--dsp-u));
|
|
35
|
+
line-height: 1.25;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
cursor: none;
|
|
38
|
+
-webkit-font-smoothing: antialiased;
|
|
39
|
+
user-select: none;
|
|
40
|
+
}
|
|
41
|
+
.acms-display[data-theme='light'] {
|
|
42
|
+
--d-bg: #f6f6f3; --d-fg: #15171c; --d-muted: #5b6270; --d-line: rgba(0, 0, 0, 0.1);
|
|
43
|
+
--d-panel: rgba(0, 0, 0, 0.045); --d-today: rgba(0, 0, 0, 0.045); --d-now: #d9342b;
|
|
44
|
+
}
|
|
45
|
+
.acms-display *, .acms-display *::before { box-sizing: border-box; }
|
|
46
|
+
|
|
47
|
+
/* header: heading, offline note, QR, clock (8u tall) */
|
|
48
|
+
.display-head { flex: 0 0 calc(8 * var(--dsp-u)); display: flex; align-items: center; gap: calc(3 * var(--dsp-u)); min-height: 0; }
|
|
49
|
+
.display-heading { flex: 1 1 auto; min-width: 0; }
|
|
50
|
+
.display-title {
|
|
51
|
+
margin: 0; font-size: calc(4.4 * var(--dsp-u)); line-height: 1.1; font-weight: 700; letter-spacing: -0.01em;
|
|
52
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
53
|
+
font-family: var(--display-heading-font, var(--font-heading, inherit));
|
|
54
|
+
}
|
|
55
|
+
.display-sub { margin: calc(0.3 * var(--dsp-u)) 0 0; font-size: calc(2.4 * var(--dsp-u)); color: var(--dsp-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
56
|
+
.display-clock { margin: 0; font-size: calc(4.4 * var(--dsp-u)); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
57
|
+
.display-status {
|
|
58
|
+
margin: 0; padding: calc(0.5 * var(--dsp-u)) calc(1.4 * var(--dsp-u)); border-radius: 999px; white-space: nowrap;
|
|
59
|
+
font-size: calc(2 * var(--dsp-u)); color: var(--dsp-muted); background: var(--display-panel, var(--d-panel));
|
|
60
|
+
}
|
|
61
|
+
.display-status[hidden], .display-now[hidden], .display-page[hidden] { display: none; }
|
|
62
|
+
.display-qr { display: flex; align-items: center; gap: calc(1.2 * var(--dsp-u)); height: 100%; }
|
|
63
|
+
.display-qr svg { height: 100%; width: auto; display: block; background: #fff; padding: calc(0.5 * var(--dsp-u)); border-radius: calc(0.5 * var(--dsp-u)); }
|
|
64
|
+
.display-qr-note { font-size: calc(1.7 * var(--dsp-u)); color: var(--dsp-muted); max-width: calc(13 * var(--dsp-u)); line-height: 1.2; }
|
|
65
|
+
|
|
66
|
+
.display-board { flex: 1 1 0; min-height: 0; position: relative; }
|
|
67
|
+
.display-legend { flex: 0 0 calc(3 * var(--dsp-u)); display: flex; align-items: center; gap: calc(2.6 * var(--dsp-u)); font-size: calc(2 * var(--dsp-u)); color: var(--dsp-muted); overflow: hidden; white-space: nowrap; }
|
|
68
|
+
.display-key { display: inline-flex; align-items: center; gap: calc(0.8 * var(--dsp-u)); }
|
|
69
|
+
|
|
70
|
+
/* shared by every event shape */
|
|
71
|
+
.display-event { min-width: 0; }
|
|
72
|
+
.display-event.is-past { opacity: 0.45; }
|
|
73
|
+
.display-event.is-cancelled { opacity: 0.6; }
|
|
74
|
+
.display-event.is-cancelled .display-event-title { text-decoration: line-through; }
|
|
75
|
+
.display-event-title { display: block; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
76
|
+
.display-event-detail, .display-event-meta { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
77
|
+
.display-event-meta { opacity: 0.85; }
|
|
78
|
+
.display-flag { font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.88em; }
|
|
79
|
+
.display-was { opacity: 0.9; }
|
|
80
|
+
.display-dot { display: inline-block; flex: none; width: calc(1.4 * var(--dsp-u)); height: calc(1.4 * var(--dsp-u)); border-radius: calc(0.35 * var(--dsp-u)); background: var(--ev, var(--display-event, var(--d-event))); }
|
|
81
|
+
|
|
82
|
+
/* hour grids: week, day, rooms */
|
|
83
|
+
.display-grid { --axis: calc(7.5 * var(--dsp-u)); height: 100%; display: flex; flex-direction: column; }
|
|
84
|
+
.display-heads, .display-top, .display-cols { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); }
|
|
85
|
+
.display-heads { flex: 0 0 calc(4.5 * var(--dsp-u)); margin-left: var(--axis); gap: calc(0.4 * var(--dsp-u)); }
|
|
86
|
+
.display-dayhead {
|
|
87
|
+
display: flex; align-items: baseline; justify-content: center; gap: 0.35em; min-width: 0; padding: 0 0.4em;
|
|
88
|
+
border-radius: var(--dsp-r); font-size: calc(2.2 * var(--dsp-u)); color: var(--dsp-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
89
|
+
align-self: stretch; align-items: center;
|
|
90
|
+
}
|
|
91
|
+
.display-dayname { overflow: hidden; text-overflow: ellipsis; }
|
|
92
|
+
.display-daynum { font-size: calc(2.8 * var(--dsp-u)); font-weight: 700; color: var(--dsp-fg); }
|
|
93
|
+
.display-dayhead.is-today { color: var(--dsp-fg); font-weight: 700; background: var(--display-today, var(--d-today)); }
|
|
94
|
+
.display-dayhead.is-past { opacity: 0.6; }
|
|
95
|
+
.display-roomhead { color: var(--dsp-fg); font-weight: 700; background: var(--display-panel, var(--d-panel)); }
|
|
96
|
+
.display-top {
|
|
97
|
+
flex: none; margin: calc(0.6 * var(--dsp-u)) 0 0 var(--axis);
|
|
98
|
+
grid-template-rows: repeat(var(--lines), calc(3.3 * var(--dsp-u))); gap: 0 calc(0.4 * var(--dsp-u));
|
|
99
|
+
}
|
|
100
|
+
.display-hours { flex: 1 1 0; min-height: 0; display: flex; position: relative; margin-top: calc(0.8 * var(--dsp-u)); }
|
|
101
|
+
.display-axis { flex: 0 0 var(--axis); position: relative; }
|
|
102
|
+
.display-hour { position: absolute; right: calc(1 * var(--dsp-u)); transform: translateY(-50%); font-size: calc(1.8 * var(--dsp-u)); color: var(--dsp-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
103
|
+
.display-cols { flex: 1 1 auto; position: relative; gap: calc(0.4 * var(--dsp-u)); }
|
|
104
|
+
.display-lines { position: absolute; inset: 0; pointer-events: none; }
|
|
105
|
+
.display-line { position: absolute; left: 0; right: 0; border-top: 1px solid var(--dsp-line); }
|
|
106
|
+
.display-col { position: relative; min-width: 0; border-radius: var(--dsp-r); }
|
|
107
|
+
.display-col.is-today { background: var(--display-today, var(--d-today)); }
|
|
108
|
+
.display-grid.is-rooms .display-col.is-today { background: none; }
|
|
109
|
+
/* timed blocks: render.ts gives each only the lines it can show whole
|
|
110
|
+
(is-mid: one title line and the meta; is-short: the title alone) */
|
|
111
|
+
.display-block {
|
|
112
|
+
position: absolute; display: flex; flex-direction: column;
|
|
113
|
+
padding: calc(0.35 * var(--dsp-u)) calc(0.7 * var(--dsp-u)); overflow: hidden;
|
|
114
|
+
border-radius: var(--dsp-r); border: calc(0.15 * var(--dsp-u)) solid var(--dsp-bg);
|
|
115
|
+
background: var(--ev, var(--display-event, var(--d-event))); color: var(--ev-ink, #fff);
|
|
116
|
+
font-size: calc(1.7 * var(--dsp-u)); line-height: 1.1;
|
|
117
|
+
}
|
|
118
|
+
.display-block .display-event-title { font-size: calc(2.15 * var(--dsp-u)); line-height: 1.05; white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
|
119
|
+
.display-block.is-mid .display-event-title, .display-block.is-short .display-event-title { display: block; white-space: nowrap; }
|
|
120
|
+
.display-block[data-cut='start'], .display-block[data-cut='both'] { border-top-left-radius: 0; border-top-right-radius: 0; }
|
|
121
|
+
.display-block[data-cut='end'], .display-block[data-cut='both'] { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
122
|
+
.display-cut { font-size: 0.75em; margin: 0 0.3em; opacity: 0.9; }
|
|
123
|
+
.display-bar, .display-chip {
|
|
124
|
+
margin: calc(0.25 * var(--dsp-u)) 0; padding: 0 calc(0.6 * var(--dsp-u)); border-radius: calc(0.5 * var(--dsp-u));
|
|
125
|
+
font-size: calc(1.9 * var(--dsp-u)); line-height: calc(2.8 * var(--dsp-u)); z-index: 1; overflow: hidden;
|
|
126
|
+
}
|
|
127
|
+
.display-bar { background: var(--ev, var(--display-event, var(--d-event))); color: var(--ev-ink, #fff); }
|
|
128
|
+
.display-bar .display-event-title, .display-chip .display-event-title { font-weight: 600; }
|
|
129
|
+
.display-bar-time, .display-chip-time { font-weight: 500; opacity: 0.85; font-variant-numeric: tabular-nums; }
|
|
130
|
+
.display-chip { display: flex; align-items: center; gap: calc(0.6 * var(--dsp-u)); }
|
|
131
|
+
.display-chip::before { content: ''; flex: none; width: calc(1.1 * var(--dsp-u)); height: calc(1.1 * var(--dsp-u)); border-radius: calc(0.3 * var(--dsp-u)); background: var(--ev, var(--display-event, var(--d-event))); }
|
|
132
|
+
.display-more { font-size: calc(1.8 * var(--dsp-u)); color: var(--dsp-muted); padding: 0 calc(0.6 * var(--dsp-u)); align-self: center; z-index: 1; white-space: nowrap; }
|
|
133
|
+
/* the now line keeps an outline in the page colour, so it shows over a block of its own colour */
|
|
134
|
+
.display-now {
|
|
135
|
+
position: absolute; left: 0; right: 0; height: calc(0.35 * var(--dsp-u)); margin-top: calc(-0.175 * var(--dsp-u)); z-index: 3; pointer-events: none;
|
|
136
|
+
background: var(--display-now, var(--d-now)); box-shadow: 0 0 0 calc(0.2 * var(--dsp-u)) var(--dsp-bg);
|
|
137
|
+
}
|
|
138
|
+
.display-now::before {
|
|
139
|
+
content: ''; position: absolute; left: calc(-0.7 * var(--dsp-u)); top: calc(-0.5 * var(--dsp-u)); width: calc(1.35 * var(--dsp-u)); height: calc(1.35 * var(--dsp-u));
|
|
140
|
+
border-radius: 50%; background: var(--display-now, var(--d-now)); box-shadow: 0 0 0 calc(0.2 * var(--dsp-u)) var(--dsp-bg);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* month: rows fill the height; each row has a date line and --lines lines */
|
|
144
|
+
.display-month { height: 100%; display: grid; grid-template-rows: calc(3.5 * var(--dsp-u)) repeat(var(--rows), minmax(0, 1fr)); }
|
|
145
|
+
.display-dows, .display-weekrow { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); }
|
|
146
|
+
.display-dows { align-items: center; font-size: calc(1.9 * var(--dsp-u)); color: var(--dsp-muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
147
|
+
.display-dow { padding: 0 calc(0.6 * var(--dsp-u)); }
|
|
148
|
+
.display-weekrow {
|
|
149
|
+
grid-template-rows: calc(3.4 * var(--dsp-u)) repeat(var(--lines), calc(3.3 * var(--dsp-u))) minmax(0, 1fr);
|
|
150
|
+
border-top: 1px solid var(--dsp-line); overflow: hidden; min-height: 0;
|
|
151
|
+
}
|
|
152
|
+
.display-cell { grid-row: 1 / -1; padding: calc(0.3 * var(--dsp-u)) calc(0.6 * var(--dsp-u)); border-left: 1px solid var(--dsp-line); min-width: 0; }
|
|
153
|
+
.display-cell:first-child { border-left: 0; }
|
|
154
|
+
.display-cell.is-today { background: var(--display-today, var(--d-today)); }
|
|
155
|
+
.display-cell.is-other .display-date { opacity: 0.4; }
|
|
156
|
+
.display-cell.is-past .display-date { opacity: 0.6; }
|
|
157
|
+
.display-date { font-size: calc(2.2 * var(--dsp-u)); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
158
|
+
.display-cell.is-today .display-date { color: var(--display-now, var(--d-now)); font-weight: 800; }
|
|
159
|
+
.display-month .display-bar, .display-month .display-chip { margin-left: calc(0.3 * var(--dsp-u)); margin-right: calc(0.3 * var(--dsp-u)); }
|
|
160
|
+
|
|
161
|
+
/* list: columns, then pages (the script turns them) */
|
|
162
|
+
.display-list { height: 100%; overflow: hidden; }
|
|
163
|
+
.display-list-cols { height: 100%; column-count: var(--dsp-list-cols, 3); column-gap: calc(3.5 * var(--dsp-u)); column-fill: auto; }
|
|
164
|
+
.display-day { margin: 0 0 calc(2.2 * var(--dsp-u)); }
|
|
165
|
+
.display-day-title { margin: 0 0 calc(0.6 * var(--dsp-u)); font-size: calc(2.3 * var(--dsp-u)); font-weight: 700; color: var(--dsp-muted); break-after: avoid; }
|
|
166
|
+
.display-day.is-today .display-day-title { color: var(--dsp-fg); }
|
|
167
|
+
.display-row {
|
|
168
|
+
display: grid; grid-template-columns: calc(1.4 * var(--dsp-u)) calc(9.5 * var(--dsp-u)) minmax(0, 1fr); gap: calc(1.2 * var(--dsp-u));
|
|
169
|
+
align-items: baseline; padding: calc(0.75 * var(--dsp-u)) 0; border-top: 1px solid var(--dsp-line); break-inside: avoid;
|
|
170
|
+
}
|
|
171
|
+
.display-row .display-dot { align-self: center; }
|
|
172
|
+
.display-row-time { color: var(--dsp-muted); font-size: calc(2.1 * var(--dsp-u)); white-space: nowrap; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; }
|
|
173
|
+
.display-row-body { min-width: 0; }
|
|
174
|
+
.display-row .display-event-title { font-size: calc(2.4 * var(--dsp-u)); white-space: normal; }
|
|
175
|
+
.display-row .display-event-meta, .display-row .display-event-detail { font-size: calc(1.9 * var(--dsp-u)); color: var(--dsp-muted); opacity: 1; }
|
|
176
|
+
.display-page { position: absolute; right: 0; bottom: calc(-0.5 * var(--dsp-u)); font-size: calc(1.8 * var(--dsp-u)); color: var(--dsp-muted); }
|
|
177
|
+
|
|
178
|
+
/* now and next */
|
|
179
|
+
.display-next { height: 100%; display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: calc(3 * var(--dsp-u)); }
|
|
180
|
+
.display-slot { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: calc(1.4 * var(--dsp-u)); overflow: hidden; }
|
|
181
|
+
.display-slot-title { margin: 0; font-size: calc(2.6 * var(--dsp-u)); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--dsp-muted); }
|
|
182
|
+
.display-card {
|
|
183
|
+
display: flex; flex-direction: column; gap: calc(0.6 * var(--dsp-u)); padding: calc(2 * var(--dsp-u)) calc(2.4 * var(--dsp-u));
|
|
184
|
+
border-radius: calc(1.2 * var(--dsp-u)); background: var(--ev, var(--display-event, var(--d-event))); color: var(--ev-ink, #fff);
|
|
185
|
+
}
|
|
186
|
+
.display-card .display-event-title { font-size: calc(3.4 * var(--dsp-u)); line-height: 1.15; white-space: normal; }
|
|
187
|
+
.display-card.is-big .display-event-title { font-size: calc(6 * var(--dsp-u)); line-height: 1.08; }
|
|
188
|
+
.display-card .display-event-meta, .display-card .display-event-detail { font-size: calc(2.4 * var(--dsp-u)); white-space: normal; }
|
|
189
|
+
.display-card.is-big .display-event-meta { font-size: calc(3 * var(--dsp-u)); }
|
|
190
|
+
.display-empty { margin: 0; color: var(--dsp-muted); font-size: calc(3 * var(--dsp-u)); }
|
|
191
|
+
.display-empty.is-board { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
192
|
+
|
|
193
|
+
/* a screen that isn't there (wrong or rotated key) */
|
|
194
|
+
.display-missing { justify-content: center; align-items: center; text-align: center; }
|
|
195
|
+
.display-missing p { margin: 0; max-width: 60ch; color: var(--dsp-muted); }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CALENDAR } from '../lib/calendar/config';
|
|
2
2
|
import type { Occurrence } from '../lib/calendar/events';
|
|
3
|
+
import type { ViewEvent } from '../lib/calendar/data';
|
|
3
4
|
|
|
4
5
|
export { eventUrl } from '../lib/calendar/config';
|
|
5
6
|
|
|
@@ -31,8 +32,9 @@ export interface UpcomingEventsOptions {
|
|
|
31
32
|
* fields except private custom fields. `cancelled` is true when the whole
|
|
32
33
|
* event or just this date is cancelled. `moved` is set when this date of a
|
|
33
34
|
* series was moved here: `from` (its old start and end) and staff's `note`.
|
|
35
|
+
* `rooms` are the public rooms this date holds ({ id, slug, name, color }).
|
|
34
36
|
*/
|
|
35
|
-
export type UpcomingEvent = Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days' | 'cancelled'> & Pick<Partial<Occurrence>, 'moved'>;
|
|
37
|
+
export type UpcomingEvent = Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days' | 'cancelled'> & Pick<Partial<Occurrence>, 'moved'> & Pick<ViewEvent, 'rooms'>;
|
|
36
38
|
|
|
37
39
|
/** Server-side helper for Astro templates and endpoints. No repository access needed. */
|
|
38
40
|
export async function upcomingEvents(
|
package/src/theme-sdk/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface EventProps { settings: Settings; event: CalEvent; html: string;
|
|
|
19
19
|
* `acms theme types <slug>` writes a typed Data interface per section.
|
|
20
20
|
*/
|
|
21
21
|
export interface SectionProps<D = Record<string, unknown>> { data: D; variant: string; settings: Settings; page: Page }
|
|
22
|
-
export type { LinkValue, EventItem, PostItem, PostAuthor, BookingItem } from '../lib/sections';
|
|
22
|
+
export type { LinkValue, EventItem, EventItemRoom, PostItem, PostAuthor, BookingItem } from '../lib/sections';
|
|
23
23
|
/** Props for templates/header.astro and templates/footer.astro. */
|
|
24
24
|
export type { SiteChrome, ChromeLink } from '../lib/site-chrome';
|
|
25
25
|
// core's chrome labels for any language (a template already gets them as site.words)
|