@acms-subir/core 0.3.0 → 0.3.2
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 +22 -0
- package/docs/live-video.md +91 -0
- package/integration.mjs +39 -0
- package/package.json +2 -1
- package/src/actions/index.ts +3 -2
- package/src/components/LinksManager.tsx +5 -1
- package/src/components/LiveCard.astro +174 -0
- package/src/components/LiveManager.tsx +910 -0
- package/src/components/ThemeSections.astro +2 -1
- package/src/components/WidgetRenderer.astro +3 -0
- package/src/components/editor/extensions/cards.tsx +3 -38
- package/src/components/editor/extensions/mediaCards.tsx +4 -0
- package/src/layouts/Admin.astro +1 -1
- package/src/layouts/Site.astro +28 -0
- package/src/lib/api.ts +6 -2
- package/src/lib/backup/content.ts +1 -1
- package/src/lib/embeds.ts +50 -0
- package/src/lib/live/client.ts +88 -0
- package/src/lib/live/crypto.ts +28 -0
- package/src/lib/live/migration.ts +15 -0
- package/src/lib/live/providers.ts +152 -0
- package/src/lib/live/service.ts +170 -0
- package/src/lib/live/signals.ts +89 -0
- package/src/lib/live/store.ts +49 -0
- package/src/lib/live/types.ts +52 -0
- package/src/lib/live/view.ts +78 -0
- package/src/lib/repo/index.ts +12 -8
- package/src/lib/repo/migrations.ts +4 -0
- package/src/lib/repo/sql.ts +58 -12
- package/src/lib/repo/types.ts +3 -0
- package/src/lib/scheduler.ts +2 -0
- package/src/lib/schema.ts +7 -1
- package/src/lib/themeTokens.ts +9 -0
- package/src/lib/webhook-sql.ts +68 -0
- package/src/lib/webhooks.ts +15 -7
- package/src/lib/widgets.ts +1 -0
- package/src/middleware.ts +10 -1
- package/src/pages/[...slug].astro +6 -0
- package/src/pages/admin/calendar/integrations.astro +2 -219
- package/src/pages/admin/integrations.astro +237 -0
- package/src/pages/admin/live.astro +12 -0
- package/src/pages/api/calendar/docs.ts +4 -2
- package/src/pages/api/live/oauth/youtube/callback.ts +19 -0
- package/src/pages/api/live/settings.ts +45 -0
- package/src/pages/api/live/status.ts +7 -0
- package/src/pages/api/live/thumb.ts +48 -0
- package/src/pages/api/webhooks/docs.ts +61 -0
- package/src/pages/index.astro +6 -0
- package/src/pages/links.astro +6 -0
- package/src/styles/live-admin.css +126 -0
- package/src/styles/live.css +199 -0
- package/src/theme-sdk/contract.mjs +1 -0
- package/src/theme-sdk/tokens.mjs +3 -0
- package/src/themes/editorial/theme.css +5 -0
- package/src/themes/editorial/theme.json +2 -0
- package/src/themes/field/theme.json +1 -0
- package/src/themes/night/theme.json +1 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { WEBHOOK_EVENTS } from '../../../lib/schema';
|
|
3
|
+
|
|
4
|
+
export const GET: APIRoute = () => new Response(`# ACMS webhooks
|
|
5
|
+
|
|
6
|
+
Owners manage endpoints in Team → API & webhooks (/admin/integrations).
|
|
7
|
+
This screen is available even when the calendar is disabled.
|
|
8
|
+
Select explicit event subscriptions; use Edit event subscriptions to extend an existing endpoint.
|
|
9
|
+
Existing explicit subscriptions do not change. Existing '*' subscriptions receive the new events too.
|
|
10
|
+
|
|
11
|
+
## Events
|
|
12
|
+
|
|
13
|
+
${WEBHOOK_EVENTS.map(e => `- ${e}`).join('\n')}
|
|
14
|
+
|
|
15
|
+
post/page.published: a document becomes public, including scheduled posts.
|
|
16
|
+
post/page.updated: public content changes (including applying a pending draft).
|
|
17
|
+
post/page.unpublished: a public document becomes draft, archived or scheduled.
|
|
18
|
+
post/page.deleted: a published document is deleted.
|
|
19
|
+
Draft creation, autosaves, staged edits and discarded drafts do not emit these events.
|
|
20
|
+
Custom post collections use post.* with the collection key in the payload.
|
|
21
|
+
|
|
22
|
+
design.published: a design draft is published, including its navigation and sections.
|
|
23
|
+
Restoring design history only stages a draft; publishing it emits this event.
|
|
24
|
+
media.created/updated/deleted: a media record is added, its alt text changes, or it is removed.
|
|
25
|
+
site.restored: a content backup is restored; resync instead of expecting individual record events.
|
|
26
|
+
Existing event.* and booking.* payloads are described at /api/calendar/docs.
|
|
27
|
+
|
|
28
|
+
## Payloads
|
|
29
|
+
|
|
30
|
+
Every POST: { "id": "delivery-id", "type": "post.published", "createdAt": "ISO timestamp", "data": { ... } }
|
|
31
|
+
|
|
32
|
+
post.* data: { "post": { "id", "slug", "title", "status", "collection" }, "previous"?: { same metadata } | null }
|
|
33
|
+
page.* data: { "page": { "id", "slug", "title", "status", "locale", "isHome" }, "previous"?: { same metadata } | null }
|
|
34
|
+
design.published data: { "design": { "revision", "activeTheme", "navigationChanged", "sectionPageIds" } }
|
|
35
|
+
media.* data: { "media": { "id", "key", "filename", "mime", "width", "height", "size", "alt" } }
|
|
36
|
+
site.restored data: { "counts": { "posts", "pages", "media" } }
|
|
37
|
+
|
|
38
|
+
Content notifications carry metadata, not document bodies or pending drafts.
|
|
39
|
+
On publication/update, fetch current public content from /api/v1/content/<id>?kind=post (or kind=page).
|
|
40
|
+
For unpublish/delete, remove the ID from your index; the public API may already return 404.
|
|
41
|
+
Use previous.slug/collection/locale/isHome to invalidate old routes after a move.
|
|
42
|
+
Treat payloads as event-time snapshots; subsequent edits may already have happened.
|
|
43
|
+
Publication events do not include account details, API keys, settings credentials or draft content.
|
|
44
|
+
Media metadata is sent only to endpoints explicitly subscribed to media events (or '*').
|
|
45
|
+
|
|
46
|
+
## Verification and delivery
|
|
47
|
+
|
|
48
|
+
Headers: X-Acms-Event, X-Acms-Delivery, X-Acms-Timestamp, X-Acms-Signature.
|
|
49
|
+
X-Acms-Signature = sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>.
|
|
50
|
+
Verify the raw bytes before parsing, compare signatures in constant time, and reject stale timestamps.
|
|
51
|
+
Return 2xx promptly after accepting a delivery; redirects are not followed.
|
|
52
|
+
Failed deliveries retry after 1, 10 and 60 minutes, subject to scheduler execution.
|
|
53
|
+
Requests time out after 8 seconds. Delivery is at-least-once; deduplicate by X-Acms-Delivery/id.
|
|
54
|
+
Delivery order is not guaranteed. Retries keep the same ID and body with a fresh signature/timestamp.
|
|
55
|
+
New notifications are persisted with the mutation and flushed after HTTP writes.
|
|
56
|
+
The scheduled worker/Node scheduler and request sweep recover pending deliveries.
|
|
57
|
+
Keep the independent scheduler configured for delivery during periods without traffic.
|
|
58
|
+
Send test emits ping. Paused endpoints receive no new notifications.
|
|
59
|
+
|
|
60
|
+
Webhooks notify another server; they do not push changes directly into an open browser.
|
|
61
|
+
`, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
|
package/src/pages/index.astro
CHANGED
|
@@ -23,6 +23,12 @@ const settings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies,
|
|
|
23
23
|
// /es must never 404 mid-setup
|
|
24
24
|
if (wantsLocaleHome && !home) home = await repo.getHomePage();
|
|
25
25
|
|
|
26
|
+
// Live state must not survive in a cached page after its snapshot expires.
|
|
27
|
+
if (settings.live.mode !== 'off' || home?.blocks.some(block => block.type === 'live') ||
|
|
28
|
+
(home && settings.sectionPages?.[home.id]?.sections.some(section => section.type === 'live'))) {
|
|
29
|
+
Astro.response.headers.set('Cache-Control', 'no-store');
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
// A theme may ship templates/home.astro — a coded homepage that wins over the
|
|
27
33
|
// isHome content page unless the Design screen forces 'content'. The template
|
|
28
34
|
// receives the page (its body renders as the hero) plus published posts/tags.
|
package/src/pages/links.astro
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import LiveCard from '../components/LiveCard.astro';
|
|
2
3
|
import Site from '../layouts/Site.astro';
|
|
3
4
|
import { getRepo } from '../lib/repo';
|
|
4
5
|
import { cleanEventName } from '../lib/analytics';
|
|
@@ -7,6 +8,7 @@ import { effectiveNav } from '../lib/designPreview';
|
|
|
7
8
|
|
|
8
9
|
const repo = await getRepo(Astro.locals);
|
|
9
10
|
const [links, settings, liveNav] = await Promise.all([repo.listLinks(), repo.getSettings(), repo.listNavItems()]);
|
|
11
|
+
if (links.some(link => link.kind === 'live')) Astro.response.headers.set('Cache-Control', 'no-store');
|
|
10
12
|
// staged social links show up in design preview mode
|
|
11
13
|
const navItems = await effectiveNav(liveNav, Astro.url, Astro.cookies, Astro.locals);
|
|
12
14
|
// the site's social links (Navigation → Social) as an icon row under the bio
|
|
@@ -104,6 +106,8 @@ const visible = links.filter((l) => {
|
|
|
104
106
|
))}
|
|
105
107
|
</nav>
|
|
106
108
|
)}
|
|
109
|
+
{/* while live, the Live block moves here from wherever it was placed */}
|
|
110
|
+
<span data-live-slot hidden></span>
|
|
107
111
|
{visible.map((l) => {
|
|
108
112
|
const ico = iconInfo(l);
|
|
109
113
|
const icon =
|
|
@@ -112,6 +116,8 @@ const visible = links.filter((l) => {
|
|
|
112
116
|
ico.type === 'emoji' ? <span class="link-ico link-ico-emoji">{ico.value}</span> :
|
|
113
117
|
null;
|
|
114
118
|
switch (l.kind) {
|
|
119
|
+
case 'live':
|
|
120
|
+
return <LiveCard compact />;
|
|
115
121
|
case 'heading':
|
|
116
122
|
return <h2 class="link-heading">{l.label}</h2>;
|
|
117
123
|
case 'text':
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/* Live video admin (LiveManager.tsx). Scoped under .live-admin; reuses the
|
|
2
|
+
Inset Paper tokens from admin.css. */
|
|
3
|
+
|
|
4
|
+
.live-admin { display: flex; flex-direction: column; gap: 14px; max-width: 900px; }
|
|
5
|
+
.live-admin .page-head { margin-bottom: 0; }
|
|
6
|
+
.live-admin .sub { max-width: 60ch; }
|
|
7
|
+
.live-admin code, .live-admin .mono { font-family: ui-monospace, 'SF Mono', monospace; font-size: 12.5px; }
|
|
8
|
+
.live-admin .hint { margin: 0; }
|
|
9
|
+
.live-admin a { color: var(--accent-deep); text-underline-offset: 2px; }
|
|
10
|
+
.live-admin fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
|
|
11
|
+
.live-admin fieldset:disabled { opacity: 0.75; }
|
|
12
|
+
.live-admin input[readonly] { background: var(--panel-2); color: var(--text-2); }
|
|
13
|
+
.live-admin .notice { margin-bottom: 0; font-size: 13px; }
|
|
14
|
+
.live-admin-msgs:empty { display: none; }
|
|
15
|
+
.live-admin-msgs .flash { margin: 0; }
|
|
16
|
+
.live-admin-flash { margin: 10px 0 0 !important; }
|
|
17
|
+
|
|
18
|
+
.live-admin-lede { margin: 0; color: var(--text-2); font-size: 13.5px; max-width: 64ch; }
|
|
19
|
+
|
|
20
|
+
.live-admin-card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow-card); padding: 16px 18px; }
|
|
21
|
+
.live-admin-card h2 { font-size: 14.5px; margin: 0; letter-spacing: -0.01em; color: var(--text); }
|
|
22
|
+
.live-admin-card h3 { font-size: 13px; margin: 0 0 4px; color: var(--text); }
|
|
23
|
+
.live-admin-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
|
24
|
+
|
|
25
|
+
/* status marker: dot + text, never a filled badge */
|
|
26
|
+
.live-admin-status { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 600; color: var(--text-2); }
|
|
27
|
+
.live-admin-status i, .live-admin-dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); display: inline-block; }
|
|
28
|
+
.live-admin .is-live i, .live-admin-dot.is-live { background: var(--red); box-shadow: 0 0 0 3px var(--red-soft); }
|
|
29
|
+
.live-admin .is-ok i, .live-admin-dot.is-ok { background: var(--green); }
|
|
30
|
+
.live-admin .is-warn i, .live-admin-dot.is-warn { background: var(--amber); }
|
|
31
|
+
.live-admin .is-bad i, .live-admin-dot.is-bad { background: var(--red); }
|
|
32
|
+
.live-admin-status.is-live { color: var(--red); }
|
|
33
|
+
.live-admin-status.is-ok { color: var(--green); }
|
|
34
|
+
.live-admin-status.is-warn { color: var(--amber); }
|
|
35
|
+
.live-admin-status.is-bad { color: var(--red); }
|
|
36
|
+
|
|
37
|
+
/* right now */
|
|
38
|
+
.live-admin-now p { margin: 0 0 10px; font-size: 14px; color: var(--text); }
|
|
39
|
+
.live-admin-now .live-admin-status { margin-right: 6px; }
|
|
40
|
+
.live-admin-conns { list-style: none; margin: 12px 0 0; padding: 10px 13px; background: var(--panel-2); border-radius: var(--r-md); display: grid; gap: 6px; }
|
|
41
|
+
.live-admin-conns li { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 13px; }
|
|
42
|
+
.live-admin-conns b { font-weight: 600; }
|
|
43
|
+
.live-admin-sched { margin-top: 10px !important; }
|
|
44
|
+
|
|
45
|
+
/* settings */
|
|
46
|
+
.live-admin-modes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-bottom: 14px !important; }
|
|
47
|
+
.live-admin-modes legend { font-size: 12px; color: var(--muted); font-weight: 500; padding: 0; margin-bottom: 5px; }
|
|
48
|
+
.live-admin-mode { display: flex; gap: 9px; align-items: flex-start; padding: 10px 12px; border: 1px solid var(--line-strong); border-radius: var(--r-md); cursor: pointer; background: var(--panel); }
|
|
49
|
+
.live-admin-mode:hover { background: var(--hover); }
|
|
50
|
+
.live-admin-mode.on { background: var(--sel); }
|
|
51
|
+
.live-admin-mode:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
|
|
52
|
+
.live-admin-mode input { accent-color: var(--accent); margin: 3px 0 0; flex: 0 0 auto; }
|
|
53
|
+
.live-admin-mode b { display: block; font-size: 13px; }
|
|
54
|
+
.live-admin-mode small { display: block; font-size: 12px; color: var(--muted); margin-top: 1px; line-height: 1.4; }
|
|
55
|
+
.live-admin-fields { margin-bottom: 4px !important; }
|
|
56
|
+
.live-admin-fields > .notice { margin: 0 0 12px !important; }
|
|
57
|
+
/* how it looks */
|
|
58
|
+
.live-admin-look { margin-top: 14px !important; padding-top: 14px !important; border-top: 1px solid var(--line) !important; }
|
|
59
|
+
.live-admin-look legend { font-size: 12px; color: var(--muted); font-weight: 500; padding: 0; float: left; width: 100%; margin-bottom: 6px; }
|
|
60
|
+
.live-admin-styles { clear: both; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
61
|
+
.live-admin-style > span { min-width: 0; }
|
|
62
|
+
.live-admin-glyph { display: block; position: relative; width: 100%; max-width: 132px; aspect-ratio: 2 / 1; margin: 1px 0 8px; border-radius: 6px; background: var(--panel-2); border: 1px solid var(--line); overflow: hidden; }
|
|
63
|
+
.live-admin-glyph em, .live-admin-glyph s { position: absolute; display: block; border-radius: 3px; }
|
|
64
|
+
.live-admin-glyph em { background: var(--line-strong); }
|
|
65
|
+
.live-admin-glyph s { width: 14px; height: 5px; background: #e0352b; border-radius: 1.5px; }
|
|
66
|
+
.live-admin-glyph.is-poster em { left: 8px; right: 8px; top: 6px; bottom: 16px; }
|
|
67
|
+
.live-admin-glyph.is-poster s { left: 11px; top: 9px; }
|
|
68
|
+
.live-admin-glyph.is-poster::after { content: ''; position: absolute; left: 8px; bottom: 7px; width: 55%; height: 4px; border-radius: 2px; background: var(--line-strong); }
|
|
69
|
+
.live-admin-glyph.is-split em { left: 6px; top: 7px; bottom: 7px; width: 54%; }
|
|
70
|
+
.live-admin-glyph.is-split s { left: 9px; top: 10px; }
|
|
71
|
+
.live-admin-glyph.is-split::after { content: ''; position: absolute; right: 8px; top: 40%; width: 28%; height: 4px; border-radius: 2px; background: var(--line-strong); box-shadow: 0 7px 0 var(--line-strong); }
|
|
72
|
+
.live-admin-glyph.is-row em { left: 6px; top: 35%; height: 30%; width: 28%; }
|
|
73
|
+
.live-admin-glyph.is-row s { left: 8px; top: calc(35% + 2px); width: 9px; height: 4px; }
|
|
74
|
+
.live-admin-glyph.is-row::after { content: ''; position: absolute; left: 40%; top: 46%; width: 44%; height: 4px; border-radius: 2px; background: var(--line-strong); }
|
|
75
|
+
.live-admin-style.on .live-admin-glyph { border-color: var(--accent); }
|
|
76
|
+
.live-admin-look-hint { display: block; margin: 6px 0 10px; }
|
|
77
|
+
.live-admin-look .check-row { margin-top: 6px; }
|
|
78
|
+
.live-admin-sub { margin: 14px 0 2px; font-size: 12px; color: var(--muted); font-weight: 500; }
|
|
79
|
+
|
|
80
|
+
.live-admin-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
|
|
81
|
+
.live-admin-place { margin-top: 14px !important; padding-top: 12px; border-top: 1px solid var(--line); }
|
|
82
|
+
|
|
83
|
+
/* provider tabs (text tabs on the kit segmented track) */
|
|
84
|
+
.live-admin-tabs { margin-bottom: 14px; }
|
|
85
|
+
.live-admin-tabs > button { font-size: 12.5px; padding: 5px 14px; gap: 7px; }
|
|
86
|
+
.live-admin-tabs > button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
87
|
+
|
|
88
|
+
.live-admin-panel[hidden] { display: none; }
|
|
89
|
+
.live-admin-health { padding: 12px 14px; background: var(--panel-2); border-radius: var(--r-md); margin-bottom: 12px; }
|
|
90
|
+
.live-admin-dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 8px 16px; margin: 10px 0 0; }
|
|
91
|
+
.live-admin-dl dt { font-size: 11.5px; color: var(--muted); }
|
|
92
|
+
.live-admin-dl dd { margin: 1px 0 0; font-size: 13px; color: var(--text); overflow-wrap: anywhere; }
|
|
93
|
+
|
|
94
|
+
.live-admin-guide { border: 1px solid var(--line); border-radius: var(--r-md); margin-bottom: 14px; }
|
|
95
|
+
.live-admin-guide > summary { cursor: pointer; padding: 10px 13px; font-size: 13px; font-weight: 600; color: var(--text); list-style-position: inside; }
|
|
96
|
+
.live-admin-guide > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--r-md); }
|
|
97
|
+
.live-admin-guide[open] > summary { border-bottom: 1px solid var(--line); }
|
|
98
|
+
.live-admin-guide ol { margin: 0; padding: 12px 16px 4px 34px; display: grid; gap: 8px; font-size: 13px; color: var(--text-2); line-height: 1.5; }
|
|
99
|
+
.live-admin-guide > .hint { padding: 6px 13px 12px; line-height: 1.5; }
|
|
100
|
+
.live-admin-copy { display: flex; gap: 6px; align-items: center; margin-top: 6px; }
|
|
101
|
+
.live-admin-copy input { flex: 1; min-width: 0; }
|
|
102
|
+
|
|
103
|
+
.live-admin-form { margin-bottom: 4px; }
|
|
104
|
+
.live-admin-form > h3 { margin-bottom: 10px; }
|
|
105
|
+
.live-admin-steps-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin-top: 14px; }
|
|
106
|
+
.live-admin-step { border: 1px solid var(--line); border-radius: var(--r-md); padding: 12px 13px; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
|
|
107
|
+
.live-admin-step .field { margin-bottom: 0; width: 100%; }
|
|
108
|
+
.live-admin-step .live-admin-actions { margin-top: 0; }
|
|
109
|
+
|
|
110
|
+
.live-admin-danger { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); }
|
|
111
|
+
.live-admin .btn.ghost.danger { color: var(--red); }
|
|
112
|
+
.live-admin .btn:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
113
|
+
.live-admin-copied:empty { display: none; }
|
|
114
|
+
.live-admin-copied { margin-top: 10px !important; }
|
|
115
|
+
|
|
116
|
+
.live-admin-roadmap p { margin: 4px 0 0; font-size: 13px; color: var(--muted); }
|
|
117
|
+
|
|
118
|
+
@media (max-width: 767px) {
|
|
119
|
+
.live-admin-card { padding: 14px; }
|
|
120
|
+
.live-admin-modes, .live-admin-styles { grid-template-columns: 1fr; }
|
|
121
|
+
.live-admin-glyph { display: none; }
|
|
122
|
+
.live-admin-tabs { display: flex; }
|
|
123
|
+
.live-admin-tabs > button { flex: 1; min-height: 36px; }
|
|
124
|
+
.live-admin-actions .btn { flex: 1; justify-content: center; }
|
|
125
|
+
.live-admin-conns li { flex-wrap: wrap; }
|
|
126
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* Live video: the card (all layouts) and the site-wide signals.
|
|
2
|
+
*
|
|
3
|
+
* Theme API — override any of these on :root (or a wrapper) in theme.css:
|
|
4
|
+
* --live-color tag, markers, avatar ring (theme token `live`)
|
|
5
|
+
* --live-ink text on --live-color
|
|
6
|
+
* --live-radius poster + card corners
|
|
7
|
+
* --live-tag-radius the LIVE tag (keep it a rectangle, never a pill)
|
|
8
|
+
* --live-poster-ratio poster shape
|
|
9
|
+
* --live-pulse marker blink duration (0s = still)
|
|
10
|
+
* Hooks: .pc-live[data-layout=poster|split|row|links][data-status=live|upcoming|offline],
|
|
11
|
+
* html[data-live=live|upcoming] while a broadcast is on.
|
|
12
|
+
*/
|
|
13
|
+
/* Wrapped in @layer base so theme CSS (inlined unlayered by Site.astro)
|
|
14
|
+
always wins ties, regardless of bundle order in the built head. */
|
|
15
|
+
@layer base {
|
|
16
|
+
:root {
|
|
17
|
+
--live-color: var(--live, #d42f25);
|
|
18
|
+
--live-ink: var(--live-on, #fff);
|
|
19
|
+
--live-radius: var(--r, 16px);
|
|
20
|
+
--live-tag-radius: 5px;
|
|
21
|
+
--live-poster-ratio: 16 / 9;
|
|
22
|
+
--live-pulse: 1.6s;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@keyframes pc-live-pulse { 50% { opacity: 0.35; } }
|
|
26
|
+
|
|
27
|
+
.pc-live { margin: 28px 0; color: var(--ink); container-type: inline-size; text-align: left; }
|
|
28
|
+
.pc-live[hidden], .pc-live [hidden], .pc-live-dock[hidden], .pc-live-bar[hidden], .site-live[hidden] { display: none !important; }
|
|
29
|
+
.pc-live :focus-visible, .pc-live-dock :focus-visible, .pc-live-bar :focus-visible, .site-live:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
30
|
+
|
|
31
|
+
/* ---- offline: one slim row instead of an empty card ---- */
|
|
32
|
+
.pc-live-off {
|
|
33
|
+
display: none; align-items: center; gap: 10px; padding: 12px 16px;
|
|
34
|
+
border: 1px solid var(--line); border-radius: var(--live-radius); background: var(--panel); font-size: 0.92em;
|
|
35
|
+
}
|
|
36
|
+
.pc-live[data-status='offline'] .pc-live-off { display: flex; }
|
|
37
|
+
.pc-live[data-status='offline'] .pc-live-body { display: none; }
|
|
38
|
+
.pc-live-off-dot { width: 7px; height: 7px; border-radius: 2px; background: var(--muted); opacity: 0.6; flex: none; }
|
|
39
|
+
.pc-live-off span { flex: 1; color: var(--muted); }
|
|
40
|
+
.pc-live-off a { font-weight: 600; color: var(--ink); text-decoration: none; white-space: nowrap; }
|
|
41
|
+
.pc-live-off a:hover { text-decoration: underline; text-underline-offset: 3px; }
|
|
42
|
+
|
|
43
|
+
/* ---- card body ---- */
|
|
44
|
+
.pc-live-body {
|
|
45
|
+
background: var(--panel); border: 1px solid var(--line);
|
|
46
|
+
border-radius: calc(var(--live-radius) + 4px); padding: 10px; box-shadow: var(--sh-1);
|
|
47
|
+
}
|
|
48
|
+
.pc-live-media { position: relative; min-width: 0; }
|
|
49
|
+
.pc-live-poster {
|
|
50
|
+
position: relative; display: block; aspect-ratio: var(--live-poster-ratio); overflow: hidden;
|
|
51
|
+
border-radius: var(--live-radius); color: var(--muted);
|
|
52
|
+
background: color-mix(in srgb, var(--live-color) 7%, var(--soft));
|
|
53
|
+
}
|
|
54
|
+
.pc-live[data-status='upcoming'] .pc-live-poster { background: color-mix(in srgb, var(--accent) 8%, var(--soft)); }
|
|
55
|
+
.pc-live-poster img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
|
56
|
+
.pc-live-ph { position: absolute; inset: 0; display: grid; place-items: center; }
|
|
57
|
+
.pc-live-ph svg { width: 22%; max-width: 64px; opacity: 0.5; }
|
|
58
|
+
|
|
59
|
+
.pc-live-tag {
|
|
60
|
+
position: absolute; top: 10px; left: 10px; display: inline-flex; align-items: center; gap: 6px;
|
|
61
|
+
padding: 6px 8px; border-radius: var(--live-tag-radius);
|
|
62
|
+
background: var(--live-color); color: var(--live-ink);
|
|
63
|
+
font: 700 11px/1 var(--font-body); letter-spacing: 0.08em;
|
|
64
|
+
}
|
|
65
|
+
.pc-live-tag::before { content: ''; width: 6px; height: 6px; border-radius: 1.5px; background: currentColor; animation: pc-live-pulse var(--live-pulse) ease-in-out infinite; }
|
|
66
|
+
.pc-live[data-status='upcoming'] .pc-live-tag { background: rgb(12 13 16 / 80%); color: #fff; letter-spacing: 0; font-weight: 600; }
|
|
67
|
+
.pc-live[data-status='upcoming'] .pc-live-tag::before { display: none; }
|
|
68
|
+
.pc-live-viewers {
|
|
69
|
+
position: absolute; left: 10px; bottom: 10px; padding: 5px 7px; border-radius: var(--live-tag-radius);
|
|
70
|
+
background: rgb(12 13 16 / 72%); color: #fff; font: 500 11.5px/1 var(--font-body);
|
|
71
|
+
}
|
|
72
|
+
.pc-live-play { position: absolute; inset: 0; }
|
|
73
|
+
.pc-live-play::before {
|
|
74
|
+
content: ''; position: absolute; left: 50%; top: 50%; width: 58px; height: 58px; margin: -29px 0 0 -29px;
|
|
75
|
+
border-radius: 16px; background: rgb(255 255 255 / 92%); box-shadow: 0 10px 28px -10px rgb(0 0 0 / 55%);
|
|
76
|
+
transition: transform 0.15s ease;
|
|
77
|
+
}
|
|
78
|
+
.pc-live-play::after {
|
|
79
|
+
content: ''; position: absolute; left: 50%; top: 50%; margin: -10px 0 0 -5px;
|
|
80
|
+
border-left: 16px solid #17181c; border-top: 10px solid transparent; border-bottom: 10px solid transparent;
|
|
81
|
+
}
|
|
82
|
+
.pc-live-poster:hover .pc-live-play::before { transform: scale(1.06); }
|
|
83
|
+
.pc-live-player iframe { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; border-radius: var(--live-radius); background: #000; }
|
|
84
|
+
|
|
85
|
+
/* ---- text ---- */
|
|
86
|
+
.pc-live-info { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; padding: 12px 6px 6px; min-width: 0; }
|
|
87
|
+
.pc-live-kicker, .pc-live-mark {
|
|
88
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
89
|
+
font: 700 11px/1.2 var(--font-body); letter-spacing: 0.08em; text-transform: uppercase; color: var(--live-color);
|
|
90
|
+
}
|
|
91
|
+
.pc-live-kicker::before, .pc-live-mark::before { content: ''; width: 7px; height: 7px; border-radius: 2px; background: currentColor; animation: pc-live-pulse var(--live-pulse) ease-in-out infinite; flex: none; }
|
|
92
|
+
.pc-live[data-status='upcoming'] .pc-live-kicker { color: var(--accent-text, var(--accent)); }
|
|
93
|
+
.pc-live[data-status='upcoming'] .pc-live-kicker::before { animation: none; }
|
|
94
|
+
.pc-live-kicker { display: none; }
|
|
95
|
+
/* typography sits on the inner span: section/prose h2 rules (unlayered) can't reach it */
|
|
96
|
+
.pc-live-title-text { display: block; font: 600 1.08rem/1.3 var(--font-heading); letter-spacing: -0.01em; overflow-wrap: anywhere; }
|
|
97
|
+
.pc-live-meta { margin: 0; color: var(--muted); font-size: 0.86em; }
|
|
98
|
+
.pc-live-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
|
|
99
|
+
.pc-live-btn {
|
|
100
|
+
display: inline-flex; align-items: center; gap: 6px; padding: 9px 14px; cursor: pointer;
|
|
101
|
+
border: 1px solid transparent; border-radius: var(--r-sm, 10px);
|
|
102
|
+
background: var(--btn, var(--ink)); color: var(--btn-ink, var(--bg));
|
|
103
|
+
font: 600 0.88em/1.2 var(--font-body); text-decoration: none; white-space: nowrap;
|
|
104
|
+
}
|
|
105
|
+
.pc-live-btn:hover { filter: brightness(1.08); }
|
|
106
|
+
.pc-live[data-playable='true'] a.pc-live-btn { background: transparent; color: var(--ink); border-color: var(--line); }
|
|
107
|
+
.pc-live-privacy { color: var(--muted); font-size: 0.78em; margin-top: 6px; }
|
|
108
|
+
|
|
109
|
+
/* ---- split: a hero (the "player" section style, or the owner's card style) ---- */
|
|
110
|
+
.pc-live[data-layout='split'] .pc-live-body {
|
|
111
|
+
display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 24px; align-items: center; padding: 12px;
|
|
112
|
+
border-radius: calc(var(--live-radius) + 6px);
|
|
113
|
+
}
|
|
114
|
+
.pc-live[data-layout='split'] .pc-live-info { padding: 4px 8px 4px 0; }
|
|
115
|
+
.pc-live[data-layout='split'] .pc-live-kicker { display: inline-flex; }
|
|
116
|
+
.pc-live[data-layout='split'] .pc-live-title-text { margin: 8px 0 4px; }
|
|
117
|
+
.pc-live[data-layout='split'] .pc-live-title-text {
|
|
118
|
+
font-size: clamp(1.3rem, 3.4cqi, 1.9rem); line-height: 1.12;
|
|
119
|
+
font-weight: var(--display-weight, 650); letter-spacing: var(--display-tracking, -0.03em);
|
|
120
|
+
}
|
|
121
|
+
@container (max-width: 700px) {
|
|
122
|
+
.pc-live[data-layout='split'] .pc-live-body { grid-template-columns: 1fr; gap: 4px; padding: 10px; }
|
|
123
|
+
.pc-live[data-layout='split'] .pc-live-info { padding: 10px 6px 6px; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ---- row: compact, for sidebars and busy pages ---- */
|
|
127
|
+
.pc-live[data-layout='row'] .pc-live-body { display: grid; grid-template-columns: 168px minmax(0, 1fr); gap: 16px; align-items: center; padding: 8px; }
|
|
128
|
+
.pc-live[data-layout='row'] .pc-live-poster { border-radius: calc(var(--live-radius) * 0.7); }
|
|
129
|
+
.pc-live[data-layout='row'] .pc-live-tag { top: 6px; left: 6px; padding: 4px 6px; font-size: 10px; }
|
|
130
|
+
.pc-live[data-layout='row'] .pc-live-viewers, .pc-live[data-layout='row'] .pc-live-ph svg { display: none; }
|
|
131
|
+
.pc-live[data-layout='row'] .pc-live-info { padding: 2px 8px 2px 0; }
|
|
132
|
+
.pc-live[data-layout='row'] .pc-live-actions { margin-top: 6px; }
|
|
133
|
+
@container (max-width: 420px) {
|
|
134
|
+
.pc-live[data-layout='row'] .pc-live-body { grid-template-columns: 112px minmax(0, 1fr); gap: 12px; }
|
|
135
|
+
.pc-live[data-layout='row'] .pc-live-tag { font-size: 0; gap: 0; padding: 5px; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ---- links page block ---- */
|
|
139
|
+
.pc-live[data-layout='links'] { margin: 12px 0; }
|
|
140
|
+
.pc-live[data-layout='links'] .pc-live-body { padding: 8px; }
|
|
141
|
+
.pc-live[data-layout='links'] .pc-live-info { flex-direction: row; align-items: center; gap: 12px; padding: 10px 4px 2px; }
|
|
142
|
+
.pc-live[data-layout='links'] .pc-live-title { flex: 1; }
|
|
143
|
+
.pc-live[data-layout='links'] .pc-live-title-text { font-size: 0.95rem; }
|
|
144
|
+
.pc-live[data-layout='links'] .pc-live-meta { display: none; }
|
|
145
|
+
.pc-live[data-layout='links'] .pc-live-actions { margin: 0; }
|
|
146
|
+
.pc-live[data-layout='links'] .pc-live-btn { padding: 7px 11px; font-size: 0.82em; }
|
|
147
|
+
html[data-live='live'] .links-avatar { box-shadow: 0 0 0 3px var(--bg), 0 0 0 5px var(--live-color); }
|
|
148
|
+
|
|
149
|
+
/* ---- site-wide: live bar (takes the announcement slot while live) ---- */
|
|
150
|
+
.pc-live-bar {
|
|
151
|
+
display: flex; align-items: center; justify-content: center; gap: 12px; position: relative;
|
|
152
|
+
padding: 9px 44px; background: var(--ink); color: var(--bg); font-size: 14px;
|
|
153
|
+
}
|
|
154
|
+
.pc-live-bar .pc-live-mark { color: color-mix(in srgb, var(--live-color) 70%, var(--bg)); }
|
|
155
|
+
.pc-live-bar-title { opacity: 0.82; min-width: 0; max-width: 46ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
156
|
+
.pc-live-bar a { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 3px; white-space: nowrap; }
|
|
157
|
+
.pc-live-x {
|
|
158
|
+
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
|
|
159
|
+
background: none; border: 0; color: inherit; opacity: 0.7; cursor: pointer; font-size: 12px; padding: 6px; border-radius: 6px;
|
|
160
|
+
}
|
|
161
|
+
.pc-live-x:hover { opacity: 1; background: color-mix(in srgb, currentColor 12%, transparent); }
|
|
162
|
+
html[data-live-bar] .announcement-bar { display: none; }
|
|
163
|
+
@media (max-width: 560px) { .pc-live-bar-title { display: none; } }
|
|
164
|
+
|
|
165
|
+
/* ---- site-wide: header marker ---- */
|
|
166
|
+
.site-live {
|
|
167
|
+
display: inline-flex; align-items: center; gap: 7px; padding: 7px 10px; white-space: nowrap;
|
|
168
|
+
border: 1px solid var(--line); border-radius: var(--r-sm, 10px);
|
|
169
|
+
color: var(--ink); font: 600 13px/1 var(--font-body); text-decoration: none;
|
|
170
|
+
}
|
|
171
|
+
.site-live::before { content: ''; width: 7px; height: 7px; border-radius: 2px; background: var(--live-color); animation: pc-live-pulse var(--live-pulse) ease-in-out infinite; }
|
|
172
|
+
.site-live:hover { background: var(--soft); }
|
|
173
|
+
|
|
174
|
+
/* ---- site-wide: floating dock ---- */
|
|
175
|
+
.pc-live-dock {
|
|
176
|
+
position: fixed; right: 16px; bottom: max(16px, env(safe-area-inset-bottom)); z-index: 60;
|
|
177
|
+
width: min(300px, calc(100vw - 32px)); padding: 8px 30px 8px 8px;
|
|
178
|
+
background: var(--panel); color: var(--ink); border: 1px solid var(--line); border-radius: var(--r, 14px);
|
|
179
|
+
box-shadow: var(--sh-3); animation: pc-live-in 0.25s ease-out;
|
|
180
|
+
}
|
|
181
|
+
.pc-live-dock > a { display: grid; grid-template-columns: 104px minmax(0, 1fr); gap: 10px; align-items: center; color: inherit; text-decoration: none; }
|
|
182
|
+
.pc-live-dock-thumb {
|
|
183
|
+
position: relative; aspect-ratio: 16 / 9; border-radius: calc(var(--live-radius) * 0.6); overflow: hidden;
|
|
184
|
+
background: color-mix(in srgb, var(--live-color) 10%, var(--soft));
|
|
185
|
+
}
|
|
186
|
+
.pc-live-dock-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
|
187
|
+
.pc-live-dock b { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-top: 4px; font-size: 13px; line-height: 1.3; font-weight: 600; }
|
|
188
|
+
.pc-live-dock .pc-live-x { top: 6px; right: 4px; transform: none; color: var(--muted); }
|
|
189
|
+
@keyframes pc-live-in { from { opacity: 0; transform: translateY(8px); } }
|
|
190
|
+
|
|
191
|
+
@media (prefers-reduced-motion: reduce) {
|
|
192
|
+
.pc-live-tag::before, .pc-live-kicker::before, .pc-live-mark::before, .site-live::before { animation: none; }
|
|
193
|
+
.pc-live-dock { animation: none; }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Unlayered on purpose: prose and section rules (`.acms-section h2`, `article h2`)
|
|
198
|
+
are unlayered too, and would otherwise restyle the card's heading. */
|
|
199
|
+
.pc-live h2.pc-live-title { margin: 0; padding: 0; border: 0; font-size: 1rem; line-height: 1; }
|
|
@@ -18,6 +18,7 @@ const f = (label, type = 'text', value = '') => ({ label, type, default: value }
|
|
|
18
18
|
const base = { heading: f('Heading'), text: f('Supporting text', 'textarea') };
|
|
19
19
|
/** @type {Record<string, import("zod").infer<typeof SectionDefinition>>} */
|
|
20
20
|
export const BUILTIN_SECTIONS = {
|
|
21
|
+
live: { name: 'Live video', version: 1, variants: ['card','player'], fields: {} },
|
|
21
22
|
'page-content': { name: 'Existing page content', version: 1, variants: ['default'], fields: {} },
|
|
22
23
|
hero: { name: 'Hero', version: 1, variants: ['split', 'centered'], fields: { ...base, image: f('Image', 'image'), alt: f('Image description'), label: f('Button label'), href: f('Button link', 'url') } },
|
|
23
24
|
features: { name: 'Features', version: 1, variants: ['grid', 'list'], fields: { ...base, items: f('Features (one per line: title | description)', 'textarea') } },
|
package/src/theme-sdk/tokens.mjs
CHANGED
|
@@ -10,6 +10,7 @@ export const DarkTokens = z.object({
|
|
|
10
10
|
accent: hex.optional(),
|
|
11
11
|
panel: hex.optional(),
|
|
12
12
|
soft: hex.optional(),
|
|
13
|
+
live: hex.optional(),
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
export const ThemeTokens = z.object({
|
|
@@ -23,6 +24,8 @@ export const ThemeTokens = z.object({
|
|
|
23
24
|
soft: hex.default('#efede8'),
|
|
24
25
|
// text on an accent fill; '' = pick white or near-black by contrast
|
|
25
26
|
accentInk: hex.default(''),
|
|
27
|
+
// the live-video signal color (LIVE tag, markers); '' = a tuned red
|
|
28
|
+
live: hex.default(''),
|
|
26
29
|
// primary buttons: ink-filled (default) or accent-filled
|
|
27
30
|
button: z.enum(['ink', 'accent']).default('ink'),
|
|
28
31
|
// dark scheme
|
|
@@ -91,3 +91,8 @@ article h1 { font-size: 42px; line-height: 1.12; }
|
|
|
91
91
|
|
|
92
92
|
/* overlay hero: keep the serif display sizing on the image variant too */
|
|
93
93
|
.post-hero-content h1 { font-size: clamp(34px, 6vw, 52px); font-weight: 500; }
|
|
94
|
+
|
|
95
|
+
/* live video: square-cut tag and spaced caps to match the masthead */
|
|
96
|
+
:root { --live-tag-radius: 2px; }
|
|
97
|
+
.pc-live-tag, .pc-live-kicker, .pc-live-mark { letter-spacing: 0.14em; }
|
|
98
|
+
.pc-live[data-layout='split'] .pc-live-title-text { font-weight: 500; }
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"ink": "#181712",
|
|
7
7
|
"muted": "#6d685e",
|
|
8
8
|
"line": "#e4e0d5",
|
|
9
|
+
"live": "#b8261b",
|
|
9
10
|
"accent": "#b33a2e",
|
|
10
11
|
"panel": "#fdfcf9",
|
|
11
12
|
"soft": "#edeae1",
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
"ink": "#ece6da",
|
|
19
20
|
"muted": "#a39c8e",
|
|
20
21
|
"line": "#2d2a24",
|
|
22
|
+
"live": "#f26b57",
|
|
21
23
|
"accent": "#e0735f",
|
|
22
24
|
"panel": "#1d1b16",
|
|
23
25
|
"soft": "#25221c"
|