@bygghammar/cms-platform 0.96.0 → 0.97.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.
@@ -5,7 +5,7 @@
5
5
  * a published page or an unsaved draft. The registry is passed in, so a site
6
6
  * only supplies its own block definitions.
7
7
  */
8
- import { useMemo } from "react";
8
+ import { Fragment, useMemo, type ReactNode } from "react";
9
9
  import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
10
10
 
11
11
  import { publishedPageStaleTime } from "../cms/page-publication";
@@ -116,6 +116,32 @@ function RenderBlock({
116
116
  );
117
117
  }
118
118
 
119
+ /** Block types that open a page full-bleed, under the header. */
120
+ export const DEFAULT_HERO_BLOCK_TYPES: readonly string[] = Object.freeze(["heroFull"]);
121
+
122
+ /**
123
+ * Where a notice goes among a page's top-level blocks: right after a leading
124
+ * hero (which sits under the header and must stay first), otherwise first. A
125
+ * global block counts by its own first visible block.
126
+ */
127
+ export function noticeIndex(
128
+ blocks: readonly BlockInstance[],
129
+ globals: Record<string, BlockInstance[]>,
130
+ heroTypes: readonly string[] = DEFAULT_HERO_BLOCK_TYPES,
131
+ ): number {
132
+ const leadOf = (block: BlockInstance): BlockInstance | null => {
133
+ if (block.visible === false) return null;
134
+ if (block.type !== "globalRef") return block;
135
+ const key = String((block.props as BlockProps | undefined)?.sectionKey ?? "");
136
+ return (globals[key] ?? []).find((inner) => inner.visible !== false) ?? null;
137
+ };
138
+ for (let i = 0; i < blocks.length; i += 1) {
139
+ const lead = leadOf(blocks[i]!);
140
+ if (lead) return heroTypes.includes(lead.type) ? i + 1 : 0;
141
+ }
142
+ return 0;
143
+ }
144
+
119
145
  /** Render an in-memory block tree — used by the builder preview and drafts. */
120
146
  export function DraftPageRendererView({
121
147
  blocks,
@@ -123,12 +149,15 @@ export function DraftPageRendererView({
123
149
  registry,
124
150
  edit,
125
151
  selectedId,
152
+ insert,
126
153
  }: {
127
154
  blocks: BlockInstance[];
128
155
  globals: Record<string, BlockInstance[]>;
129
156
  registry: BlockRegistry;
130
157
  edit?: boolean;
131
158
  selectedId?: string | null;
159
+ /** Something that is not a block, rendered among the top-level blocks at `at`. */
160
+ insert?: { at: number; node: ReactNode };
132
161
  }) {
133
162
  // Container blocks (`layout`) render their children through this, so nesting
134
163
  // uses exactly the same code path — and the same globals, selection
@@ -154,9 +183,17 @@ export function DraftPageRendererView({
154
183
  // Every page render gets its own priority scope, so the first first-screen
155
184
  // image loads eagerly and the rest stay lazy. In the builder (`edit`) the
156
185
  // editor jumps around the page, so no image is treated as the hero.
186
+ const rendered = children.render(blocks);
187
+ if (insert) {
188
+ rendered.splice(
189
+ Math.max(0, Math.min(insert.at, rendered.length)),
190
+ 0,
191
+ <Fragment key="__inserted">{insert.node}</Fragment>,
192
+ );
193
+ }
157
194
  return (
158
195
  <PriorityMediaProvider enabled={!edit}>
159
- <BlockChildrenProvider value={children}>{children.render(blocks)}</BlockChildrenProvider>
196
+ <BlockChildrenProvider value={children}>{rendered}</BlockChildrenProvider>
160
197
  </PriorityMediaProvider>
161
198
  );
162
199
  }
@@ -212,14 +249,24 @@ export function createPageRenderer(
212
249
  * fetched. The cached original is not changed, so a later policy can show
213
250
  * it again. Pass a stable function, or none.
214
251
  */
252
+ /*
253
+ * `notice` is shown among the page's blocks — after a leading hero (one of
254
+ * `heroBlockTypes`), otherwise first — without changing the page itself: a
255
+ * booking-off landing page's notice, say. It is decided by the caller from
256
+ * the current policy, like `transform`.
257
+ */
215
258
  function PageRenderer({
216
259
  pageKey,
217
260
  variant,
218
261
  transform,
262
+ notice,
263
+ heroBlockTypes,
219
264
  }: {
220
265
  pageKey: string;
221
266
  variant?: string;
222
267
  transform?: PageTransform;
268
+ notice?: ReactNode;
269
+ heroBlockTypes?: readonly string[];
223
270
  }) {
224
271
  const { data } = useSuspenseQuery(pageQueryOptions(pageKey, variant));
225
272
  const fetched = data as RenderedPage | null;
@@ -228,8 +275,17 @@ export function createPageRenderer(
228
275
  [fetched, transform],
229
276
  );
230
277
  if (!page) return null;
278
+ const insert =
279
+ notice != null && notice !== false
280
+ ? { at: noticeIndex(page.blocks, page.globals, heroBlockTypes), node: notice }
281
+ : undefined;
231
282
  return (
232
- <DraftPageRendererView blocks={page.blocks} globals={page.globals} registry={registry} />
283
+ <DraftPageRendererView
284
+ blocks={page.blocks}
285
+ globals={page.globals}
286
+ registry={registry}
287
+ insert={insert}
288
+ />
233
289
  );
234
290
  }
235
291
 
@@ -7,9 +7,13 @@
7
7
  * provider nothing is filtered, which is what the backoffice and any screen
8
8
  * that renders the chrome outside the public site get.
9
9
  */
10
- import { createContext, useContext, type ReactNode } from "react";
10
+ import { createContext, useCallback, useContext, type ReactNode } from "react";
11
11
 
12
- import { OPEN_LINK_POLICY, type LinkVisibilityPolicy } from "../navigation/visibility";
12
+ import {
13
+ OPEN_LINK_POLICY,
14
+ isHiddenFromDiscovery,
15
+ type LinkVisibilityPolicy,
16
+ } from "../navigation/visibility";
13
17
 
14
18
  const LinkVisibilityContext = createContext<LinkVisibilityPolicy>(OPEN_LINK_POLICY);
15
19
 
@@ -26,3 +30,18 @@ export function LinkVisibilityProvider({
26
30
  export function useLinkVisibility(): LinkVisibilityPolicy {
27
31
  return useContext(LinkVisibilityContext);
28
32
  }
33
+
34
+ /**
35
+ * The one rule every structured link on a page follows — buttons, cards,
36
+ * tiles, slide links: may it be offered right now? False only for a link to a
37
+ * page hidden from the website while booking is hidden (see
38
+ * `isHiddenFromDiscovery`); external links, anchors, `mailto:`/`tel:` and
39
+ * every other page are always offered. Read from the policy the server
40
+ * rendered with and every client navigation refreshes, so the server's HTML
41
+ * and the browser agree. Without a provider (the builder, the backoffice)
42
+ * everything is offered. Running text is not filtered: see docs/PAGES.md.
43
+ */
44
+ export function useDiscoverableLink(): (href: string | null | undefined) => boolean {
45
+ const policy = useLinkVisibility();
46
+ return useCallback((href) => !isHiddenFromDiscovery(href, policy), [policy]);
47
+ }