@bygghammar/cms-platform 0.73.0 → 0.75.0

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 CHANGED
@@ -1,5 +1,17 @@
1
+ ## 0.75.0
2
+
3
+ - Countdown: pick the replacement global block from a dropdown instead of typing its key.
4
+
1
5
  # Changelog
2
6
 
7
+ ## 0.74.0
8
+
9
+ - `CountdownBlock`: typography controls for the heading, sub text and fine print
10
+ — size (small/medium/large/extra large), weight (normal → black), serif/sans,
11
+ letter spacing, capitals and line height, each resettable. Defaults reproduce
12
+ the previous appearance, so saved pages are unchanged.
13
+
14
+
3
15
  ## 0.73.0
4
16
 
5
17
  - New **Countdown** block: timezone-aware deadline (IANA zone + wall clock),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bygghammar/cms-platform",
3
- "version": "0.73.0",
3
+ "version": "0.75.0",
4
4
  "description": "Shared CMS platform: blocks, block types, i18n core, UI primitives and design-token contract used by every Bygghammar/Voyara site.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -30,6 +30,7 @@
30
30
  "./host": "./src/host/index.tsx",
31
31
  "./blocks": "./src/blocks/index.ts",
32
32
  "./blocks/types": "./src/blocks/types.ts",
33
+ "./blocks/countdown-typography": "./src/blocks/countdown-typography.ts",
33
34
  "./blocks/icons": "./src/blocks/icons.ts",
34
35
  "./blocks/block-props-context": "./src/blocks/block-props-context.tsx",
35
36
  "./blocks/components/*": "./src/blocks/components/*.tsx",
@@ -7,6 +7,17 @@
7
7
  import { useEffect, useMemo, useState } from "react";
8
8
  import { BUTTON_COLOR_DEFAULTS, type ButtonColors } from "../../blocks/button-colors";
9
9
  import { COUNTDOWN_COLOR_DEFAULTS, type CountdownColors } from "../../blocks/countdown-colors";
