@acms-subir/core 0.3.40 → 0.3.42
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 +24 -0
- package/cli/migrations.generated.mjs +15 -0
- package/docs/mobile.md +12 -2
- package/integration.mjs +4 -0
- package/mobile-template/recipes/events/files/components/RsvpPanel.tsx +66 -31
- package/mobile-template/recipes/push/files/app/link-phone.tsx +43 -0
- package/mobile-template/recipes/push/files/app/notifications.tsx +14 -4
- package/mobile-template/recipes/push/recipe.json +4 -2
- package/mobile-template/recipes/rsvp/files/components/RsvpPanel.tsx +66 -31
- package/mobile-template/recipes/rsvp/recipe.json +2 -2
- package/package.json +1 -1
- package/src/actions/index.ts +1 -1
- package/src/components/Announcements.tsx +11 -2
- package/src/components/EventPage.astro +16 -8
- package/src/components/MobileApp.tsx +20 -8
- package/src/components/TestPhones.tsx +177 -0
- package/src/components/editor/EventPanel.tsx +2 -2
- package/src/components/editor/PostEditor.tsx +1 -1
- package/src/components/editor/event/RegistrationCard.tsx +5 -0
- package/src/components/editor/event/registration.ts +1 -1
- package/src/lib/announcements/api.ts +19 -2
- package/src/lib/announcements/service.ts +1 -1
- package/src/lib/booking/agent.ts +1 -1
- package/src/lib/booking/emails.ts +20 -5
- package/src/lib/booking/service.ts +148 -14
- package/src/lib/calendar/rsvp-words.ts +69 -8
- package/src/lib/content-service.ts +11 -6
- package/src/lib/drafts.ts +2 -2
- package/src/lib/editor/bookings.ts +5 -3
- package/src/lib/editor/documents.ts +3 -2
- package/src/lib/editor/schemas.ts +1 -1
- package/src/lib/email/booking-words.ts +26 -2
- package/src/lib/email/service.ts +14 -2
- package/src/lib/email/store.ts +6 -2
- package/src/lib/email/templates.ts +20 -0
- package/src/lib/forms/events.ts +4 -2
- package/src/lib/legal/app-privacy.ts +4 -4
- package/src/lib/legal/text-en.ts +2 -2
- package/src/lib/legal/text-es.ts +2 -2
- package/src/lib/mobile/bookings.ts +4 -3
- package/src/lib/mobile/guest.ts +2 -1
- package/src/lib/mobile/manage.ts +17 -3
- package/src/lib/mobile/openapi.ts +22 -5
- package/src/lib/mobile/rsvp.ts +10 -5
- package/src/lib/mobile/schemas.ts +14 -3
- package/src/lib/push/api.ts +37 -8
- package/src/lib/push/service.ts +6 -2
- package/src/lib/push/staff.ts +162 -0
- package/src/lib/push/store.ts +49 -2
- package/src/lib/push/types.ts +4 -1
- package/src/lib/repo/migrations.ts +15 -0
- package/src/lib/repo/sql.ts +57 -14
- package/src/lib/repo/types.ts +11 -4
- package/src/lib/scheduler.ts +3 -1
- package/src/lib/schema.ts +10 -2
- package/src/pages/admin/calendar/bookings.astro +57 -13
- package/src/pages/api/announcements.ts +2 -2
- package/src/pages/api/calendar/bookings/index.ts +1 -1
- package/src/pages/api/calendar/docs.ts +2 -2
- package/src/pages/api/push/admin.ts +1 -1
- package/src/pages/api/push/phones.ts +25 -0
- package/src/pages/api/v1/bookings/manage/confirm.ts +6 -0
- package/src/pages/api/v1/devices/[token]/staff-link.ts +20 -0
- package/src/pages/book/manage/[token].astro +57 -15
- package/src/pages/events/[slug].astro +3 -2
- package/src/styles/admin.css +3 -1
- package/src/styles/site.css +10 -1
- package/src/styles/test-phones.css +30 -0
- package/src/theme-sdk/index.ts +1 -1
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
import Site from '../../../layouts/Site.astro';
|
|
3
3
|
import { getRepo } from '../../../lib/repo';
|
|
4
4
|
import { CALENDAR, bookingLinkUrl, bookingsEnabled, eventUrl, resourceUrl } from '../../../lib/calendar/config';
|
|
5
|
-
import { guestCancel, guestChanges, BookingError } from '../../../lib/booking/service';
|
|
5
|
+
import { confirmOffer, guestCancel, guestChanges, waitlistPosition, BookingError, WAITLIST_HOLD } from '../../../lib/booking/service';
|
|
6
6
|
import { bookingIcs, whenText } from '../../../lib/booking/emails';
|
|
7
7
|
import { resourceZone } from '../../../lib/booking/availability';
|
|
8
8
|
import { eventTimeZone, locationLine } from '../../../lib/calendar/events';
|
|
9
9
|
import { siteOrigin } from '../../../lib/seo';
|
|
10
10
|
import { waitUntilFrom } from '../../../lib/webhooks';
|
|
11
11
|
import { I18N, localePath, requestLocale } from '../../../lib/i18n';
|
|
12
|
-
import { siteLang } from '../../../lib/dates';
|
|
12
|
+
import { siteLang, siteLocale } from '../../../lib/dates';
|
|
13
13
|
import { isExtraLocale, localizeDoc } from '../../../lib/translations';
|
|
14
|
-
import { rsvpStrings } from '../../../lib/calendar/rsvp-words';
|
|
14
|
+
import { deadlineParts, rsvpStrings } from '../../../lib/calendar/rsvp-words';
|
|
15
15
|
import { bookingStrings } from '../../../lib/email/booking-words';
|
|
16
16
|
import { fill } from '../../../lib/live/words';
|
|
17
17
|
|
|
@@ -21,7 +21,10 @@ import { fill } from '../../../lib/live/words';
|
|
|
21
21
|
// speaks the request's language, for RSVPs and room bookings alike
|
|
22
22
|
// (/es/book/manage/… in Spanish, as the emails of a booking made in Spanish
|
|
23
23
|
// link it); opened without the prefix it shows the main language, with no
|
|
24
|
-
// redirect. The room booking flow it links back to stays in English.
|
|
24
|
+
// redirect. The room booking flow it links back to stays in English. An RSVP
|
|
25
|
+
// on a full date's waitlist shows its place in line ("#3 in line · 2 seats")
|
|
26
|
+
// or the spot held for it ("A spot is yours until …", Confirm my spot), and
|
|
27
|
+
// can leave the line or release the spot.
|
|
25
28
|
if (!CALENDAR.enabled) return new Response(null, { status: 404 });
|
|
26
29
|
const repo = await getRepo(Astro.locals);
|
|
27
30
|
const booking = await repo.getBookingByToken(Astro.params.token ?? '');
|
|
@@ -52,31 +55,62 @@ if (Astro.url.searchParams.get('ics')) {
|
|
|
52
55
|
}
|
|
53
56
|
let error = '';
|
|
54
57
|
let current = booking;
|
|
58
|
+
// what this request just did: left the line (or released a held spot), or took the spot
|
|
59
|
+
let left = false, took = false;
|
|
55
60
|
if (Astro.request.method === 'POST') {
|
|
56
61
|
const form = await Astro.request.formData();
|
|
57
|
-
|
|
62
|
+
const flow = { origin, locals: Astro.locals, waitUntil: waitUntilFrom(Astro.locals) };
|
|
63
|
+
if (form.get('op') === 'cancel' && ['pending', 'confirmed', 'waitlisted', 'offered'].includes(booking.status)) {
|
|
58
64
|
try {
|
|
59
|
-
current = await guestCancel(repo, booking,
|
|
65
|
+
current = await guestCancel(repo, booking, flow);
|
|
66
|
+
left = booking.status === 'waitlisted' || booking.status === 'offered';
|
|
60
67
|
} catch (err) {
|
|
61
68
|
error = err instanceof BookingError ? err.message : w.cancelError;
|
|
62
69
|
}
|
|
63
70
|
}
|
|
71
|
+
if (form.get('op') === 'confirm' && booking.status === 'offered') {
|
|
72
|
+
try {
|
|
73
|
+
current = await confirmOffer(repo, booking, flow);
|
|
74
|
+
took = true;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
current = (await repo.getBooking(booking.id)) ?? booking;
|
|
77
|
+
// released: the page says so in its own words
|
|
78
|
+
error = current.status === 'expired' ? '' : err instanceof BookingError ? err.message : w.confirmError;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
64
81
|
}
|
|
65
82
|
const tz = event ? eventTimeZone(event, settings) : resource ? resourceZone(resource, settings.timezone) : settings.timezone || 'UTC';
|
|
66
|
-
const
|
|
67
|
-
const
|
|
83
|
+
const dateSettings = isExtraLocale(locale) ? { ...settings, locale } : settings;
|
|
84
|
+
const when = whenText(current, tz, dateSettings, link?.mode === 'day' || event?.allDay);
|
|
85
|
+
const waiting = current.status === 'waitlisted' || current.status === 'offered';
|
|
86
|
+
const active = current.status === 'pending' || current.status === 'confirmed' || waiting;
|
|
68
87
|
const upcoming = current.end > Date.now();
|
|
69
88
|
const can = guestChanges(current, link);
|
|
70
|
-
const isNew = Astro.url.searchParams.has('new');
|
|
89
|
+
const isNew = Astro.url.searchParams.has('new') || took;
|
|
71
90
|
const moved = Astro.url.searchParams.has('moved');
|
|
72
|
-
const
|
|
91
|
+
const hours = String(WAITLIST_HOLD / 3_600_000);
|
|
92
|
+
// "#3 in line · 2 seats"; "A spot is yours until Fri, Oct 24 at 9 PM"
|
|
93
|
+
const position = await waitlistPosition(repo, current);
|
|
94
|
+
const inLine = position === null ? '' : fill(w.inLine, { position: String(position), seats: fill(current.seats === 1 ? w.seat : w.seats, { count: String(current.seats) }) });
|
|
95
|
+
const holdUntil = current.status === 'offered' && current.holdUntil !== null ? deadlineParts(current.holdUntil, tz, siteLocale(dateSettings)) : null;
|
|
96
|
+
const heading = current.status === 'cancelled' ? (left ? w.offWaitlist : w.cancelledTitle)
|
|
73
97
|
: current.status === 'declined' ? w.notAvailable
|
|
98
|
+
: current.status === 'waitlisted' ? w.onWaitlist
|
|
99
|
+
: current.status === 'offered' ? w.spotOpened
|
|
100
|
+
: current.status === 'expired' ? w.released
|
|
74
101
|
: current.status === 'pending' ? (isNew ? w.requestSent : w.awaitingApproval)
|
|
75
102
|
: moved ? bw.moved : isNew ? w.booked : w.yours;
|
|
76
103
|
const lede = current.status === 'pending' ? fill(w.emailWhenApproved, { email: current.email })
|
|
77
104
|
: current.status === 'confirmed' ? (current.formResponse?.definition.confirmation || link?.confirmMessage || fill(w.confirmationOnItsWay, { email: current.email }))
|
|
78
105
|
: current.status === 'declined' ? w.declined
|
|
79
|
-
: w.
|
|
106
|
+
: current.status === 'waitlisted' ? fill(w.waitlistPromise, { hours })
|
|
107
|
+
: current.status === 'offered' ? (holdUntil ? fill(w.spotYours, holdUntil) : '')
|
|
108
|
+
: current.status === 'expired' ? fill(w.releasedText, { hours })
|
|
109
|
+
: left ? w.offWaitlistText : w.cancelledText;
|
|
110
|
+
// a room booking's own words list only its four statuses
|
|
111
|
+
const statusLabel = (w.statuses as Record<string, string>)[current.status] ?? rsvpStrings(lang).statuses[current.status];
|
|
112
|
+
const cancelLabel = current.status === 'waitlisted' ? w.leaveWaitlist : current.status === 'offered' ? w.releaseSpot : w.cancel;
|
|
113
|
+
const cancelAsk = `return confirm(${JSON.stringify(current.status === 'waitlisted' ? w.leaveConfirm : current.status === 'offered' ? w.releaseConfirm : w.cancelConfirm)})`;
|
|
80
114
|
const cutoff = fill(bw.cutoff, { hours: String(can.cutoffHours) });
|
|
81
115
|
const googleUrl = `https://calendar.google.com/calendar/render?${new URLSearchParams({ action: 'TEMPLATE', text: title, dates: `${new Date(current.start).toISOString().replace(/[-:]|\.\d{3}/g, '')}/${new Date(current.end).toISOString().replace(/[-:]|\.\d{3}/g, '')}`, details: url, location })}`;
|
|
82
116
|
---
|
|
@@ -85,10 +119,12 @@ const googleUrl = `https://calendar.google.com/calendar/render?${new URLSearchPa
|
|
|
85
119
|
<div class="pc-manage">
|
|
86
120
|
<div class={`pc-manage-icon is-${current.status}`} aria-hidden="true">
|
|
87
121
|
{current.status === 'confirmed' ? <svg viewBox="0 0 24 24"><path d="M5 12l5 5L19 7" /></svg>
|
|
88
|
-
: current.status === 'pending' ? <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" /><path d="M12 8v4l3 2" /></svg>
|
|
122
|
+
: current.status === 'pending' || current.status === 'offered' ? <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" /><path d="M12 8v4l3 2" /></svg>
|
|
123
|
+
: current.status === 'waitlisted' ? <svg viewBox="0 0 24 24"><path d="M5 7h14M5 12h14M5 17h8" /></svg>
|
|
89
124
|
: <svg viewBox="0 0 24 24"><path d="M7 7l10 10M17 7 7 17" /></svg>}
|
|
90
125
|
</div>
|
|
91
126
|
<h1>{heading}</h1>
|
|
127
|
+
{inLine && <p class="pc-manage-wait">{inLine}</p>}
|
|
92
128
|
<p class="pc-manage-lede">{lede}</p>
|
|
93
129
|
{error && <p class="pc-bk-error">{error}</p>}
|
|
94
130
|
<dl class="pc-manage-card">
|
|
@@ -96,7 +132,7 @@ const googleUrl = `https://calendar.google.com/calendar/render?${new URLSearchPa
|
|
|
96
132
|
<div><dt>{w.when}</dt><dd>{when}</dd></div>
|
|
97
133
|
{location && <div><dt>{w.where}</dt><dd>{location}</dd></div>}
|
|
98
134
|
{current.seats > 1 && <div><dt>{w.seatsField}</dt><dd>{current.seats}</dd></div>}
|
|
99
|
-
<div><dt>{w.status}</dt><dd class={`pc-manage-status is-${current.status}`}>{
|
|
135
|
+
<div><dt>{w.status}</dt><dd class={`pc-manage-status is-${current.status}`}>{statusLabel}</dd></div>
|
|
100
136
|
<div><dt>{w.reference}</dt><dd class="mono">{current.id.slice(0, 8).toUpperCase()}</dd></div>
|
|
101
137
|
</dl>
|
|
102
138
|
{active && upcoming && (
|
|
@@ -111,13 +147,19 @@ const googleUrl = `https://calendar.google.com/calendar/render?${new URLSearchPa
|
|
|
111
147
|
</div>
|
|
112
148
|
</details>
|
|
113
149
|
)}
|
|
150
|
+
{can.confirm && (
|
|
151
|
+
<form method="post">
|
|
152
|
+
<input type="hidden" name="op" value="confirm" />
|
|
153
|
+
<button class="pc-manage-btn is-primary" type="submit">{w.confirmSpot}</button>
|
|
154
|
+
</form>
|
|
155
|
+
)}
|
|
114
156
|
{resource && link && can.reschedule && (
|
|
115
157
|
<a class="pc-manage-btn" href={`${bookingLinkUrl(resource.slug, link.slug)}?reschedule=${encodeURIComponent(current.manageToken)}`}>{bw.reschedule}</a>
|
|
116
158
|
)}
|
|
117
159
|
{can.cancel && (
|
|
118
|
-
<form method="post" onsubmit={
|
|
160
|
+
<form method="post" onsubmit={cancelAsk}>
|
|
119
161
|
<input type="hidden" name="op" value="cancel" />
|
|
120
|
-
<button class="pc-manage-btn is-danger" type="submit">{
|
|
162
|
+
<button class="pc-manage-btn is-danger" type="submit">{cancelLabel}</button>
|
|
121
163
|
</form>
|
|
122
164
|
)}
|
|
123
165
|
</div>
|
|
@@ -22,7 +22,7 @@ import { CALENDAR, eventUrl, withoutPrivateFields } from '../../lib/calendar/con
|
|
|
22
22
|
import { eventIsPublic, featuredOccurrence, rsvpDeadline } from '../../lib/calendar/events';
|
|
23
23
|
import { eventJsonLd } from '../../lib/calendar/seo';
|
|
24
24
|
import { upcomingEvents } from '../../lib/calendar/cards';
|
|
25
|
-
import { seatsTaken, BookingError } from '../../lib/booking/service';
|
|
25
|
+
import { seatsTaken, waitlistOn, BookingError } from '../../lib/booking/service';
|
|
26
26
|
import { manageUrl } from '../../lib/calendar/config';
|
|
27
27
|
import { waitUntilFrom } from '../../lib/webhooks';
|
|
28
28
|
|
|
@@ -77,9 +77,10 @@ const formHtml = hiddenFields(await formVersion(definition), requestId) + `<inpu
|
|
|
77
77
|
const taken = event.rsvp.enabled ? await seatsTaken(repo, event.id, occurrence.start) : 0;
|
|
78
78
|
// the event's RSVP deadline for this date (null: open until it ends)
|
|
79
79
|
const closesAt = event.rsvp.enabled ? rsvpDeadline(occurrence, settings) : null;
|
|
80
|
+
// waitlist: a full date offers "Join the waitlist" (the same form, posting waitlist=1)
|
|
80
81
|
const rsvp = event.rsvp.enabled
|
|
81
82
|
? { going: taken, left: event.rsvp.capacity ? Math.max(0, event.rsvp.capacity - taken) : null, maxSeats: event.rsvp.maxSeats, error: rsvpError, duplicate: rsvpDuplicate, formHtml, fieldsHtml: renderFields(definition, formValues, 'event-registration'), values: coreValues,
|
|
82
|
-
open: occurrence.end > now && !occurrence.cancelled && !(closesAt !== null && closesAt <= now), closesAt }
|
|
83
|
+
open: occurrence.end > now && !occurrence.cancelled && !(closesAt !== null && closesAt <= now), closesAt, waitlist: waitlistOn(event) }
|
|
83
84
|
: null;
|
|
84
85
|
const calendars = event.calendarIds.map((id) => calMap.get(id)).filter((c): c is NonNullable<typeof c> => Boolean(c) && c!.visibility !== 'private');
|
|
85
86
|
const html = highlightCodeHtml(await expandDynamicBlocks(event.bodyHtml, repo));
|
package/src/styles/admin.css
CHANGED
|
@@ -1476,7 +1476,9 @@ a.bk-row:hover, .bk-row.is-sel { background: var(--hover); }
|
|
|
1476
1476
|
.bk-st { font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
1477
1477
|
.bk-st.is-confirmed { color: var(--green); }
|
|
1478
1478
|
.bk-st.is-pending { color: var(--amber); }
|
|
1479
|
-
.bk-st.is-cancelled, .bk-st.is-declined, .bk-st.is-block { color: var(--muted); }
|
|
1479
|
+
.bk-st.is-cancelled, .bk-st.is-declined, .bk-st.is-block, .bk-st.is-expired { color: var(--muted); }
|
|
1480
|
+
.bk-st.is-waitlisted { color: var(--text-2); }
|
|
1481
|
+
.bk-st.is-offered { color: var(--accent); }
|
|
1480
1482
|
.bk-panel { padding: 16px 18px; position: sticky; top: 64px; }
|
|
1481
1483
|
.bk-panel-head { display: flex; justify-content: space-between; align-items: center; }
|
|
1482
1484
|
.bk-panel h2 { font-size: 18px; margin: 6px 0 2px; }
|
package/src/styles/site.css
CHANGED
|
@@ -1119,7 +1119,9 @@ li::marker { color: var(--muted); }
|
|
|
1119
1119
|
.pc-manage-icon svg { width: 26px; height: 26px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
|
|
1120
1120
|
.pc-manage-icon.is-confirmed { background: color-mix(in srgb, var(--success) 14%, var(--panel)); color: var(--success); }
|
|
1121
1121
|
.pc-manage-icon.is-pending { background: color-mix(in srgb, var(--warning) 14%, var(--panel)); color: var(--warning); }
|
|
1122
|
-
.pc-manage-icon.is-cancelled, .pc-manage-icon.is-declined { background: var(--soft); color: var(--muted); }
|
|
1122
|
+
.pc-manage-icon.is-cancelled, .pc-manage-icon.is-declined, .pc-manage-icon.is-expired { background: var(--soft); color: var(--muted); }
|
|
1123
|
+
.pc-manage-icon.is-waitlisted { background: var(--soft); color: var(--ink); }
|
|
1124
|
+
.pc-manage-icon.is-offered { background: color-mix(in srgb, var(--success) 14%, var(--panel)); color: var(--success); }
|
|
1123
1125
|
.pc-manage h1 { font-size: 30px; margin: 0 0 8px; }
|
|
1124
1126
|
.pc-manage-lede { color: var(--muted); margin: 0 0 18px; }
|
|
1125
1127
|
.pc-manage-card { text-align: left; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); margin: 0; padding: 4px 18px; box-shadow: var(--sh-1); }
|
|
@@ -1130,12 +1132,17 @@ li::marker { color: var(--muted); }
|
|
|
1130
1132
|
.pc-manage-card .mono { font-family: var(--font-mono); font-size: 13px; }
|
|
1131
1133
|
.pc-manage-status.is-confirmed { color: var(--success); font-weight: 600; }
|
|
1132
1134
|
.pc-manage-status.is-pending { color: var(--warning); font-weight: 600; }
|
|
1135
|
+
.pc-manage-status.is-waitlisted { font-weight: 600; }
|
|
1136
|
+
.pc-manage-status.is-offered { color: var(--success); font-weight: 600; }
|
|
1137
|
+
.pc-manage-wait { font-weight: 600; margin: 0 0 6px; }
|
|
1133
1138
|
.pc-manage-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 16px; }
|
|
1134
1139
|
.pc-manage-actions .pc-ev-add { margin: 0; }
|
|
1135
1140
|
.pc-manage-actions .pc-ev-add > summary { width: auto; }
|
|
1136
1141
|
.pc-manage-btn { display: inline-flex; align-items: center; height: 40px; padding: 0 14px; border: 1px solid var(--line-2); border-radius: 12px; background: var(--panel); font: 550 14px/1.2 var(--font-body); color: var(--ink); text-decoration: none; cursor: pointer; }
|
|
1137
1142
|
.pc-manage-btn:hover { background: var(--soft); }
|
|
1138
1143
|
.pc-manage-btn.is-danger { color: var(--danger); }
|
|
1144
|
+
.pc-manage-btn.is-primary { background: var(--btn); color: var(--btn-ink); border-color: transparent; }
|
|
1145
|
+
.pc-manage-btn.is-primary:hover { background: var(--btn); filter: brightness(1.08); }
|
|
1139
1146
|
.pc-manage-note { font-size: 12.5px; color: var(--muted); margin-top: 22px; }
|
|
1140
1147
|
|
|
1141
1148
|
/* RSVP on the event page */
|
|
@@ -1149,6 +1156,8 @@ li::marker { color: var(--muted); }
|
|
|
1149
1156
|
.pc-ev-full { font-size: 14px; font-weight: 600; color: var(--muted); }
|
|
1150
1157
|
.pc-ev-closes { font-size: 13.5px; color: var(--muted); margin: 0 0 10px; }
|
|
1151
1158
|
.pc-ev-full + .pc-ev-closes { margin-top: 4px; }
|
|
1159
|
+
.pc-ev-full.is-waitlist { color: var(--ink); margin: 0 0 2px; }
|
|
1160
|
+
.pc-ev-waitlist { font-size: 13.5px; color: var(--muted); margin: 0 0 10px; }
|
|
1152
1161
|
.pc-ev-notice { font-size: 14px; line-height: 1.5; background: var(--soft); border-radius: var(--r-sm); padding: 9px 12px; margin: 0 0 14px; }
|
|
1153
1162
|
|
|
1154
1163
|
@media (max-width: 860px) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* Test phones panel and "Send test to my phone" (Admin → Announcements and
|
|
2
|
+
Admin → Mobile app). Scoped under .tp; admin.css tokens only. */
|
|
3
|
+
.tp { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; scroll-margin-top: 72px; }
|
|
4
|
+
.tp > * { align-self: stretch; }
|
|
5
|
+
.tp > .btn { align-self: flex-start; }
|
|
6
|
+
.tp-head h2 { font-size: 14.5px; margin: 0; letter-spacing: -0.01em; }
|
|
7
|
+
.tp-head p { margin: 2px 0 0; color: var(--muted); font-size: 13px; max-width: 64ch; }
|
|
8
|
+
.tp-note { margin: 0; max-width: 64ch; }
|
|
9
|
+
.tp-intro { margin: 0 0 14px; max-width: 64ch; }
|
|
10
|
+
.tp-empty { margin: 0; color: var(--text-2); font-size: 13px; }
|
|
11
|
+
.tp-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
|
|
12
|
+
.tp-list li { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--line); }
|
|
13
|
+
.tp-phone { display: flex; flex-direction: column; min-width: 0; }
|
|
14
|
+
.tp-phone b { font-size: 13.5px; }
|
|
15
|
+
.tp-phone small { color: var(--muted); font-size: 12px; }
|
|
16
|
+
.tp-code { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 4px 16px; background: var(--panel-2); border-radius: var(--r-md); padding: 12px 14px; }
|
|
17
|
+
.tp-code p { margin: 0; color: var(--text-2); font-size: 13px; max-width: 52ch; }
|
|
18
|
+
.tp-code .btn { grid-column: 2; justify-self: start; margin-top: 4px; }
|
|
19
|
+
.tp-digits { grid-row: span 2; font: 700 30px/1.1 ui-monospace, 'SF Mono', Menlo, monospace; letter-spacing: 0.18em; font-variant-numeric: tabular-nums; }
|
|
20
|
+
.tp-flash .flash { margin: 0; }
|
|
21
|
+
/* an always-present live region; no gap while empty */
|
|
22
|
+
.tp > .tp-flash:empty { margin-top: -12px; }
|
|
23
|
+
.tp-send > .tp-flash:empty { margin-top: -8px; }
|
|
24
|
+
.tp-send { display: flex; flex-direction: column; align-items: center; gap: 8px; align-self: stretch; }
|
|
25
|
+
.tp-send .tp-flash { align-self: stretch; text-align: center; }
|
|
26
|
+
@media (max-width: 560px) {
|
|
27
|
+
.tp-code { grid-template-columns: minmax(0, 1fr); }
|
|
28
|
+
.tp-digits { grid-row: auto; }
|
|
29
|
+
.tp-code .btn { grid-column: 1; }
|
|
30
|
+
}
|
package/src/theme-sdk/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface HomeProps { settings: Settings; page: Page | null; posts: Post[
|
|
|
11
11
|
export interface PageProps { settings: Settings; page: Page; html: string }
|
|
12
12
|
export interface PostProps { settings: Settings; post: Post; html: string; authors: User[]; related: Post[] }
|
|
13
13
|
import type { Occurrence } from '../lib/calendar/events';
|
|
14
|
-
export interface EventProps { settings: Settings; event: CalEvent; html: string; calendars: Calendar[]; origin: string; occurrence: Occurrence; upcoming: Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days'>[]; preview?: boolean; rsvp?: {going:number;left:number|null;maxSeats:number;error:string;open:boolean;closesAt?:number|null;duplicate?:boolean}|null }
|
|
14
|
+
export interface EventProps { settings: Settings; event: CalEvent; html: string; calendars: Calendar[]; origin: string; occurrence: Occurrence; upcoming: Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days'>[]; preview?: boolean; rsvp?: {going:number;left:number|null;maxSeats:number;error:string;open:boolean;closesAt?:number|null;duplicate?:boolean;waitlist?:boolean}|null }
|
|
15
15
|
/**
|
|
16
16
|
* Props for a coded section. `data` holds resolved values: link fields arrive
|
|
17
17
|
* as LinkValue, lists as row arrays, source fields as fresh item arrays
|