ferst-core 0.2.6 → 0.2.7

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.
@@ -9,7 +9,7 @@
9
9
  * The embed URL is required to be https (isSafeEmbedUrl) before it can reach the
10
10
  * iframe; an unset or unsafe URL degrades to a friendly note + optional link.
11
11
  */
12
- import { isSafeEmbedUrl } from '../content/calendar';
12
+ import { isSafeEmbedUrl, withWeekStart } from '../content/calendar';
13
13
 
14
14
  interface Props {
15
15
  title?: string;
@@ -18,6 +18,8 @@ interface Props {
18
18
  publicUrl?: string;
19
19
  height?: number;
20
20
  provider?: string;
21
+ /** First day of the weekly view (Google Calendar embeds only). Default Monday. */
22
+ weekStart?: 'monday' | 'sunday';
21
23
  }
22
24
  const {
23
25
  title = 'Calendar',
@@ -26,16 +28,19 @@ const {
26
28
  publicUrl = '',
27
29
  height = 600,
28
30
  provider = 'Google Calendar',
31
+ weekStart = 'monday',
29
32
  } = Astro.props;
30
33
 
31
- const safe = isSafeEmbedUrl(embedUrl);
34
+ // Apply the week-start preference to the embed (no-op for non-Google URLs).
35
+ const finalEmbedUrl = withWeekStart(embedUrl, weekStart);
36
+ const safe = isSafeEmbedUrl(finalEmbedUrl);
32
37
  ---
33
38
 
34
39
  <section class="b-calendar">
35
40
  {title && <h2 class="b-calendar__title">{title}</h2>}
36
41
  {intro && <p class="b-calendar__intro">{intro}</p>}
37
42
 
38
- {!embedUrl ? (
43
+ {!finalEmbedUrl ? (
39
44
  <p class="b-calendar__note">The calendar isn't set up yet.</p>
40
45
  ) : !safe ? (
41
46
  <p class="b-calendar__note">
@@ -47,7 +52,7 @@ const safe = isSafeEmbedUrl(embedUrl);
47
52
  ) : (
48
53
  <div
49
54
  class="b-calendar__embed"
50
- data-embed-url={embedUrl}
55
+ data-embed-url={finalEmbedUrl}
51
56
  data-height={String(height)}
52
57
  data-title={title}
53
58
  >
@@ -75,7 +75,7 @@ const { title, intro, columns = [], rows = [] } = Astro.props;
75
75
  padding: 0.7rem 0.9rem;
76
76
  text-align: start;
77
77
  vertical-align: top;
78
- border-bottom: 1px solid var(--line, var(--card-border-color, rgba(0,0,0,0.08)));
78
+ border-bottom: 1px solid var(--border);
79
79
  }
80
80
  .b-table__table thead th {
81
81
  font-family: var(--font-heading);
@@ -21,10 +21,37 @@ export const calendarSettingsSchema = z.object({
21
21
  height: z.number().int().min(200).max(2000).default(600),
22
22
  /** Provider name, shown in the consent notice ("shared with …"). */
23
23
  provider: z.string().default('Google Calendar'),
24
+ /** First day of the week in the weekly view. UK convention is Monday, so that is
25
+ * the default; sites with a US/Sunday-first audience can switch it. Applied to
26
+ * Google Calendar embeds via `wkst` (other providers ignore it). */
27
+ weekStart: z.enum(['monday', 'sunday']).default('monday'),
24
28
  });
25
29
 
26
30
  export type CalendarSettings = z.infer<typeof calendarSettingsSchema>;
27
31
 
32
+ /** Google Calendar's `wkst` numbering: 1 = Sunday, 2 = Monday, … 7 = Saturday. */
33
+ const WKST: Record<'monday' | 'sunday', string> = { monday: '2', sunday: '1' };
34
+
35
+ /**
36
+ * Return `embedUrl` with the week-start applied. Only Google Calendar embed URLs
37
+ * carry a `wkst` param, so we touch nothing else — a non-Google URL (or an
38
+ * unparseable one) is returned unchanged. Any existing `wkst` is overridden so the
39
+ * CMS setting always wins. Pure + testable.
40
+ */
41
+ export function withWeekStart(embedUrl: string, weekStart: 'monday' | 'sunday' = 'monday'): string {
42
+ if (!embedUrl) return embedUrl;
43
+ try {
44
+ const u = new URL(embedUrl);
45
+ const isGoogleCalendar =
46
+ u.hostname.endsWith('google.com') && u.pathname.includes('/calendar/embed');
47
+ if (!isGoogleCalendar) return embedUrl;
48
+ u.searchParams.set('wkst', WKST[weekStart]);
49
+ return u.toString();
50
+ } catch {
51
+ return embedUrl;
52
+ }
53
+ }
54
+
28
55
  /**
29
56
  * Whether an embed URL is safe to load into an iframe. The value is trusted repo /
30
57
  * CMS data, but we still require `https:` so a stray `javascript:` / `http:` value
@@ -313,14 +313,23 @@ const metaDescription = description ?? identity.description;
313
313
  article :where(img, picture, video, table, figure, iframe) {
314
314
  max-width: 100%;
315
315
  }
316
- article img:not(.group-card__img) {
316
+ /* Prose images — editor/markdown images dropped into long-form body content
317
+ get a centred block treatment (rounded, soft shadow). Scoped to the real
318
+ prose containers (post body, Prose block) so it never leaks onto STRUCTURAL
319
+ component images — cards, banners, media headers — which own their own
320
+ framing and crop absolutely. (A leaked `margin` pushes an inset image down
321
+ inside its overflow-hidden box, exposing a strip above it — the "empty
322
+ space above the image" a bare `article img` rule caused.) */
323
+ .b-post__body img,
324
+ .b-prose img {
317
325
  display: block;
318
326
  margin: 1.5rem auto;
319
327
  border-radius: 8px;
320
328
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
321
329
  }
322
330
  @media (min-width: 44em) {
323
- article img:not(.group-card__img) {
331
+ .b-post__body img,
332
+ .b-prose img {
324
333
  border-radius: 12px;
325
334
  }
326
335
  }
@@ -340,11 +349,10 @@ const metaDescription = description ?? identity.description;
340
349
  }
341
350
 
342
351
  /* ── Fluid media: never overflow container ── */
343
- img:not(.group-card__img):not(.primary-logo), picture, video {
344
- max-width: 100%;
345
- height: auto;
346
- }
347
- .group-card__img {
352
+ /* The logo is exempt: it carries a fixed height in SiteHeader that `height:
353
+ auto` would collapse. Card/media images set their own size at higher
354
+ specificity, so this only ever backstops loose images. */
355
+ img:not(.primary-logo), picture, video {
348
356
  max-width: 100%;
349
357
  height: auto;
350
358
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ferst-core",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "description": "Ferst Core — the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
6
6
  "license": "BUSL-1.1",