@bygghammar/cms-platform 0.69.0 → 0.70.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bygghammar/cms-platform",
3
- "version": "0.69.0",
3
+ "version": "0.70.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": [
@@ -5,6 +5,7 @@
5
5
  * stay in each site's own inspector router.
6
6
  */
7
7
  import { useEffect, useMemo, useState } from "react";
8
+ import { BUTTON_COLOR_DEFAULTS, type ButtonColors } from "../../blocks/button-colors";
8
9
  import type {
9
10
  BlockInstance,
10
11
  BlockProps,
@@ -1281,6 +1282,12 @@ function LinkTilesInspector({ block, patch }: BlockPatchProps) {
1281
1282
  </label>
1282
1283
  <TextField label="Anchor id" value={p.anchor} onChange={(v) => update({ ...p, anchor: v })} />
1283
1284
  </InspectorSection>
1285
+ <InspectorSection title="Button colours">
1286
+ <ButtonColorFields
1287
+ value={p.buttonColors}
1288
+ onChange={(next) => update({ ...p, buttonColors: next })}
1289
+ />
1290
+ </InspectorSection>
1284
1291
  <InspectorSection title={`Tiles (${items.length})`}>
1285
1292
  <div className="space-y-4">
1286
1293
  {items.map((item, i) => (
@@ -1503,6 +1510,64 @@ function CabinTabsInspector({ block, patch }: BlockPatchProps) {
1503
1510
  );
1504
1511
  }
1505
1512
 
1513
+ /* ---------------- Button colours ---------------- */
1514
+ /**
1515
+ * Colour controls for a block's CTA button. Empty values mean "use the brand
1516
+ * default", which matches the shared price-table button.
1517
+ */
1518
+ function ButtonColorFields({
1519
+ value,
1520
+ onChange,
1521
+ }: {
1522
+ value?: ButtonColors;
1523
+ onChange: (next: ButtonColors) => void;
1524
+ }) {
1525
+ const v = value ?? {};
1526
+ const row = (label: string, key: keyof ButtonColors) => {
1527
+ const current = v[key]?.trim() || "";
1528
+ const swatch = current.startsWith("#") ? current : "#ffffff";
1529
+ return (
1530
+ <label key={key} className="block space-y-1.5">
1531
+ <span className="block text-[0.7rem] uppercase tracking-[0.22em] text-ink/60">{label}</span>
1532
+ <span className="flex items-center gap-2">
1533
+ <input
1534
+ type="color"
1535
+ value={swatch}
1536
+ onChange={(e) => onChange({ ...v, [key]: e.target.value })}
1537
+ className="h-9 w-12 shrink-0 cursor-pointer border border-ink/15 bg-white p-0.5"
1538
+ aria-label={`${label} colour picker`}
1539
+ />
1540
+ <input
1541
+ type="text"
1542
+ value={current}
1543
+ placeholder={BUTTON_COLOR_DEFAULTS[key]}
1544
+ onChange={(e) => onChange({ ...v, [key]: e.target.value })}
1545
+ className="w-full border border-ink/15 bg-white px-2.5 py-1.5 text-sm text-ink focus:outline-none focus:border-sea"
1546
+ />
1547
+ </span>
1548
+ </label>
1549
+ );
1550
+ };
1551
+ return (
1552
+ <>
1553
+ <p className="text-xs leading-relaxed text-ink/60">
1554
+ Leave a field empty to use the site default. Accepts hex values such as #ff4f9a.
1555
+ </p>
1556
+ {row("Background", "bg")}
1557
+ {row("Text", "text")}
1558
+ {row("Background on hover", "hoverBg")}
1559
+ {row("Text on hover", "hoverText")}
1560
+ <button
1561
+ type="button"
1562
+ onClick={() => onChange({})}
1563
+ className="text-xs uppercase tracking-[0.18em] text-ink/60 underline hover:text-ink"
1564
+ >
1565
+ Reset to defaults
1566
+ </button>
1567
+ </>
1568
+ );
1569
+ }
1570
+
1506
1571
  /* ---------------- Price table ---------------- */
1507
1572
  function PriceTableInspector({ block, patch }: BlockPatchProps) {
1508
1573
  const p = (block.props ?? {}) as PriceTableProps;
@@ -1532,6 +1597,12 @@ function PriceTableInspector({ block, patch }: BlockPatchProps) {
1532
1597
  <LocalizedField label="Button label" value={p.ctaLabel} onChange={(v) => update({ ...p, ctaLabel: v })} />
1533
1598
  <TextField label="Button link" value={p.ctaHref} placeholder="/boka" onChange={(v) => update({ ...p, ctaHref: v })} />
1534
1599
  </InspectorSection>
1600
+ <InspectorSection title="Button colours">
1601
+ <ButtonColorFields
1602
+ value={p.ctaColors}
1603
+ onChange={(next) => update({ ...p, ctaColors: next })}
1604
+ />
1605
+ </InspectorSection>
1535
1606
  <InspectorSection title="Layout">
1536
1607
  <label className="block space-y-1.5">
1537
1608
  <span className="block text-[0.7rem] uppercase tracking-[0.22em] text-ink/60">Columns (desktop)</span>
@@ -0,0 +1,46 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ /**
4
+ * Editable colours for a block's call-to-action button.
5
+ * Every value is a plain CSS colour string (hex, rgb(), oklch(), var(--token)).
6
+ * Empty/undefined values fall back to the brand defaults below, which match the
7
+ * shared price-table CTA styling.
8
+ */
9
+ export type ButtonColors = {
10
+ /** Background colour. */
11
+ bg?: string;
12
+ /** Label colour. */
13
+ text?: string;
14
+ /** Background colour on hover/focus. */
15
+ hoverBg?: string;
16
+ /** Label colour on hover/focus. */
17
+ hoverText?: string;
18
+ };
19
+
20
+ export const BUTTON_COLOR_DEFAULTS: Required<ButtonColors> = {
21
+ bg: "var(--sea)",
22
+ text: "#ffffff",
23
+ hoverBg: "var(--brass)",
24
+ hoverText: "#ffffff",
25
+ };
26
+
27
+ /**
28
+ * Tailwind classes that read the CSS variables produced by
29
+ * {@link buttonColorStyle}. Kept as a static literal so the compiler keeps them.
30
+ */
31
+ export const BUTTON_COLOR_CLASS =
32
+ "bg-[var(--btn-bg)] text-[var(--btn-text)] transition-colors hover:bg-[var(--btn-bg-hover)] hover:text-[var(--btn-text-hover)] focus-visible:bg-[var(--btn-bg-hover)] focus-visible:text-[var(--btn-text-hover)]";
33
+
34
+ /** Inline CSS variables for a button's colours, with brand defaults applied. */
35
+ export function buttonColorStyle(colors?: ButtonColors): CSSProperties {
36
+ const bg = colors?.bg?.trim() || BUTTON_COLOR_DEFAULTS.bg;
37
+ const text = colors?.text?.trim() || BUTTON_COLOR_DEFAULTS.text;
38
+ const hoverBg = colors?.hoverBg?.trim() || BUTTON_COLOR_DEFAULTS.hoverBg;
39
+ const hoverText = colors?.hoverText?.trim() || BUTTON_COLOR_DEFAULTS.hoverText;
40
+ return {
41
+ ["--btn-bg" as string]: bg,
42
+ ["--btn-text" as string]: text,
43
+ ["--btn-bg-hover" as string]: hoverBg,
44
+ ["--btn-text-hover" as string]: hoverText,
45
+ } as CSSProperties;
46
+ }
@@ -2,6 +2,7 @@ import { useT } from "../../i18n";
2
2
  import { Reveal } from "../../ui/Reveal";
3
3
  import { FocalImage } from "../../host";
4
4
  import type { LinkTilesProps, LinkTilesItem } from "../types";
5
+ import { BUTTON_COLOR_CLASS, buttonColorStyle, type ButtonColors } from "../button-colors";
5
6
 
6
7
  const TONE: Record<string, string> = {
7
8
  white: "bg-white",
@@ -36,12 +37,14 @@ function Tile({
36
37
  rounded,
37
38
  overlay,
38
39
  labelAlign,
40
+ buttonColors,
39
41
  }: {
40
42
  item: LinkTilesItem;
41
43
  aspectClass: string;
42
44
  rounded: boolean;
43
45
  overlay: "none" | "light" | "medium" | "strong" | "brand";
44
46
  labelAlign: "left" | "center";
47
+ buttonColors?: ButtonColors;
45
48
  }) {
46
49
  const { t } = useT();
47
50
  const label = t(item.label);
@@ -80,7 +83,10 @@ function Tile({
80
83
  </span>
81
84
  ) : null}
82
85
  {label ? (
83
- <span className="mt-3 inline-flex items-center justify-center bg-sea px-6 py-3 text-xs uppercase tracking-[0.22em] text-ink shadow-lg transition-colors hover:bg-ink hover:text-paper">
86
+ <span
87
+ style={buttonColorStyle(buttonColors)}
88
+ className={`mt-3 inline-flex items-center justify-center px-6 py-3 text-xs uppercase tracking-[0.22em] shadow-lg ${BUTTON_COLOR_CLASS}`}
89
+ >
84
90
  {label}
85
91
  </span>
86
92
  ) : null}
@@ -172,6 +178,7 @@ export function LinkTilesBlock({ props }: { props: LinkTilesProps }) {
172
178
  rounded={props.rounded ?? false}
173
179
  overlay={props.overlay ?? "medium"}
174
180
  labelAlign={props.labelAlign ?? "left"}
181
+ buttonColors={props.buttonColors}
175
182
  />
176
183
  ))}
177
184
  </div>
@@ -1,4 +1,5 @@
1
1
  import { useT } from "../../i18n";
2
+ import { BUTTON_COLOR_CLASS, buttonColorStyle } from "../button-colors";
2
3
  import { Reveal } from "../../ui/Reveal";
3
4
  import type { PriceTableProps } from "../types";
4
5
 
@@ -143,7 +144,8 @@ export function PriceTableBlock({ props }: { props: PriceTableProps }) {
143
144
  {props.ctaLabel && props.ctaHref ? (
144
145
  <a
145
146
  href={props.ctaHref}
146
- className="inline-flex items-center bg-sea px-6 py-3 text-[0.7rem] uppercase tracking-[0.22em] text-white hover:bg-brass"
147
+ style={buttonColorStyle(props.ctaColors)}
148
+ className={`inline-flex items-center px-6 py-3 text-[0.7rem] uppercase tracking-[0.22em] ${BUTTON_COLOR_CLASS}`}
147
149
  >
148
150
  {t(props.ctaLabel)}
149
151
  </a>
@@ -11,3 +11,9 @@ export { MediaSplitBlock } from "./components/MediaSplitBlock";
11
11
  export { PriceTableBlock } from "./components/PriceTableBlock";
12
12
  export { ProseBlock } from "./components/ProseBlock";
13
13
  export { RichTextBlock } from "./components/RichTextBlock";
14
+ export {
15
+ BUTTON_COLOR_CLASS,
16
+ BUTTON_COLOR_DEFAULTS,
17
+ buttonColorStyle,
18
+ type ButtonColors,
19
+ } from "./button-colors";
@@ -1,3 +1,4 @@
1
+ import type { ButtonColors } from "./button-colors";
1
2
  import type { LocalizedString } from "../i18n";
2
3
 
3
4
  /**
@@ -344,6 +345,8 @@ export type LinkTilesItem = {
344
345
  };
345
346
 
346
347
  export type LinkTilesProps = {
348
+ /** Colours for the tile label buttons. */
349
+ buttonColors?: ButtonColors;
347
350
  eyebrow?: Partial<LocalizedString>;
348
351
  title?: Partial<LocalizedString>;
349
352
  lead?: Partial<LocalizedString>;
@@ -408,6 +411,8 @@ export type PriceGroup = {
408
411
  };
409
412
 
410
413
  export type PriceTableProps = {
414
+ /** Colours for the CTA button. */
415
+ ctaColors?: ButtonColors;
411
416
  eyebrow?: Partial<LocalizedString>;
412
417
  title?: Partial<LocalizedString>;
413
418
  lead?: Partial<LocalizedString>;
@@ -212,8 +212,15 @@ export function SiteHeader({
212
212
  useEffect(() => {
213
213
  const detect = () => setDarkHero(Boolean(document.querySelector('[data-site-hero="dark"]')));
214
214
  detect();
215
- const timer = setTimeout(detect, 150);
216
- return () => clearTimeout(timer);
215
+ // Blocks stream in after the route commits, so keep watching the document
216
+ // until the hero (or its absence) is settled.
217
+ const observer = new MutationObserver(detect);
218
+ observer.observe(document.body, { childList: true, subtree: true });
219
+ const stop = setTimeout(() => observer.disconnect(), 3000);
220
+ return () => {
221
+ observer.disconnect();
222
+ clearTimeout(stop);
223
+ };
217
224
  }, [pathname]);
218
225
 
219
226
  const lightText = (lightTextPaths.includes(pathname) || darkHero) && !scrolled;