@acms-subir/core 0.3.16 → 0.3.18
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 +16 -0
- package/cli/common.mjs +2 -2
- package/cli/main.mjs +10 -3
- package/cli/migrations.generated.mjs +71 -1
- package/cli/remote.mjs +1 -1
- package/cli/sync.mjs +876 -0
- package/docs/environment-sync.md +302 -0
- package/integration.mjs +2 -0
- package/package.json +1 -1
- package/section-library/upcoming-events/UpcomingEvents.astro +26 -17
- package/section-library/upcoming-events/section.json +41 -0
- package/src/actions/index.ts +5 -1
- package/src/components/CalendarFallbackImage.tsx +18 -0
- package/src/components/DesignAccess.tsx +3 -0
- package/src/components/EventPage.astro +4 -2
- package/src/components/LinksManager.tsx +86 -55
- package/src/components/ThemeSectionsEditor.tsx +13 -1
- package/src/layouts/Site.astro +1 -0
- package/src/lib/calendar/cards.ts +4 -2
- package/src/lib/calendar/images.ts +17 -0
- package/src/lib/editor/schemas.ts +2 -2
- package/src/lib/editor/site-data.ts +5 -3
- package/src/lib/links-avatar.ts +10 -0
- package/src/lib/mobile/links.ts +4 -0
- package/src/lib/mobile/schemas.ts +2 -2
- package/src/lib/repo/migrations.ts +26 -0
- package/src/lib/repo/sql.ts +8 -3
- package/src/lib/repo/types.ts +3 -1
- package/src/lib/schema.ts +14 -0
- package/src/lib/sync/auth.ts +24 -0
- package/src/lib/sync/catalog.ts +250 -0
- package/src/lib/sync/migration.ts +51 -0
- package/src/lib/sync/protocol.mjs +125 -0
- package/src/lib/sync/service.ts +994 -0
- package/src/lib/sync/store.ts +349 -0
- package/src/pages/admin/calendar/settings.astro +21 -2
- package/src/pages/admin/integrations.astro +2 -0
- package/src/pages/admin/links.astro +1 -1
- package/src/pages/api/agent/docs.ts +2 -0
- package/src/pages/api/design/keys.ts +7 -3
- package/src/pages/api/sync/index.ts +114 -0
- package/src/pages/api/sync/media.ts +90 -0
- package/src/pages/links.astro +13 -2
- package/src/styles/admin.css +27 -36
- package/src/styles/page-transitions.css +27 -0
- package/src/styles/site.css +3 -1
- package/src/styles/theme-editor.css +47 -12
- package/src/theme-sdk/index.ts +1 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Selective environment sync
|
|
2
|
+
|
|
3
|
+
Use `acms sync` to transfer selected native ACMS content between two running
|
|
4
|
+
sites. Both directions work: local → production, production → local, or staging
|
|
5
|
+
→ production. Both sites must run a version that includes sync protocol 1.
|
|
6
|
+
Run `acms sync --help` for the command reference or `acms docs sync` to read
|
|
7
|
+
this guide from an installed package.
|
|
8
|
+
|
|
9
|
+
This transfers content and portable configuration, not application code,
|
|
10
|
+
database schemas, environment variables, or an entire database. Deploy theme
|
|
11
|
+
code, collection definitions and enabled modules through the application's
|
|
12
|
+
normal deployment process first. Secrets remain in each environment's own
|
|
13
|
+
Infisical project/environment/folder. Backups remain the disaster recovery tool.
|
|
14
|
+
|
|
15
|
+
## Connect and choose a direction
|
|
16
|
+
|
|
17
|
+
An owner creates scoped keys under **Design → Access**. The existing design-key
|
|
18
|
+
screen also offers these permissions:
|
|
19
|
+
|
|
20
|
+
| Scope | Permission |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `sync:read` | Read the portable inventory, drafts, media and transfer history |
|
|
23
|
+
| `sync:write` | Create plans, upload media and apply draft transfers; also needs `sync:read` |
|
|
24
|
+
| `sync:publish` | Apply live, publication and rollback plans; also needs both scopes above |
|
|
25
|
+
|
|
26
|
+
Only an owner can issue sync keys. Existing design, agent and booking keys do
|
|
27
|
+
not implicitly grant sync access. For a one-way transfer, the source needs
|
|
28
|
+
read access and the destination needs read/write (plus publish for live work).
|
|
29
|
+
Reverse transfers need the corresponding permissions in the other direction.
|
|
30
|
+
|
|
31
|
+
Supply the keys privately through the named environment variables, preferably
|
|
32
|
+
using the CLI's existing Infisical runtime-injection setup. Do not place tokens
|
|
33
|
+
in command arguments, plan files, source control, or chat.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
acms site connect http://localhost:4321 --site local --token-env ACMS_LOCAL_TOKEN
|
|
37
|
+
acms site connect https://example.com --site production --token-env ACMS_PRODUCTION_TOKEN
|
|
38
|
+
acms sync init --from local --to production
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`site connect` is an existing command. It stores the URL and environment-variable
|
|
42
|
+
name, not the credential. Remote sites require HTTPS; loopback HTTP is allowed.
|
|
43
|
+
Sync accesses each site's authenticated HTTP API, so the CLI needs network
|
|
44
|
+
access to both sites. It does not open a production database directly.
|
|
45
|
+
|
|
46
|
+
`init` selects a direction and resumes that pair's saved selection. It does not
|
|
47
|
+
transfer content. Switching directions keeps separate selections and baselines:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
acms sync init --from production --to local
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The CLI binds the pair to installation identities as well as URLs. A changed
|
|
54
|
+
profile URL or replaced installation requires a fresh `init`. Two aliases for
|
|
55
|
+
the same installation cannot be synchronized to each other. Cloning the entire
|
|
56
|
+
database also clones its installation identity; use independent installations
|
|
57
|
+
and content transfers rather than using a database clone as a second sync site.
|
|
58
|
+
|
|
59
|
+
## Inspect and select
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
acms sync status
|
|
63
|
+
acms sync status --type pages
|
|
64
|
+
acms sync status --selected
|
|
65
|
+
acms sync status --conflicts
|
|
66
|
+
acms sync list
|
|
67
|
+
acms sync list --type events --calendar community
|
|
68
|
+
acms sync list --search welcome
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Status distinguishes create, update, skip, preserve and conflict. The inventory
|
|
72
|
+
shows references and stable keys. Use the full key when names are ambiguous.
|
|
73
|
+
Posts accept `post:<collection>/<slug>`; `post:<slug>` also works when unique.
|
|
74
|
+
Forms and places have names rather than slugs, so quote references containing
|
|
75
|
+
spaces, such as `'form:Contact us'`.
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
acms sync add page:home page:about
|
|
79
|
+
acms sync add post:posts/welcome
|
|
80
|
+
acms sync add event:open-house
|
|
81
|
+
acms sync add resource:meeting-room
|
|
82
|
+
acms sync add 'form:Contact us'
|
|
83
|
+
acms sync add calendar:community --include-events
|
|
84
|
+
acms sync add --type navigation
|
|
85
|
+
acms sync add --type booking-config
|
|
86
|
+
acms sync add --all
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`add --all` selects eligible changed source items, including conflicts for
|
|
90
|
+
review. It never selects customer bookings, submissions, account credentials,
|
|
91
|
+
secrets or deletions. `add --type` selects changed items in that category.
|
|
92
|
+
Selecting a calendar alone does not select all its events; `--include-events`
|
|
93
|
+
does. Selected events include the calendars and other referenced objects they
|
|
94
|
+
need. Pages include referenced media and native ID-based dependencies. Links
|
|
95
|
+
to arbitrary URLs are not crawled or copied.
|
|
96
|
+
|
|
97
|
+
`acms sync select` is a terminal picker: it displays numbered items and accepts
|
|
98
|
+
a comma-separated selection. Blank input preserves the selection. Explicit
|
|
99
|
+
commands provide the equivalent workflow for scripts; interactive mode cannot
|
|
100
|
+
be combined with `--json`.
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
acms sync remove page:test-landing
|
|
104
|
+
acms sync remove --type events
|
|
105
|
+
acms sync clear
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Removing items only changes the pending selection. An explicitly removed
|
|
109
|
+
dependency blocks its parent rather than silently reselecting it. Add the
|
|
110
|
+
dependency again, preserve an existing destination version, or remove the parent.
|
|
111
|
+
`clear` clears selections, exclusions and conflict decisions, not history or
|
|
112
|
+
the last successful synchronization baseline.
|
|
113
|
+
|
|
114
|
+
## Review differences and conflicts
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
acms sync diff
|
|
118
|
+
acms sync diff page:home
|
|
119
|
+
acms sync diff --selected
|
|
120
|
+
acms sync resolve page:home --use source
|
|
121
|
+
acms sync resolve page:home --use destination
|
|
122
|
+
acms sync resolve page:home --interactive
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Diff shows actual before/after field values. The source inventory includes
|
|
126
|
+
saved pending drafts as the editable source version; it does not capture
|
|
127
|
+
unsaved browser edits. Live versus draft behavior on the destination is chosen
|
|
128
|
+
when planning.
|
|
129
|
+
|
|
130
|
+
The CLI remembers the last successfully applied source and destination hashes:
|
|
131
|
+
|
|
132
|
+
| Comparison | Default action |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| Same content on both sites | Skip |
|
|
135
|
+
| Only the source changed since the baseline | Update |
|
|
136
|
+
| Only the destination changed | Preserve destination |
|
|
137
|
+
| Both changed differently | Conflict |
|
|
138
|
+
| Existing item with no established baseline | Review before overwrite |
|
|
139
|
+
| Previously transferred item deleted on destination | Conflict |
|
|
140
|
+
| Item exists only on destination | Preserve |
|
|
141
|
+
|
|
142
|
+
Matching uses recorded identities first. A unique natural-key candidate
|
|
143
|
+
(such as a collection and slug) can be proposed for the initial match, but a
|
|
144
|
+
differing existing item requires explicit resolution. Ambiguous names are
|
|
145
|
+
blocked; fix ambiguity on the destination first. Slug changes keep an established
|
|
146
|
+
identity. Sync does not automatically create redirects for renamed URLs;
|
|
147
|
+
select the appropriate redirect records as well.
|
|
148
|
+
|
|
149
|
+
`--use source` approves the source version; `--use destination` leaves the
|
|
150
|
+
destination content intact. If a previously transferred item was deleted on
|
|
151
|
+
the destination, `--use source` explicitly approves recreating it. Required
|
|
152
|
+
preserved dependencies are linked without overwriting their data. Interactive
|
|
153
|
+
resolution chooses whole top-level fields;
|
|
154
|
+
it is not a text-editor or automatic rich-text/array merge. Conflict decisions
|
|
155
|
+
are tied to the reviewed source and destination versions and expire when either
|
|
156
|
+
changes. There is no `--force` bypass.
|
|
157
|
+
|
|
158
|
+
## Plan, apply and publish
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
acms sync plan
|
|
162
|
+
acms sync plan --mode draft --out deployment-plan.json
|
|
163
|
+
acms sync plan --mode live
|
|
164
|
+
acms sync show <plan-id>
|
|
165
|
+
acms sync apply --plan <plan-id>
|
|
166
|
+
acms sync apply --file deployment-plan.json
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Draft is the default. Pages, posts, events, forms and design support draft
|
|
170
|
+
transfers. New draft-capable records are created unpublished; existing live
|
|
171
|
+
content retains its live version while changes are staged. New required media
|
|
172
|
+
and tag records are added to their libraries. Updating existing media/tag
|
|
173
|
+
metadata and transferring navigation, calendars, resources, booking links,
|
|
174
|
+
places, redirects or portable settings requires an explicit live plan. A
|
|
175
|
+
published form's definition can be staged; its name/kind/purpose changes need
|
|
176
|
+
a live plan. An event with room holds must have those configured on the
|
|
177
|
+
destination after transfer.
|
|
178
|
+
|
|
179
|
+
Plans list selected operations, dependencies, field differences and whether
|
|
180
|
+
changes affect drafts or live content. They are stored on the destination and
|
|
181
|
+
bound to its installation ID, revision and exact payload. Planning changes
|
|
182
|
+
transfer metadata only, never site content. Exported files include a human-
|
|
183
|
+
readable copy, source/destination binding and a digest; editing their payload
|
|
184
|
+
does not alter the immutable server plan. Create a new plan to change it.
|
|
185
|
+
|
|
186
|
+
Apply displays the plan and asks for confirmation. Automation uses:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
acms sync apply --plan <plan-id> --yes --json
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`--yes` skips only the confirmation. It cannot bypass scopes, dependencies,
|
|
193
|
+
schema/theme compatibility, conflicts or stale revisions. Apply does not need
|
|
194
|
+
the current selection to match the saved plan; it applies that exact plan.
|
|
195
|
+
|
|
196
|
+
The database checks the reviewed revision inside the same transaction as all
|
|
197
|
+
record writes, identity links, search-index changes and the completed-run
|
|
198
|
+
receipt. Content/draft changes through other APIs also advance the revision.
|
|
199
|
+
The first implementation uses a conservative **site-wide revision**: unrelated
|
|
200
|
+
content edits or booking/submission activity after planning can require a new
|
|
201
|
+
plan too. This favors rejecting stale plans over overwriting concurrent work.
|
|
202
|
+
|
|
203
|
+
Media is uploaded before that transaction to immutable checksum-based keys.
|
|
204
|
+
Already present verified objects are skipped. Failed uploads or a rejected
|
|
205
|
+
database transaction can leave unused files, but do not partially publish the
|
|
206
|
+
database changes. Sync does not delete these files automatically.
|
|
207
|
+
|
|
208
|
+
If a request or terminal is interrupted, rerun the same apply command. A
|
|
209
|
+
completed server receipt is returned without repeating writes; otherwise
|
|
210
|
+
verified files are reused and the database transaction can be retried if the
|
|
211
|
+
reviewed revision still matches. There is no blind retry of stale writes.
|
|
212
|
+
|
|
213
|
+
After reviewing staged content in the site's editors/previews:
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
acms sync plan --publish <run-id>
|
|
217
|
+
acms sync apply --plan <publish-plan-id>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
This publishes only the draft-capable items in that run. It rejects later edits
|
|
221
|
+
to those items instead of implicitly publishing somebody else's changes.
|
|
222
|
+
Design/layout changes conservatively share a settings guard.
|
|
223
|
+
|
|
224
|
+
## History and rollback
|
|
225
|
+
|
|
226
|
+
```sh
|
|
227
|
+
acms sync history
|
|
228
|
+
acms sync show <run-id>
|
|
229
|
+
acms sync plan --rollback <run-id>
|
|
230
|
+
acms sync apply --plan <rollback-plan-id>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
History lists the latest 100 plans and runs. An older known ID can still be
|
|
234
|
+
read with `show`. Rollback creates another reviewable live plan. It restores
|
|
235
|
+
previous live/draft values for changed items and can delete items that the run
|
|
236
|
+
created, provided they have not subsequently changed or acquired dependents or
|
|
237
|
+
production activity. Media bytes are retained, even if their newly created
|
|
238
|
+
library record is removed. Later design/settings edits may conservatively block
|
|
239
|
+
rollback of a page layout. Rollback does not undo delivered emails, webhook
|
|
240
|
+
deliveries, external effects or code deployments.
|
|
241
|
+
|
|
242
|
+
## Transfer coverage
|
|
243
|
+
|
|
244
|
+
| Category | Included | Environment-specific data preserved |
|
|
245
|
+
| --- | --- | --- |
|
|
246
|
+
| `pages` | Native rich text, legacy blocks, translations, layout, SEO and routing fields | Unselected pages |
|
|
247
|
+
| `posts` | Native content, translations, metadata and byline references | Staff credentials and account authority |
|
|
248
|
+
| `design` | Theme selection, design overrides and branding images | Theme source code; page layouts travel with pages |
|
|
249
|
+
| `navigation` | Individual navigation records | Other destination navigation records |
|
|
250
|
+
| `media` | Metadata, originals and variants, with checksums | Unselected assets |
|
|
251
|
+
| `redirects` | Selected redirect records | Other destination redirects |
|
|
252
|
+
| `calendars` | Calendar definitions | Feed tokens and source URLs |
|
|
253
|
+
| `events` | Native events and registration-form definitions | Registrations and notification history |
|
|
254
|
+
| `resources` | Resource configuration | Private busy-feed URLs, notification recipients and feed tokens |
|
|
255
|
+
| `booking-config` | Booking-link definitions and calendar/booking defaults | Reservations, retry ledgers and private management links |
|
|
256
|
+
| `forms` | Definitions/templates, native drafts | Submitted responses |
|
|
257
|
+
| `site-settings` | Title, tagline, footer text, locale, timezone, announcement | Canonical site URL, integration/analytics configuration, injected code, secrets |
|
|
258
|
+
| `places` | Saved place definitions | Other saved places |
|
|
259
|
+
| `tags` | Tag metadata | Other tag records |
|
|
260
|
+
|
|
261
|
+
Byline dependencies map to existing destination profiles. Sync never creates
|
|
262
|
+
or edits user accounts; provision the intended profile separately and preserve
|
|
263
|
+
its destination version when appropriate. Feed calendars must already exist
|
|
264
|
+
on the destination with their own configured URL. Calendar and booking modules,
|
|
265
|
+
post collections, theme code and section definitions must be deployed before
|
|
266
|
+
their associated content can be applied.
|
|
267
|
+
|
|
268
|
+
Events with existing destination reservations and resources with active
|
|
269
|
+
reservations cannot be overwritten through sync. Use the destination editor
|
|
270
|
+
for those operational changes. Bookings and submissions created after review
|
|
271
|
+
invalidate the plan; rollback also checks for activity before deleting items.
|
|
272
|
+
|
|
273
|
+
## Automation, storage and limits
|
|
274
|
+
|
|
275
|
+
- `--json` returns the existing `acms.cli/1` envelope; exit 4 denotes conflicts,
|
|
276
|
+
3 authentication/permission failures, 5 network/server failures, and 2 invalid
|
|
277
|
+
usage/input. Diagnostics and interactive prompts use stderr.
|
|
278
|
+
- `--dry-run` on selection commands does not save selection changes. On a
|
|
279
|
+
normal plan it asks the server to validate without storing a plan; on apply
|
|
280
|
+
it displays the saved plan without uploading or applying. Publication and
|
|
281
|
+
rollback plans are also validated without storing them in dry-run mode.
|
|
282
|
+
- `--cwd <project>` chooses the project workspace. Selection, baselines and
|
|
283
|
+
exported-plan references are stored under
|
|
284
|
+
`${ACMS_CONFIG_DIR:-~/.config/acms}/sync/<workspace-hash>/`, with private file
|
|
285
|
+
permissions. They are not committed into the project. Exported plan files
|
|
286
|
+
contain unpublished content and should be kept out of source control.
|
|
287
|
+
- A workspace lock prevents simultaneous CLI commands from losing selections.
|
|
288
|
+
After a crashed CLI process, verify that the PID in the reported lock file
|
|
289
|
+
is no longer running before removing that file.
|
|
290
|
+
- Protocol 1 supports up to 2,000 inventory items, an 8 MiB inventory, 200
|
|
291
|
+
selected operations per plan and 16 MiB per media object. Oversized inputs
|
|
292
|
+
fail explicitly; split selections where possible. The inventory is bounded
|
|
293
|
+
rather than silently truncated. This version hashes media during inventory
|
|
294
|
+
reads; large media libraries may need a future paginated/cached inventory.
|
|
295
|
+
Hosting-platform request, subrequest and CPU limits can impose lower practical
|
|
296
|
+
limits; local D1 tests do not establish a live Cloudflare account's limits.
|
|
297
|
+
- Sync history lives on the destination; it is not part of portable content
|
|
298
|
+
backups. Sync does not promise a cross-resource transaction with object
|
|
299
|
+
storage. Keep normal off-site backups and test disaster recovery separately.
|
|
300
|
+
|
|
301
|
+
There is no automatic deletion propagation, background two-way synchronization,
|
|
302
|
+
stash command, automatic text merge, or full database transfer.
|
package/integration.mjs
CHANGED
|
@@ -87,6 +87,8 @@ const ROUTES = [
|
|
|
87
87
|
['/api/ai/clean', 'api/ai/clean.ts'],
|
|
88
88
|
['/api/ai/draft', 'api/ai/draft.ts'],
|
|
89
89
|
['/api/design', 'api/design/index.ts'],
|
|
90
|
+
['/api/sync', 'api/sync/index.ts'],
|
|
91
|
+
['/api/sync/media', 'api/sync/media.ts'],
|
|
90
92
|
['/api/design/keys', 'api/design/keys.ts'],
|
|
91
93
|
['/api/design/capabilities', 'api/design/capabilities.ts'],
|
|
92
94
|
['/api/design/media', 'api/design/media.ts'],
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { eventImage } from '@acms-subir/core/theme';
|
|
2
3
|
import type { SectionProps, EventItem, LinkValue } from '@acms-subir/core/theme';
|
|
3
|
-
interface Data { heading: string; intro: string; events: EventItem[]; more: LinkValue; empty: string }
|
|
4
|
-
const { data, variant } = Astro.props as SectionProps<Data>;
|
|
4
|
+
interface Data { heading: string; intro: string; events: EventItem[]; more: LinkValue; empty: string; fallbackImage: string; showImages: boolean; imageRatio: string; imageFit: string; showLocation: boolean; showSummary: boolean }
|
|
5
|
+
const { data, variant, settings } = Astro.props as SectionProps<Data>;
|
|
6
|
+
const ratio = ({ landscape: '16 / 9', square: '1', portrait: '3 / 4' } as Record<string, string>)[data.imageRatio] || '16 / 9';
|
|
7
|
+
const fit = data.imageFit === 'contain' ? 'contain' : 'cover';
|
|
5
8
|
---
|
|
6
|
-
<div class:list={['upev', `is-${variant}`]}>
|
|
9
|
+
<div style={`--event-image-ratio:${ratio};--event-image-fit:${fit}`} class:list={['upev', `is-${variant}`]}>
|
|
7
10
|
<header class="upev-head">
|
|
8
11
|
<div>
|
|
9
12
|
<h2>{data.heading}</h2>
|
|
@@ -13,18 +16,22 @@ const { data, variant } = Astro.props as SectionProps<Data>;
|
|
|
13
16
|
</header>
|
|
14
17
|
{data.events.length === 0 ? <p class="upev-empty">{data.empty}</p> : (
|
|
15
18
|
<ol class="upev-list">
|
|
16
|
-
{data.events.map((e) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
{data.events.map((e) => {
|
|
20
|
+
const image = eventImage({ cover: e.image, coverAlt: e.imageAlt }, settings, data.fallbackImage);
|
|
21
|
+
return (
|
|
22
|
+
<li class:list={['upev-item', { 'is-cancelled': e.cancelled }]}>
|
|
23
|
+
{data.showImages !== false && image.src
|
|
24
|
+
? <a class="upev-media" href={e.url} tabindex="-1" aria-hidden="true"><img src={image.src} alt="" loading="lazy" /></a>
|
|
25
|
+
: <span class="upev-date" aria-hidden="true"><b>{e.day}</b>{e.month}</span>}
|
|
26
|
+
<div class="upev-body">
|
|
27
|
+
<span class="upev-when">{e.cancelled ? 'Cancelled · ' : ''}{e.when}</span>
|
|
28
|
+
<h3 class="upev-title"><a href={e.url}>{e.title}</a></h3>
|
|
29
|
+
{data.showSummary && e.summary && <p class="upev-summary">{e.summary}</p>}
|
|
30
|
+
{data.showLocation !== false && e.location && <span class="upev-loc">{e.location}</span>}
|
|
31
|
+
</div>
|
|
32
|
+
</li>
|
|
33
|
+
);
|
|
34
|
+
})}
|
|
28
35
|
</ol>
|
|
29
36
|
)}
|
|
30
37
|
</div>
|
|
@@ -40,8 +47,10 @@ const { data, variant } = Astro.props as SectionProps<Data>;
|
|
|
40
47
|
.upev-item { display: flex; gap: 1rem; align-items: flex-start; }
|
|
41
48
|
.is-cards .upev-item { flex-direction: column; padding: 1rem; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); }
|
|
42
49
|
.is-list .upev-item { padding-bottom: 1rem; border-bottom: 1px solid var(--line); }
|
|
43
|
-
.upev-media { display: block; width: 100%; aspect-ratio:
|
|
44
|
-
.upev-media img { width: 100%; height: 100%; object-fit:
|
|
50
|
+
.upev-media { display: block; width: 100%; aspect-ratio: var(--event-image-ratio); border-radius: var(--r-sm); overflow: hidden; background: var(--soft); }
|
|
51
|
+
.upev-media img { width: 100%; height: 100%; object-fit: var(--event-image-fit); }
|
|
52
|
+
.is-list .upev-media { width: 6rem; flex: 0 0 6rem; }
|
|
53
|
+
.upev-summary { margin: 0.4rem 0 0; color: var(--muted); }
|
|
45
54
|
.upev-date { flex-shrink: 0; display: grid; place-items: center; width: 3.4rem; padding: 0.45rem 0; border-radius: var(--r-sm); background: var(--ink); color: var(--bg); font-size: 0.7rem; font-weight: 600; letter-spacing: 0.06em; line-height: 1.2; }
|
|
46
55
|
.upev-date b { font-size: 1.35rem; letter-spacing: 0; }
|
|
47
56
|
.upev-body { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
|
|
@@ -40,6 +40,47 @@
|
|
|
40
40
|
"label": "Text when nothing is scheduled",
|
|
41
41
|
"type": "text",
|
|
42
42
|
"default": "Nothing scheduled right now. Check back soon."
|
|
43
|
+
},
|
|
44
|
+
"fallbackImage": {
|
|
45
|
+
"label": "Fallback event image",
|
|
46
|
+
"type": "image",
|
|
47
|
+
"default": "",
|
|
48
|
+
"help": "Used only when an event has no image. Leave empty to use the fallback in Calendar settings."
|
|
49
|
+
},
|
|
50
|
+
"showImages": {
|
|
51
|
+
"label": "Show event images",
|
|
52
|
+
"type": "toggle",
|
|
53
|
+
"default": true
|
|
54
|
+
},
|
|
55
|
+
"imageRatio": {
|
|
56
|
+
"label": "Image proportions",
|
|
57
|
+
"type": "select",
|
|
58
|
+
"options": [
|
|
59
|
+
"landscape",
|
|
60
|
+
"square",
|
|
61
|
+
"portrait"
|
|
62
|
+
],
|
|
63
|
+
"default": "landscape"
|
|
64
|
+
},
|
|
65
|
+
"imageFit": {
|
|
66
|
+
"label": "Image fit",
|
|
67
|
+
"type": "select",
|
|
68
|
+
"options": [
|
|
69
|
+
"cover",
|
|
70
|
+
"contain"
|
|
71
|
+
],
|
|
72
|
+
"default": "cover",
|
|
73
|
+
"help": "Cover fills the frame; contain shows the whole image."
|
|
74
|
+
},
|
|
75
|
+
"showLocation": {
|
|
76
|
+
"label": "Show event location",
|
|
77
|
+
"type": "toggle",
|
|
78
|
+
"default": true
|
|
79
|
+
},
|
|
80
|
+
"showSummary": {
|
|
81
|
+
"label": "Show event summary",
|
|
82
|
+
"type": "toggle",
|
|
83
|
+
"default": false
|
|
43
84
|
}
|
|
44
85
|
},
|
|
45
86
|
"category": "Events & times"
|
package/src/actions/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { getMediaStore } from '../lib/media/store';
|
|
|
10
10
|
import { validateRequestSession } from '../lib/auth/request';
|
|
11
11
|
import { accessFor } from '../lib/permissions/access';
|
|
12
12
|
import { deniedMessage, type Permission } from '../lib/permissions/catalog';
|
|
13
|
-
import { DocTranslations, LinkKind, parseConfig, slugify, widgetTypes, type WidgetType } from '../lib/schema';
|
|
13
|
+
import { DocTranslations, LinksAvatarShape, LinksAvatarFit, LinksAvatarScale, LinkKind, parseConfig, slugify, widgetTypes, type WidgetType } from '../lib/schema';
|
|
14
14
|
import { env } from '../lib/env';
|
|
15
15
|
import { CONTENT, collectionUrl } from '../lib/content';
|
|
16
16
|
import { invalidateRedirectCache } from '../middleware';
|
|
@@ -489,6 +489,10 @@ export const server = {
|
|
|
489
489
|
linksName: z.string().optional(),
|
|
490
490
|
linksBio: z.string().optional(),
|
|
491
491
|
linksAvatar: z.string().optional(),
|
|
492
|
+
linksAvatarShape: LinksAvatarShape.optional(),
|
|
493
|
+
linksAvatarFit: LinksAvatarFit.optional(),
|
|
494
|
+
linksAvatarScale: LinksAvatarScale.optional(),
|
|
495
|
+
linksVerified: z.boolean().optional(),
|
|
492
496
|
activeTheme: z.string().optional(),
|
|
493
497
|
designOverrides: z.record(z.any()).optional(),
|
|
494
498
|
siteLogo: z.string().optional(),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { ImagePicker } from './editor/ImagePicker';
|
|
3
|
+
|
|
4
|
+
export default function CalendarFallbackImage({ value, unsplashEnabled }: { value: string; unsplashEnabled: boolean }) {
|
|
5
|
+
const [image, setImage] = useState(value);
|
|
6
|
+
const [open, setOpen] = useState(false);
|
|
7
|
+
return (
|
|
8
|
+
<div>
|
|
9
|
+
<input type="hidden" name="eventFallbackImage" value={image} />
|
|
10
|
+
{image && <img src={image} alt="Selected fallback event image" style={{ display: 'block', width: '100%', maxWidth: 320, aspectRatio: '16 / 9', objectFit: 'cover', borderRadius: 8, marginBottom: 12 }} />}
|
|
11
|
+
<div className="row-actions">
|
|
12
|
+
<button type="button" className="btn" onClick={() => setOpen(true)}>{image ? 'Change fallback image' : 'Choose fallback image'}</button>
|
|
13
|
+
{image && <button type="button" className="btn ghost" onClick={() => setImage('')}>Remove image</button>}
|
|
14
|
+
</div>
|
|
15
|
+
<ImagePicker open={open} unsplashEnabled={unsplashEnabled} onClose={() => setOpen(false)} onSelect={(picked) => { setImage(picked.src); setOpen(false); }} />
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -12,6 +12,9 @@ const SCOPES: { key: string; label: string; hint: string }[] = [
|
|
|
12
12
|
{ key: 'design:publish', label: 'Publish design', hint: 'Makes the draft live for visitors. Only grant this to tools you trust' },
|
|
13
13
|
{ key: 'media:read', label: 'Read media', hint: 'Browse the media library' },
|
|
14
14
|
{ key: 'media:write', label: 'Upload media', hint: 'Add images to the media library' },
|
|
15
|
+
{ key: 'sync:read', label: 'Read environment content', hint: 'Read portable content, media and sync history, including drafts' },
|
|
16
|
+
{ key: 'sync:write', label: 'Stage environment transfers', hint: 'Create plans, upload assets and apply draft transfers; also needs Read environment content' },
|
|
17
|
+
{ key: 'sync:publish', label: 'Apply live environment transfers', hint: 'Publish content and apply reviewed live or rollback plans; also needs both sync permissions above' },
|
|
15
18
|
];
|
|
16
19
|
const DEFAULT_SCOPES = ['design:read', 'design:write-draft'];
|
|
17
20
|
|
|
@@ -9,6 +9,7 @@ import { addToCalendarLinks, eventTimeZone, formatOccurrence, safeColor, type Oc
|
|
|
9
9
|
import { describeRepeat } from '../lib/calendar/rrule';
|
|
10
10
|
import { eventCardHtml, safeHref } from '../lib/calendar/cards';
|
|
11
11
|
import { siteLocale } from '../lib/dates';
|
|
12
|
+
import { eventImage } from '../lib/calendar/images';
|
|
12
13
|
import { directionsUrl, mapAttribution, MAP_HEIGHT, MAP_WIDTH } from '../lib/maps/service';
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
@@ -24,6 +25,7 @@ interface Props {
|
|
|
24
25
|
rsvp?: { going: number; left: number | null; maxSeats: number; error: string; open: boolean; formHtml?: string; fieldsHtml?: string; values?: { name: string; email: string; seats: string } } | null;
|
|
25
26
|
}
|
|
26
27
|
const { event: e, occurrence: o, settings, calendars, html, upcoming, origin, preview = false, rsvp = null } = Astro.props;
|
|
28
|
+
const image = eventImage(e, settings);
|
|
27
29
|
const tz = eventTimeZone(e, settings);
|
|
28
30
|
const locale = siteLocale(settings);
|
|
29
31
|
const f = formatOccurrence(o, tz, locale);
|
|
@@ -61,8 +63,8 @@ const online = /zoom\.us|meet\.google|teams\.microsoft|webex|whereby|jit\.si/i.t
|
|
|
61
63
|
|
|
62
64
|
<article class={`pc-ev-page${e.cancelled ? ' is-cancelled' : ''}`}>
|
|
63
65
|
{preview && <p class="pc-ev-preview">Preview — only you can see this {e.status === 'published' ? 'version' : 'draft'}.</p>}
|
|
64
|
-
{
|
|
65
|
-
<figure class="pc-ev-hero"><img src={
|
|
66
|
+
{image.src && (
|
|
67
|
+
<figure class="pc-ev-hero"><img src={image.src} alt={image.alt} /></figure>
|
|
66
68
|
)}
|
|
67
69
|
<div class="pc-ev-layout">
|
|
68
70
|
<div class="pc-ev-main">
|