@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/cli/common.mjs +2 -2
  3. package/cli/main.mjs +10 -3
  4. package/cli/migrations.generated.mjs +71 -1
  5. package/cli/remote.mjs +1 -1
  6. package/cli/sync.mjs +876 -0
  7. package/docs/environment-sync.md +302 -0
  8. package/integration.mjs +2 -0
  9. package/package.json +1 -1
  10. package/section-library/upcoming-events/UpcomingEvents.astro +26 -17
  11. package/section-library/upcoming-events/section.json +41 -0
  12. package/src/actions/index.ts +5 -1
  13. package/src/components/CalendarFallbackImage.tsx +18 -0
  14. package/src/components/DesignAccess.tsx +3 -0
  15. package/src/components/EventPage.astro +4 -2
  16. package/src/components/LinksManager.tsx +86 -55
  17. package/src/components/ThemeSectionsEditor.tsx +13 -1
  18. package/src/layouts/Site.astro +1 -0
  19. package/src/lib/calendar/cards.ts +4 -2
  20. package/src/lib/calendar/images.ts +17 -0
  21. package/src/lib/editor/schemas.ts +2 -2
  22. package/src/lib/editor/site-data.ts +5 -3
  23. package/src/lib/links-avatar.ts +10 -0
  24. package/src/lib/mobile/links.ts +4 -0
  25. package/src/lib/mobile/schemas.ts +2 -2
  26. package/src/lib/repo/migrations.ts +26 -0
  27. package/src/lib/repo/sql.ts +8 -3
  28. package/src/lib/repo/types.ts +3 -1
  29. package/src/lib/schema.ts +14 -0
  30. package/src/lib/sync/auth.ts +24 -0
  31. package/src/lib/sync/catalog.ts +250 -0
  32. package/src/lib/sync/migration.ts +51 -0
  33. package/src/lib/sync/protocol.mjs +125 -0
  34. package/src/lib/sync/service.ts +994 -0
  35. package/src/lib/sync/store.ts +349 -0
  36. package/src/pages/admin/calendar/settings.astro +21 -2
  37. package/src/pages/admin/integrations.astro +2 -0
  38. package/src/pages/admin/links.astro +1 -1
  39. package/src/pages/api/agent/docs.ts +2 -0
  40. package/src/pages/api/design/keys.ts +7 -3
  41. package/src/pages/api/sync/index.ts +114 -0
  42. package/src/pages/api/sync/media.ts +90 -0
  43. package/src/pages/links.astro +13 -2
  44. package/src/styles/admin.css +27 -36
  45. package/src/styles/page-transitions.css +27 -0
  46. package/src/styles/site.css +3 -1
  47. package/src/styles/theme-editor.css +47 -12
  48. 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@acms-subir/core",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -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
- <li class:list={['upev-item', { 'is-cancelled': e.cancelled }]}>
18
- {variant === 'cards' && e.image
19
- ? <a class="upev-media" href={e.url} tabindex="-1" aria-hidden="true"><img src={e.image} alt="" loading="lazy" /></a>
20
- : <span class="upev-date" aria-hidden="true"><b>{e.day}</b>{e.month}</span>}
21
- <div class="upev-body">
22
- <span class="upev-when">{e.cancelled ? 'Cancelled · ' : ''}{e.when}</span>
23
- <h3 class="upev-title"><a href={e.url}>{e.title}</a></h3>
24
- {e.location && <span class="upev-loc">{e.location}</span>}
25
- </div>
26
- </li>
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: 16 / 9; border-radius: var(--r-sm); overflow: hidden; background: var(--soft); }
44
- .upev-media img { width: 100%; height: 100%; object-fit: cover; }
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"
@@ -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
- {e.cover && (
65
- <figure class="pc-ev-hero"><img src={e.cover} alt={e.coverAlt} /></figure>
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">