10
+ import {
11
+ COUNTDOWN_TEXT_DEFAULTS,
12
+ COUNTDOWN_TEXT_FAMILY_OPTIONS,
13
+ COUNTDOWN_TEXT_LEADING_OPTIONS,
14
+ COUNTDOWN_TEXT_SIZE_OPTIONS,
15
+ COUNTDOWN_TEXT_TRACKING_OPTIONS,
16
+ COUNTDOWN_TEXT_TRANSFORM_OPTIONS,
17
+ COUNTDOWN_TEXT_WEIGHT_OPTIONS,
18
+ type CountdownTextSlot,
19
+ type CountdownTextStyle,
20
+ } from "../../blocks/countdown-typography";
10
21
  import {
11
22
  COUNTDOWN_DEFAULT_TIMEZONE,
12
23
  COUNTDOWN_TIMEZONES,
@@ -2402,7 +2413,11 @@ export {
2402
2413
  };
2403
2414
 
2404
2415
  /* ---------------- Countdown ---------------- */
2405
- export function CountdownInspector({ block, patch }: BlockPatchProps) {
2416
+ export function CountdownInspector({
2417
+ block,
2418
+ patch,
2419
+ globalSections = [],
2420
+ }: BlockPatchProps & { globalSections?: { key: string; name: string }[] }) {
2406
2421
  const p = (block.props ?? {}) as CountdownProps;
2407
2422
  const update = (next: CountdownProps) => patch(next as unknown as BlockProps);
2408
2423
 
@@ -2467,6 +2482,47 @@ export function CountdownInspector({ block, patch }: BlockPatchProps) {
2467
2482
  </label>
2468
2483
  );
2469
2484
 
2485
+ const textStyle = (
2486
+ title: string,
2487
+ slot: CountdownTextSlot,
2488
+ key: "titleType" | "bodyType" | "noteType",
2489
+ ) => {
2490
+ const current = (p[key] ?? {}) as CountdownTextStyle;
2491
+ const d = COUNTDOWN_TEXT_DEFAULTS[slot];
2492
+ const set = (patchStyle: CountdownTextStyle) =>
2493
+ update({ ...p, [key]: { ...current, ...patchStyle } });
2494
+ return (
2495
+ <div key={key} className="space-y-2 border border-ink/10 bg-paper/60 p-3">
2496
+ <p className="text-[0.7rem] uppercase tracking-[0.22em] text-ink/70">{title}</p>
2497
+ <div className="grid grid-cols-2 gap-2">
2498
+ {select("Size", current.size, d.size, COUNTDOWN_TEXT_SIZE_OPTIONS, (v) => set({ size: v }))}
2499
+ {select("Weight", current.weight, d.weight, COUNTDOWN_TEXT_WEIGHT_OPTIONS, (v) =>
2500
+ set({ weight: v }),
2501
+ )}
2502
+ {select("Style", current.family, d.family, COUNTDOWN_TEXT_FAMILY_OPTIONS, (v) =>
2503
+ set({ family: v }),
2504
+ )}
2505
+ {select("Letter spacing", current.tracking, d.tracking, COUNTDOWN_TEXT_TRACKING_OPTIONS, (v) =>
2506
+ set({ tracking: v }),
2507
+ )}
2508
+ {select("Capitals", current.transform, d.transform, COUNTDOWN_TEXT_TRANSFORM_OPTIONS, (v) =>
2509
+ set({ transform: v }),
2510
+ )}
2511
+ {select("Line height", current.leading, d.leading, COUNTDOWN_TEXT_LEADING_OPTIONS, (v) =>
2512
+ set({ leading: v }),
2513
+ )}
2514
+ </div>
2515
+ <button
2516
+ type="button"
2517
+ onClick={() => update({ ...p, [key]: {} })}
2518
+ className="text-xs uppercase tracking-[0.18em] text-ink/60 underline hover:text-ink"
2519
+ >
2520
+ Reset to default
2521
+ </button>
2522
+ </div>
2523
+ );
2524
+ };
2525
+
2470
2526
  const toggle = (label: string, value: boolean, onPick: (v: boolean) => void) => (
2471
2527
  <label className="flex items-center gap-2 text-sm text-ink">
2472
2528
  <input type="checkbox" checked={value} onChange={(e) => onPick(e.target.checked)} />
@@ -2482,6 +2538,15 @@ export function CountdownInspector({ block, patch }: BlockPatchProps) {
2482
2538
  <LocalizedField label="Fine print (under the timer)" value={p.note} onChange={(v) => update({ ...p, note: v })} />
2483
2539
  </InspectorSection>
2484
2540
 
2541
+ <InspectorSection title="Typography">
2542
+ <p className="text-xs leading-relaxed text-ink/60">
2543
+ Size and weight for the three texts above and under the timer.
2544
+ </p>
2545
+ {textStyle("Heading", "heading", "titleType")}
2546
+ {textStyle("Sub text", "sub", "bodyType")}
2547
+ {textStyle("Fine print", "note", "noteType")}
2548
+ </InspectorSection>
2549
+
2485
2550
  <InspectorSection title="Deadline">
2486
2551
  <label className="block space-y-1.5">
2487
2552
  <span className="block text-[0.7rem] uppercase tracking-[0.22em] text-ink/60">
@@ -2650,11 +2715,37 @@ export function CountdownInspector({ block, patch }: BlockPatchProps) {
2650
2715
  (v) => update({ ...p, onExpire: v as CountdownProps["onExpire"] }),
2651
2716
  )}
2652
2717
  {p.onExpire === "replace" ? (
2653
- <TextField
2654
- label="Global block key to show instead"
2655
- value={p.expiredSectionKey ?? ""}
2656
- onChange={(v) => update({ ...p, expiredSectionKey: v })}
2657
- />
2718
+ globalSections.length ? (
2719
+ <label className="block space-y-1.5">
2720
+ <span className="block text-[0.7rem] uppercase tracking-[0.22em] text-ink/60">
2721
+ Global block to show instead
2722
+ </span>
2723
+ <select
2724
+ value={p.expiredSectionKey ?? ""}
2725
+ onChange={(e) => update({ ...p, expiredSectionKey: e.target.value })}
2726
+ className="w-full border border-ink/15 bg-white px-2.5 py-1.5 text-sm text-ink focus:border-sea focus:outline-none"
2727
+ >
2728
+ <option value="">— none —</option>
2729
+ {globalSections.map((g) => (
2730
+ <option key={g.key} value={g.key}>
2731
+ {g.name} ({g.key})
2732
+ </option>
2733
+ ))}
2734
+ {p.expiredSectionKey &&
2735
+ !globalSections.some((g) => g.key === p.expiredSectionKey) ? (
2736
+ <option value={p.expiredSectionKey}>
2737
+ {p.expiredSectionKey} (missing)
2738
+ </option>
2739
+ ) : null}
2740
+ </select>
2741
+ </label>
2742
+ ) : (
2743
+ <TextField
2744
+ label="Global block key to show instead"
2745
+ value={p.expiredSectionKey ?? ""}
2746
+ onChange={(v) => update({ ...p, expiredSectionKey: v })}
2747
+ />
2748
+ )
2658
2749
  ) : null}
2659
2750
  {p.onExpire === "revealPage" ? (
2660
2751
  <>
@@ -750,7 +750,7 @@ export function BlockInspector({
750
750
  case "courseList":
751
751
  return <TripListInspector block={block} patch={patch} />;
752
752
  case "countdown":
753
- return <CountdownInspector block={block} patch={patch} />;
753
+ return <CountdownInspector block={block} patch={patch} globalSections={globalSections} />;
754
754
  case "ctaBanner":
755
755
  return <CtaBannerInspector block={block} patch={patch} />;
756
756
  case "mediaSplit":
@@ -5,6 +5,7 @@ import type { LocalizedString } from "../../i18n";
5
5
  import { Reveal } from "../../ui/Reveal";
6
6
  import { countdownColorStyle, countdownRadius } from "../countdown-colors";
7
7
  import { countdownParts, resolveCountdownTarget } from "../countdown-time";
8
+ import { countdownTextClasses } from "../countdown-typography";
8
9
  import { useCountdownReplacement } from "../countdown-replacement";
9
10
  import type { CountdownProps } from "../types";
10
11
 
@@ -93,12 +94,16 @@ export function CountdownBlock({ props: p }: { props: CountdownProps }) {
93
94
  const heading = (title?: Partial<LocalizedString>, body?: Partial<LocalizedString>) => (
94
95
  <>
95
96
  {t(title) ? (
96
- <h2 className="font-serif text-[clamp(1.6rem,3.6vw,2.9rem)] leading-[1.1] tracking-[-0.02em] whitespace-pre-line text-[var(--cd-heading)]">
97
+ <h2
98
+ className={`${countdownTextClasses("heading", p.titleType)} whitespace-pre-line text-[var(--cd-heading)]`}
99
+ >
97
100
  {t(title)}
98
101
  </h2>
99
102
  ) : null}
100
103
  {t(body) ? (
101
- <p className="mt-4 max-w-2xl text-base md:text-lg leading-relaxed whitespace-pre-line text-[var(--cd-sub)]">
104
+ <p
105
+ className={`mt-4 max-w-2xl ${countdownTextClasses("sub", p.bodyType)} whitespace-pre-line text-[var(--cd-sub)]`}
106
+ >
102
107
  {t(body)}
103
108
  </p>
104
109
  ) : null}
@@ -179,7 +184,7 @@ export function CountdownBlock({ props: p }: { props: CountdownProps }) {
179
184
  ))}
180
185
  </div>
181
186
  {t(p.note) ? (
182
- <p className="mt-5 text-xs uppercase tracking-[0.18em] text-[var(--cd-sub)]">
187
+ <p className={`mt-5 ${countdownTextClasses("note", p.noteType)} text-[var(--cd-sub)]`}>
183
188
  {t(p.note)}
184
189
  </p>
185
190
  ) : null}
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Typography options for the Countdown block's texts (heading, sub text and
3
+ * fine print). Every value is optional; leaving a field unset keeps the
4
+ * original theme-driven look, so saved pages never change appearance.
5
+ */
6
+
7
+ export type CountdownTextSize = "sm" | "md" | "lg" | "xl";
8
+ export type CountdownTextWeight = "normal" | "medium" | "semibold" | "bold" | "black";
9
+ export type CountdownTextFamily = "serif" | "sans";
10
+ export type CountdownTextTracking = "tight" | "normal" | "wide" | "wider";
11
+ export type CountdownTextTransform = "none" | "uppercase" | "lowercase";
12
+ export type CountdownTextLeading = "tight" | "normal" | "relaxed";
13
+
14
+ export type CountdownTextStyle = {
15
+ size?: CountdownTextSize;
16
+ weight?: CountdownTextWeight;
17
+ family?: CountdownTextFamily;
18
+ tracking?: CountdownTextTracking;
19
+ transform?: CountdownTextTransform;
20
+ leading?: CountdownTextLeading;
21
+ };
22
+
23
+ export type CountdownTextSlot = "heading" | "sub" | "note";
24
+
25
+ /** Responsive sizes per slot; `md` reproduces the original hardcoded size. */
26
+ const SIZE: Record<CountdownTextSlot, Record<CountdownTextSize, string>> = {
27
+ heading: {
28
+ sm: "text-[clamp(1.2rem,2.4vw,1.75rem)]",
29
+ md: "text-[clamp(1.6rem,3.6vw,2.9rem)]",
30
+ lg: "text-[clamp(2rem,5vw,4rem)]",
31
+ xl: "text-[clamp(2.4rem,6.5vw,5.5rem)]",
32
+ },
33
+ sub: {
34
+ sm: "text-sm md:text-base",
35
+ md: "text-base md:text-lg",
36
+ lg: "text-lg md:text-2xl",
37
+ xl: "text-xl md:text-3xl",
38
+ },
39
+ note: {
40
+ sm: "text-[0.7rem]",
41
+ md: "text-xs",
42
+ lg: "text-sm md:text-base",
43
+ xl: "text-base md:text-lg",
44
+ },
45
+ };
46
+
47
+ const WEIGHT: Record<CountdownTextWeight, string> = {
48
+ normal: "font-normal",
49
+ medium: "font-medium",
50
+ semibold: "font-semibold",
51
+ bold: "font-bold",
52
+ black: "font-black",
53
+ };
54
+
55
+ const FAMILY: Record<CountdownTextFamily, string> = {
56
+ serif: "font-serif",
57
+ sans: "font-sans",
58
+ };
59
+
60
+ const TRACKING: Record<CountdownTextTracking, string> = {
61
+ tight: "tracking-[-0.02em]",
62
+ normal: "tracking-normal",
63
+ wide: "tracking-[0.08em]",
64
+ wider: "tracking-[0.22em]",
65
+ };
66
+
67
+ const TRANSFORM: Record<CountdownTextTransform, string> = {
68
+ none: "normal-case",
69
+ uppercase: "uppercase",
70
+ lowercase: "lowercase",
71
+ };
72
+
73
+ const LEADING: Record<CountdownTextLeading, string> = {
74
+ tight: "leading-[1.1]",
75
+ normal: "leading-snug",
76
+ relaxed: "leading-relaxed",
77
+ };
78
+
79
+ /** Defaults matching the block's original appearance per slot. */
80
+ export const COUNTDOWN_TEXT_DEFAULTS: Record<CountdownTextSlot, Required<CountdownTextStyle>> = {
81
+ heading: {
82
+ size: "md",
83
+ weight: "normal",
84
+ family: "serif",
85
+ tracking: "tight",
86
+ transform: "none",
87
+ leading: "tight",
88
+ },
89
+ sub: {
90
+ size: "md",
91
+ weight: "normal",
92
+ family: "sans",
93
+ tracking: "normal",
94
+ transform: "none",
95
+ leading: "relaxed",
96
+ },
97
+ note: {
98
+ size: "md",
99
+ weight: "normal",
100
+ family: "sans",
101
+ tracking: "wider",
102
+ transform: "uppercase",
103
+ leading: "normal",
104
+ },
105
+ };
106
+
107
+ /** Tailwind classes for one countdown text, falling back to the slot default. */
108
+ export function countdownTextClasses(slot: CountdownTextSlot, style?: CountdownTextStyle): string {
109
+ const d = COUNTDOWN_TEXT_DEFAULTS[slot];
110
+ const s = style ?? {};
111
+ return [
112
+ FAMILY[s.family ?? d.family],
113
+ SIZE[slot][s.size ?? d.size],
114
+ WEIGHT[s.weight ?? d.weight],
115
+ TRACKING[s.tracking ?? d.tracking],
116
+ TRANSFORM[s.transform ?? d.transform],
117
+ LEADING[s.leading ?? d.leading],
118
+ ].join(" ");
119
+ }
120
+
121
+ export const COUNTDOWN_TEXT_SIZE_OPTIONS: { value: CountdownTextSize; label: string }[] = [
122
+ { value: "sm", label: "Small" },
123
+ { value: "md", label: "Medium" },
124
+ { value: "lg", label: "Large" },
125
+ { value: "xl", label: "Extra large" },
126
+ ];
127
+
128
+ export const COUNTDOWN_TEXT_WEIGHT_OPTIONS: { value: CountdownTextWeight; label: string }[] = [
129
+ { value: "normal", label: "Normal" },
130
+ { value: "medium", label: "Medium" },
131
+ { value: "semibold", label: "Semibold" },
132
+ { value: "bold", label: "Bold" },
133
+ { value: "black", label: "Black" },
134
+ ];
135
+
136
+ export const COUNTDOWN_TEXT_FAMILY_OPTIONS: { value: CountdownTextFamily; label: string }[] = [
137
+ { value: "serif", label: "Serif (display)" },
138
+ { value: "sans", label: "Sans (body)" },
139
+ ];
140
+
141
+ export const COUNTDOWN_TEXT_TRACKING_OPTIONS: { value: CountdownTextTracking; label: string }[] = [
142
+ { value: "tight", label: "Tight" },
143
+ { value: "normal", label: "Normal" },
144
+ { value: "wide", label: "Wide" },
145
+ { value: "wider", label: "Extra wide" },
146
+ ];
147
+
148
+ export const COUNTDOWN_TEXT_TRANSFORM_OPTIONS: { value: CountdownTextTransform; label: string }[] = [
149
+ { value: "none", label: "As typed" },
150
+ { value: "uppercase", label: "UPPERCASE" },
151
+ { value: "lowercase", label: "lowercase" },
152
+ ];
153
+
154
+ export const COUNTDOWN_TEXT_LEADING_OPTIONS: { value: CountdownTextLeading; label: string }[] = [
155
+ { value: "tight", label: "Tight" },
156
+ { value: "normal", label: "Normal" },
157
+ { value: "relaxed", label: "Relaxed" },
158
+ ];
@@ -39,3 +39,5 @@ export {
39
39
  CountdownReplacementProvider,
40
40
  useCountdownReplacement,
41
41
  } from "./countdown-replacement";
42
+
43
+ export * from "./countdown-typography";
@@ -1,5 +1,6 @@
1
1
  import type { ButtonColors } from "./button-colors";
2
2
  import type { CountdownColors, CountdownRounding } from "./countdown-colors";
3
+ import type { CountdownTextStyle } from "./countdown-typography";
3
4
  import type { LocalizedString } from "../i18n";
4
5
 
5
6
  /**
@@ -463,6 +464,11 @@ export type CountdownProps = {
463
464
  /** Fine print under the timer. */
464
465
  note?: Partial<LocalizedString>;
465
466
 
467
+ /** Typography for the heading, sub text and fine print. */
468
+ titleType?: CountdownTextStyle;
469
+ bodyType?: CountdownTextStyle;
470
+ noteType?: CountdownTextStyle;
471
+
466
472
  /** Deadline as wall-clock time, `YYYY-MM-DDTHH:mm`. */
467
473
  targetLocal?: string;
468
474
  /** IANA timezone the deadline is expressed in, e.g. `Europe/Stockholm`. */