@brandlab-digital/site-next 3.15.0 → 3.16.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.
Files changed (54) hide show
  1. package/dist/image-runtime.js +6 -1
  2. package/dist/image-runtime.js.map +1 -1
  3. package/dist/navigation/use-sticky-call-bar.d.ts +6 -1
  4. package/dist/navigation/use-sticky-call-bar.d.ts.map +1 -1
  5. package/dist/navigation/use-sticky-call-bar.js +9 -4
  6. package/dist/navigation/use-sticky-call-bar.js.map +1 -1
  7. package/dist/next-sharp.d.ts +14 -9
  8. package/dist/next-sharp.d.ts.map +1 -1
  9. package/dist/next-sharp.js +26 -25
  10. package/dist/next-sharp.js.map +1 -1
  11. package/dist/ui-kit/brandlab-skin.d.ts +2 -0
  12. package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
  13. package/dist/ui-kit/brandlab-skin.js.map +1 -1
  14. package/dist/ui-kit/repeated-slots.d.ts +1 -1
  15. package/dist/ui-kit/repeated-slots.js +1 -1
  16. package/dist/ui-kit/repeated-slots.js.map +1 -1
  17. package/dist/ui-kit/sections/cta-band.d.ts +10 -2
  18. package/dist/ui-kit/sections/cta-band.d.ts.map +1 -1
  19. package/dist/ui-kit/sections/cta-band.js +2 -2
  20. package/dist/ui-kit/sections/cta-band.js.map +1 -1
  21. package/dist/ui-kit/sections/hero.d.ts +43 -2
  22. package/dist/ui-kit/sections/hero.d.ts.map +1 -1
  23. package/dist/ui-kit/sections/hero.js +86 -3
  24. package/dist/ui-kit/sections/hero.js.map +1 -1
  25. package/dist/ui-kit/sections/index.d.ts +3 -2
  26. package/dist/ui-kit/sections/index.d.ts.map +1 -1
  27. package/dist/ui-kit/sections/index.js +3 -2
  28. package/dist/ui-kit/sections/index.js.map +1 -1
  29. package/dist/ui-kit/sections/proof-strip.d.ts +7 -1
  30. package/dist/ui-kit/sections/proof-strip.d.ts.map +1 -1
  31. package/dist/ui-kit/sections/proof-strip.js +6 -2
  32. package/dist/ui-kit/sections/proof-strip.js.map +1 -1
  33. package/dist/ui-kit/sections/rating-chip.d.ts +15 -5
  34. package/dist/ui-kit/sections/rating-chip.d.ts.map +1 -1
  35. package/dist/ui-kit/sections/rating-chip.js +10 -4
  36. package/dist/ui-kit/sections/rating-chip.js.map +1 -1
  37. package/dist/ui-kit/sections/section-sequence.d.ts +3 -1
  38. package/dist/ui-kit/sections/section-sequence.d.ts.map +1 -1
  39. package/dist/ui-kit/sections/section-sequence.js +4 -2
  40. package/dist/ui-kit/sections/section-sequence.js.map +1 -1
  41. package/dist/ui-kit/sections/signature-section.d.ts +42 -0
  42. package/dist/ui-kit/sections/signature-section.d.ts.map +1 -0
  43. package/dist/ui-kit/sections/signature-section.js +14 -0
  44. package/dist/ui-kit/sections/signature-section.js.map +1 -0
  45. package/dist/ui-kit/shell/call-bar-client.d.ts +2 -1
  46. package/dist/ui-kit/shell/call-bar-client.d.ts.map +1 -1
  47. package/dist/ui-kit/shell/call-bar-client.js +3 -3
  48. package/dist/ui-kit/shell/call-bar-client.js.map +1 -1
  49. package/dist/ui-kit/shell/mobile-call-bar.d.ts +6 -2
  50. package/dist/ui-kit/shell/mobile-call-bar.d.ts.map +1 -1
  51. package/dist/ui-kit/shell/mobile-call-bar.js +5 -2
  52. package/dist/ui-kit/shell/mobile-call-bar.js.map +1 -1
  53. package/dist/ui-kit/structure.css +137 -2
  54. package/package.json +2 -2
@@ -3,23 +3,33 @@
3
3
  * and a link to the Business Profile. Server component. The numbers come only from the reviews feed (T4), never typed
4
4
  * by hand; without both an average and a total of 1 or more it renders nothing. `data-bl-source="feed"` marks the
5
5
  * numbers as the feed's for the ready gate (T4, T5).
6
+ *
7
+ * 3.16.0: `layout.display: 'figure'` sets the average as a large figure ("4.9", with "out of 5" for assistive
8
+ * technology) followed by the stars and the count, as AA's hero shows it under the H1; `moreLabel` adds the site's own
9
+ * link to the Business Profile ("Read all reviews on Google"), and the count becomes plain text so the profile is
10
+ * linked once.
6
11
  */
7
12
  import type { PreparedBusinessProfileReviews } from '@brandlab-digital/site-core/google-business-reviews';
8
13
  import { type SkinEntry } from '../skin.js';
9
- export type RatingChipSlot = 'root' | 'stars' | 'rating' | 'source';
10
- export type RatingChipSkin = SkinEntry<RatingChipSlot, never, {
11
- stars?: boolean;
12
- }>;
14
+ export type RatingChipSlot = 'root' | 'stars' | 'rating' | 'figure' | 'source' | 'more';
15
+ export interface RatingChipLayout {
16
+ readonly stars?: boolean;
17
+ /** `inline` (the default): one line of words; `figure`: the average set large, then the stars and the count. */
18
+ readonly display?: 'inline' | 'figure';
19
+ }
20
+ export type RatingChipSkin = SkinEntry<RatingChipSlot, never, RatingChipLayout>;
13
21
  /** The part of the prepared reviews the chip reads: Google's average, total and profile link. */
14
22
  export type RatingChipData = Pick<PreparedBusinessProfileReviews, 'averageRating' | 'totalReviewCount' | 'profileUrl'>;
15
23
  export interface RatingChipProps extends RatingChipSkin {
16
24
  /** From `prepareBusinessProfileReviews`; null renders nothing. */
17
25
  readonly reviews: RatingChipData | null | undefined;
26
+ /** The site's words for a link to the Business Profile, for example "Read all reviews on Google". */
27
+ readonly moreLabel?: string | undefined;
18
28
  }
19
29
  /** Whether the feed gave Google's average and a total of 1 or more, so a rating can be shown. */
20
30
  export declare function hasGoogleRating(reviews: RatingChipData | null | undefined): reviews is RatingChipData & {
21
31
  averageRating: number;
22
32
  totalReviewCount: number;
23
33
  };
24
- export declare function RatingChip({ reviews, layout, classes }: RatingChipProps): import("react/jsx-runtime").JSX.Element | null;
34
+ export declare function RatingChip({ reviews, moreLabel, layout, classes }: RatingChipProps): import("react/jsx-runtime").JSX.Element | null;
25
35
  //# sourceMappingURL=rating-chip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rating-chip.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/rating-chip.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,qDAAqD,CAAA;AAEzG,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAEjD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;AACnE,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC,cAAc,EAAE,KAAK,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAElF,iGAAiG;AACjG,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,8BAA8B,EAAE,eAAe,GAAG,kBAAkB,GAAG,YAAY,CAAC,CAAA;AAEtH,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAA;CACpD;AAID,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,IAAI,cAAc,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAE3J;AAED,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,eAAe,kDAgBvE"}
1
+ {"version":3,"file":"rating-chip.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/rating-chip.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,qDAAqD,CAAA;AAEzG,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAEjD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AACvF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;IACxB,gHAAgH;IAChH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;CACvC;AACD,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC,cAAc,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAA;AAE/E,iGAAiG;AACjG,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,8BAA8B,EAAE,eAAe,GAAG,kBAAkB,GAAG,YAAY,CAAC,CAAA;AAEtH,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAA;IACnD,qGAAqG;IACrG,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC;AAID,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,IAAI,cAAc,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAE3J;AAED,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,eAAe,kDAqClF"}
@@ -1,15 +1,21 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { RatingStars } from '../primitives/layout.js';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { RatingStars, VisuallyHidden } from '../primitives/layout.js';
3
3
  import { slot } from '../skin.js';
4
4
  const COUNT = new Intl.NumberFormat('en-AU');
5
5
  /** Whether the feed gave Google's average and a total of 1 or more, so a rating can be shown. */
6
6
  export function hasGoogleRating(reviews) {
7
7
  return typeof reviews?.averageRating === 'number' && typeof reviews.totalReviewCount === 'number' && reviews.totalReviewCount > 0;
8
8
  }
9
- export function RatingChip({ reviews, layout, classes }) {
9
+ export function RatingChip({ reviews, moreLabel, layout, classes }) {
10
10
  if (!hasGoogleRating(reviews))
11
11
  return null;
12
12
  const total = reviews.totalReviewCount;
13
- return (_jsxs("p", { "data-bl": "rating-chip", "data-bl-source": "feed", className: classes?.root, children: [layout?.stars === false ? null : (_jsx("span", { ...slot(classes, 'stars'), "aria-hidden": "true", children: _jsx(RatingStars, { rating: reviews.averageRating }) })), _jsxs("span", { ...slot(classes, 'rating'), children: [reviews.averageRating.toFixed(1), " out of 5"] }), " from", ' ', _jsxs("a", { ...slot(classes, 'source'), href: reviews.profileUrl, target: "_blank", rel: "noopener noreferrer", children: [COUNT.format(total), " Google ", total === 1 ? 'review' : 'reviews'] })] }));
13
+ const figure = layout?.display === 'figure';
14
+ const average = reviews.averageRating.toFixed(1);
15
+ const stars = layout?.stars === false ? null : (_jsx("span", { ...slot(classes, 'stars'), "aria-hidden": "true", children: _jsx(RatingStars, { rating: reviews.averageRating }) }));
16
+ const count = `${COUNT.format(total)} Google ${total === 1 ? 'review' : 'reviews'}`;
17
+ const more = moreLabel?.trim();
18
+ const link = { href: reviews.profileUrl, target: '_blank', rel: 'noopener noreferrer' };
19
+ return (_jsxs("p", { "data-bl": "rating-chip", "data-bl-source": "feed", "data-display": figure ? 'figure' : undefined, className: classes?.root, children: [figure ? null : stars, figure ? (_jsxs("span", { ...slot(classes, 'rating'), children: [_jsx("span", { ...slot(classes, 'figure'), children: average }), _jsx(VisuallyHidden, { children: " out of 5" })] })) : (_jsxs("span", { ...slot(classes, 'rating'), children: [average, " out of 5"] })), figure ? stars : null, " from", ' ', more ? _jsx("span", { ...slot(classes, 'source'), children: count }) : _jsx("a", { ...slot(classes, 'source'), ...link, children: count }), more ? (_jsxs(_Fragment, { children: [' ', _jsx("a", { ...slot(classes, 'more'), ...link, children: more })] })) : null] }));
14
20
  }
15
21
  //# sourceMappingURL=rating-chip.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rating-chip.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/rating-chip.tsx"],"names":[],"mappings":";AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAajD,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;AAE5C,iGAAiG;AACjG,MAAM,UAAU,eAAe,CAAC,OAA0C;IACxE,OAAO,OAAO,OAAO,EAAE,aAAa,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,gBAAgB,KAAK,QAAQ,IAAI,OAAO,CAAC,gBAAgB,GAAG,CAAC,CAAA;AACnI,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAmB;IACtE,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1C,MAAM,KAAK,GAAG,OAAO,CAAC,gBAAgB,CAAA;IACtC,OAAO,CACL,wBAAW,aAAa,oBAAgB,MAAM,EAAC,SAAS,EAAE,OAAO,EAAE,IAAI,aACpE,MAAM,EAAE,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAChC,kBAAU,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,iBAAc,MAAM,YAClD,KAAC,WAAW,IAAC,MAAM,EAAE,OAAO,CAAC,aAAa,GAAI,GACzC,CACR,EACD,mBAAU,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,aAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,WAAM,GAAG,EAC9F,gBAAO,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,aAChG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,cAAU,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,IAC9D,IACF,CACL,CAAA;AACH,CAAC","sourcesContent":["/**\n * `RatingChip`: Google's own average and total in words (\"4.9 out of 5 from 99 Google reviews\"), with decorative stars\n * and a link to the Business Profile. Server component. The numbers come only from the reviews feed (T4), never typed\n * by hand; without both an average and a total of 1 or more it renders nothing. `data-bl-source=\"feed\"` marks the\n * numbers as the feed's for the ready gate (T4, T5).\n */\nimport type { PreparedBusinessProfileReviews } from '@brandlab-digital/site-core/google-business-reviews'\nimport { RatingStars } from '../primitives/layout.js'\nimport { slot, type SkinEntry } from '../skin.js'\n\nexport type RatingChipSlot = 'root' | 'stars' | 'rating' | 'source'\nexport type RatingChipSkin = SkinEntry<RatingChipSlot, never, { stars?: boolean }>\n\n/** The part of the prepared reviews the chip reads: Google's average, total and profile link. */\nexport type RatingChipData = Pick<PreparedBusinessProfileReviews, 'averageRating' | 'totalReviewCount' | 'profileUrl'>\n\nexport interface RatingChipProps extends RatingChipSkin {\n /** From `prepareBusinessProfileReviews`; null renders nothing. */\n readonly reviews: RatingChipData | null | undefined\n}\n\nconst COUNT = new Intl.NumberFormat('en-AU')\n\n/** Whether the feed gave Google's average and a total of 1 or more, so a rating can be shown. */\nexport function hasGoogleRating(reviews: RatingChipData | null | undefined): reviews is RatingChipData & { averageRating: number; totalReviewCount: number } {\n return typeof reviews?.averageRating === 'number' && typeof reviews.totalReviewCount === 'number' && reviews.totalReviewCount > 0\n}\n\nexport function RatingChip({ reviews, layout, classes }: RatingChipProps) {\n if (!hasGoogleRating(reviews)) return null\n const total = reviews.totalReviewCount\n return (\n <p data-bl=\"rating-chip\" data-bl-source=\"feed\" className={classes?.root}>\n {layout?.stars === false ? null : (\n <span {...slot(classes, 'stars')} aria-hidden=\"true\">\n <RatingStars rating={reviews.averageRating} />\n </span>\n )}\n <span {...slot(classes, 'rating')}>{reviews.averageRating.toFixed(1)} out of 5</span> from{' '}\n <a {...slot(classes, 'source')} href={reviews.profileUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n {COUNT.format(total)} Google {total === 1 ? 'review' : 'reviews'}\n </a>\n </p>\n )\n}\n"]}
1
+ {"version":3,"file":"rating-chip.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/rating-chip.tsx"],"names":[],"mappings":";AAYA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AACrE,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAoBjD,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;AAE5C,iGAAiG;AACjG,MAAM,UAAU,eAAe,CAAC,OAA0C;IACxE,OAAO,OAAO,OAAO,EAAE,aAAa,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,gBAAgB,KAAK,QAAQ,IAAI,OAAO,CAAC,gBAAgB,GAAG,CAAC,CAAA;AACnI,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAmB;IACjF,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1C,MAAM,KAAK,GAAG,OAAO,CAAC,gBAAgB,CAAA;IACtC,MAAM,MAAM,GAAG,MAAM,EAAE,OAAO,KAAK,QAAQ,CAAA;IAC3C,MAAM,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IAChD,MAAM,KAAK,GACT,MAAM,EAAE,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC/B,kBAAU,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,iBAAc,MAAM,YAClD,KAAC,WAAW,IAAC,MAAM,EAAE,OAAO,CAAC,aAAa,GAAI,GACzC,CACR,CAAA;IACH,MAAM,KAAK,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;IACnF,MAAM,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,CAAA;IAC9B,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,qBAAqB,EAAW,CAAA;IAChG,OAAO,CACL,wBAAW,aAAa,oBAAgB,MAAM,kBAAe,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,aACjH,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EACrB,MAAM,CAAC,CAAC,CAAC,CACR,mBAAU,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,aAC/B,kBAAU,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAG,OAAO,GAAQ,EACnD,KAAC,cAAc,4BAA2B,IACrC,CACR,CAAC,CAAC,CAAC,CACF,mBAAU,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,aAAG,OAAO,iBAAiB,CAC7D,EACA,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,WAAO,GAAG,EAC/B,IAAI,CAAC,CAAC,CAAC,kBAAU,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAG,KAAK,GAAQ,CAAC,CAAC,CAAC,eAAO,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAM,IAAI,YAAG,KAAK,GAAK,EAChH,IAAI,CAAC,CAAC,CAAC,CACN,8BACG,GAAG,EACJ,eAAO,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,KAAM,IAAI,YACnC,IAAI,GACH,IACH,CACJ,CAAC,CAAC,CAAC,IAAI,IACN,CACL,CAAA;AACH,CAAC","sourcesContent":["/**\n * `RatingChip`: Google's own average and total in words (\"4.9 out of 5 from 99 Google reviews\"), with decorative stars\n * and a link to the Business Profile. Server component. The numbers come only from the reviews feed (T4), never typed\n * by hand; without both an average and a total of 1 or more it renders nothing. `data-bl-source=\"feed\"` marks the\n * numbers as the feed's for the ready gate (T4, T5).\n *\n * 3.16.0: `layout.display: 'figure'` sets the average as a large figure (\"4.9\", with \"out of 5\" for assistive\n * technology) followed by the stars and the count, as AA's hero shows it under the H1; `moreLabel` adds the site's own\n * link to the Business Profile (\"Read all reviews on Google\"), and the count becomes plain text so the profile is\n * linked once.\n */\nimport type { PreparedBusinessProfileReviews } from '@brandlab-digital/site-core/google-business-reviews'\nimport { RatingStars, VisuallyHidden } from '../primitives/layout.js'\nimport { slot, type SkinEntry } from '../skin.js'\n\nexport type RatingChipSlot = 'root' | 'stars' | 'rating' | 'figure' | 'source' | 'more'\nexport interface RatingChipLayout {\n readonly stars?: boolean\n /** `inline` (the default): one line of words; `figure`: the average set large, then the stars and the count. */\n readonly display?: 'inline' | 'figure'\n}\nexport type RatingChipSkin = SkinEntry<RatingChipSlot, never, RatingChipLayout>\n\n/** The part of the prepared reviews the chip reads: Google's average, total and profile link. */\nexport type RatingChipData = Pick<PreparedBusinessProfileReviews, 'averageRating' | 'totalReviewCount' | 'profileUrl'>\n\nexport interface RatingChipProps extends RatingChipSkin {\n /** From `prepareBusinessProfileReviews`; null renders nothing. */\n readonly reviews: RatingChipData | null | undefined\n /** The site's words for a link to the Business Profile, for example \"Read all reviews on Google\". */\n readonly moreLabel?: string | undefined\n}\n\nconst COUNT = new Intl.NumberFormat('en-AU')\n\n/** Whether the feed gave Google's average and a total of 1 or more, so a rating can be shown. */\nexport function hasGoogleRating(reviews: RatingChipData | null | undefined): reviews is RatingChipData & { averageRating: number; totalReviewCount: number } {\n return typeof reviews?.averageRating === 'number' && typeof reviews.totalReviewCount === 'number' && reviews.totalReviewCount > 0\n}\n\nexport function RatingChip({ reviews, moreLabel, layout, classes }: RatingChipProps) {\n if (!hasGoogleRating(reviews)) return null\n const total = reviews.totalReviewCount\n const figure = layout?.display === 'figure'\n const average = reviews.averageRating.toFixed(1)\n const stars =\n layout?.stars === false ? null : (\n <span {...slot(classes, 'stars')} aria-hidden=\"true\">\n <RatingStars rating={reviews.averageRating} />\n </span>\n )\n const count = `${COUNT.format(total)} Google ${total === 1 ? 'review' : 'reviews'}`\n const more = moreLabel?.trim()\n const link = { href: reviews.profileUrl, target: '_blank', rel: 'noopener noreferrer' } as const\n return (\n <p data-bl=\"rating-chip\" data-bl-source=\"feed\" data-display={figure ? 'figure' : undefined} className={classes?.root}>\n {figure ? null : stars}\n {figure ? (\n <span {...slot(classes, 'rating')}>\n <span {...slot(classes, 'figure')}>{average}</span>\n <VisuallyHidden> out of 5</VisuallyHidden>\n </span>\n ) : (\n <span {...slot(classes, 'rating')}>{average} out of 5</span>\n )}\n {figure ? stars : null} from{' '}\n {more ? <span {...slot(classes, 'source')}>{count}</span> : <a {...slot(classes, 'source')} {...link}>{count}</a>}\n {more ? (\n <>\n {' '}\n <a {...slot(classes, 'more')} {...link}>\n {more}\n </a>\n </>\n ) : null}\n </p>\n )\n}\n"]}
@@ -5,7 +5,9 @@
5
5
  * its reason without rendering twice. Then the cap drops optional sections first, the light tones alternate so
6
6
  * neighbours never share a background (at most one `band` and one `photo`), and the page's manifest is written as
7
7
  * `<script type="application/json" data-bl-manifest>` (`SECTION_MANIFEST_ATTRIBUTE`) for K12: what was planned, what
8
- * rendered with which tone, and what was skipped and why.
8
+ * rendered with which tone, and what was skipped and why. The strips (proof strip, emergency band) and signature
9
+ * sections don't count toward the cap (3.16.0); a `signature` section names itself and may say which section it
10
+ * follows: `{ id: 'signature', signature: 'storm-line', after: 'services', render: (tone) => <StormLine tone={tone} /> }`.
9
11
  *
10
12
  * ```tsx
11
13
  * <SectionSequence
@@ -1 +1 @@
1
- {"version":3,"file":"section-sequence.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/section-sequence.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEhD,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC/B,MAAM,2CAA2C,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAE1D,oHAAoH;AACpH,MAAM,WAAW,eAAgB,SAAQ,yBAAyB;IAChE,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAA;CAClD;AAED,MAAM,WAAW,oBAAoB;IACnC,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;IAC7C,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,yFAAyF;AACzF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,uBAAuB,GAAG,MAAM,CAElF;AAED,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,oBAAoB,2CAc9E"}
1
+ {"version":3,"file":"section-sequence.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/section-sequence.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEhD,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC/B,MAAM,2CAA2C,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAE1D,oHAAoH;AACpH,MAAM,WAAW,eAAgB,SAAQ,yBAAyB;IAChE,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAA;CAClD;AAED,MAAM,WAAW,oBAAoB;IACnC,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;IAC7C,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,yFAAyF;AACzF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,uBAAuB,GAAG,MAAM,CAElF;AAED,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,oBAAoB,2CAc9E"}
@@ -6,7 +6,9 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
6
6
  * its reason without rendering twice. Then the cap drops optional sections first, the light tones alternate so
7
7
  * neighbours never share a background (at most one `band` and one `photo`), and the page's manifest is written as
8
8
  * `<script type="application/json" data-bl-manifest>` (`SECTION_MANIFEST_ATTRIBUTE`) for K12: what was planned, what
9
- * rendered with which tone, and what was skipped and why.
9
+ * rendered with which tone, and what was skipped and why. The strips (proof strip, emergency band) and signature
10
+ * sections don't count toward the cap (3.16.0); a `signature` section names itself and may say which section it
11
+ * follows: `{ id: 'signature', signature: 'storm-line', after: 'services', render: (tone) => <StormLine tone={tone} /> }`.
10
12
  *
11
13
  * ```tsx
12
14
  * <SectionSequence
@@ -29,6 +31,6 @@ export function serialiseSectionManifest(manifest) {
29
31
  }
30
32
  export function SectionSequence({ order, sections, page }) {
31
33
  const plan = planSectionSequence(order, sections, page);
32
- return (_jsxs(_Fragment, { children: [plan.rendered.map((item) => (_jsx(Fragment, { children: sections[item.candidate]?.render(item.tone) }, item.id))), _jsx("script", { type: "application/json", [SECTION_MANIFEST_ATTRIBUTE]: '', dangerouslySetInnerHTML: { __html: serialiseSectionManifest(plan.manifest) } })] }));
34
+ return (_jsxs(_Fragment, { children: [plan.rendered.map((item) => (_jsx(Fragment, { children: sections[item.candidate]?.render(item.tone) }, `${item.id}-${item.candidate}`))), _jsx("script", { type: "application/json", [SECTION_MANIFEST_ATTRIBUTE]: '', dangerouslySetInnerHTML: { __html: serialiseSectionManifest(plan.manifest) } })] }));
33
35
  }
34
36
  //# sourceMappingURL=section-sequence.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"section-sequence.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/section-sequence.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AACnE,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,GAIpB,MAAM,2CAA2C,CAAA;AAgBlD,yFAAyF;AACzF,MAAM,UAAU,wBAAwB,CAAC,QAAiC;IACxE,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAA;AACjC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAwB;IAC7E,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;IACvD,OAAO,CACL,8BACG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAC3B,KAAC,QAAQ,cAAgB,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAArD,IAAI,CAAC,EAAE,CAA0D,CACjF,CAAC,EACF,iBACE,IAAI,EAAC,kBAAkB,EACjB,CAAC,0BAA0B,CAAC,EAAE,EAAE,EACtC,uBAAuB,EAAE,EAAE,MAAM,EAAE,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,GAC5E,IACD,CACJ,CAAA;AACH,CAAC","sourcesContent":["/**\n * `SectionSequence` (F15): renders a page family's sections in order. Server component, ported from Omega's\n * `Sequence` and `alternateTones`. Each section the page offers comes with its guard, computed from the same data it\n * renders (`faqsReady(props)`, `priceGuideReady(props)` …), so a section that would render nothing is skipped with\n * its reason without rendering twice. Then the cap drops optional sections first, the light tones alternate so\n * neighbours never share a background (at most one `band` and one `photo`), and the page's manifest is written as\n * `<script type=\"application/json\" data-bl-manifest>` (`SECTION_MANIFEST_ATTRIBUTE`) for K12: what was planned, what\n * rendered with which tone, and what was skipped and why.\n *\n * ```tsx\n * <SectionSequence\n * order={orders.service}\n * page=\"/tree-removal\"\n * sections={[\n * { id: 'hero', tone: 'photo', render: (tone) => <Hero tone={tone} {...hero} /> },\n * { id: 'price-guide', ready: priceGuideReady(guide), render: (tone) => <PriceGuide tone={tone} {...guide} /> },\n * { id: 'cta', tone: 'band', render: (tone) => <CTABand tone={tone} {...cta} /> },\n * ]}\n * />\n * ```\n */\nimport { Fragment, type ReactNode } from 'react'\nimport { sanitizeJsonLd } from '@brandlab-digital/site-core/schema'\nimport {\n SECTION_MANIFEST_ATTRIBUTE,\n planSectionSequence,\n type BrandlabFamilyOrder,\n type BrandlabSectionManifest,\n type BrandlabSequenceCandidate,\n} from '@brandlab-digital/site-core/section-order'\nimport type { SectionTone } from '../primitives/layout.js'\n\n/** One section the page offers: its matrix id, its guard, the tone it asks for and how to render it with a tone. */\nexport interface SequenceSection extends BrandlabSequenceCandidate {\n readonly render: (tone: SectionTone) => ReactNode\n}\n\nexport interface SectionSequenceProps {\n /** The family's order, from the site's `defineSectionOrder` (or `defaultSectionOrder`). */\n readonly order: BrandlabFamilyOrder\n readonly sections: readonly SequenceSection[]\n /** The page path, recorded in the manifest. */\n readonly page?: string\n}\n\n/** The manifest as safe inline JSON (no `<`, `>` or `&` that could close the script). */\nexport function serialiseSectionManifest(manifest: BrandlabSectionManifest): string {\n return sanitizeJsonLd(manifest)\n}\n\nexport function SectionSequence({ order, sections, page }: SectionSequenceProps) {\n const plan = planSectionSequence(order, sections, page)\n return (\n <>\n {plan.rendered.map((item) => (\n <Fragment key={item.id}>{sections[item.candidate]?.render(item.tone)}</Fragment>\n ))}\n <script\n type=\"application/json\"\n {...{ [SECTION_MANIFEST_ATTRIBUTE]: '' }}\n dangerouslySetInnerHTML={{ __html: serialiseSectionManifest(plan.manifest) }}\n />\n </>\n )\n}\n"]}
1
+ {"version":3,"file":"section-sequence.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/section-sequence.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AACnE,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,GAIpB,MAAM,2CAA2C,CAAA;AAgBlD,yFAAyF;AACzF,MAAM,UAAU,wBAAwB,CAAC,QAAiC;IACxE,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAA;AACjC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAwB;IAC7E,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;IACvD,OAAO,CACL,8BACG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAC3B,KAAC,QAAQ,cAAuC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAA5E,GAAG,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,SAAS,EAAE,CAA0D,CACxG,CAAC,EACF,iBACE,IAAI,EAAC,kBAAkB,EACjB,CAAC,0BAA0B,CAAC,EAAE,EAAE,EACtC,uBAAuB,EAAE,EAAE,MAAM,EAAE,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,GAC5E,IACD,CACJ,CAAA;AACH,CAAC","sourcesContent":["/**\n * `SectionSequence` (F15): renders a page family's sections in order. Server component, ported from Omega's\n * `Sequence` and `alternateTones`. Each section the page offers comes with its guard, computed from the same data it\n * renders (`faqsReady(props)`, `priceGuideReady(props)` …), so a section that would render nothing is skipped with\n * its reason without rendering twice. Then the cap drops optional sections first, the light tones alternate so\n * neighbours never share a background (at most one `band` and one `photo`), and the page's manifest is written as\n * `<script type=\"application/json\" data-bl-manifest>` (`SECTION_MANIFEST_ATTRIBUTE`) for K12: what was planned, what\n * rendered with which tone, and what was skipped and why. The strips (proof strip, emergency band) and signature\n * sections don't count toward the cap (3.16.0); a `signature` section names itself and may say which section it\n * follows: `{ id: 'signature', signature: 'storm-line', after: 'services', render: (tone) => <StormLine tone={tone} /> }`.\n *\n * ```tsx\n * <SectionSequence\n * order={orders.service}\n * page=\"/tree-removal\"\n * sections={[\n * { id: 'hero', tone: 'photo', render: (tone) => <Hero tone={tone} {...hero} /> },\n * { id: 'price-guide', ready: priceGuideReady(guide), render: (tone) => <PriceGuide tone={tone} {...guide} /> },\n * { id: 'cta', tone: 'band', render: (tone) => <CTABand tone={tone} {...cta} /> },\n * ]}\n * />\n * ```\n */\nimport { Fragment, type ReactNode } from 'react'\nimport { sanitizeJsonLd } from '@brandlab-digital/site-core/schema'\nimport {\n SECTION_MANIFEST_ATTRIBUTE,\n planSectionSequence,\n type BrandlabFamilyOrder,\n type BrandlabSectionManifest,\n type BrandlabSequenceCandidate,\n} from '@brandlab-digital/site-core/section-order'\nimport type { SectionTone } from '../primitives/layout.js'\n\n/** One section the page offers: its matrix id, its guard, the tone it asks for and how to render it with a tone. */\nexport interface SequenceSection extends BrandlabSequenceCandidate {\n readonly render: (tone: SectionTone) => ReactNode\n}\n\nexport interface SectionSequenceProps {\n /** The family's order, from the site's `defineSectionOrder` (or `defaultSectionOrder`). */\n readonly order: BrandlabFamilyOrder\n readonly sections: readonly SequenceSection[]\n /** The page path, recorded in the manifest. */\n readonly page?: string\n}\n\n/** The manifest as safe inline JSON (no `<`, `>` or `&` that could close the script). */\nexport function serialiseSectionManifest(manifest: BrandlabSectionManifest): string {\n return sanitizeJsonLd(manifest)\n}\n\nexport function SectionSequence({ order, sections, page }: SectionSequenceProps) {\n const plan = planSectionSequence(order, sections, page)\n return (\n <>\n {plan.rendered.map((item) => (\n <Fragment key={`${item.id}-${item.candidate}`}>{sections[item.candidate]?.render(item.tone)}</Fragment>\n ))}\n <script\n type=\"application/json\"\n {...{ [SECTION_MANIFEST_ATTRIBUTE]: '' }}\n dangerouslySetInnerHTML={{ __html: serialiseSectionManifest(plan.manifest) }}\n />\n </>\n )\n}\n"]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `SignatureSection` (`signature`, 3.16.0): the frame for one of a client's signature features, the sections that give
3
+ * a site its own character (Superior's waterline story, AA's storm line, Finishing Touch's `FinishBoard`). Server
4
+ * component. The site builds the body from the library's primitives in `src/site/signature/`; the frame keeps the
5
+ * rules every section keeps:
6
+ * - a `<section data-bl-section="signature" data-bl-signature="<name>">` labelled by its one H2, so K8, K11 and axe read
7
+ * it like any other section;
8
+ * - the name matches the `signature` the page offers `SectionSequence` and the look record in `strategy.ts`
9
+ * (`homeSections.signatures`), which D4 compares with the competitors' and K13 holds the rendered page to;
10
+ * - decoration is `aria-hidden`, and the body carries no client JS of its own (B1 budgets the page);
11
+ * - claims inside it come from the business facts (`renderableCredentials`) or the reviews feed, as everywhere else
12
+ * (T4, E8): a signature adds character, never an unsupported claim.
13
+ *
14
+ * `SectionSequence` places it where the site declares (`{ id: 'signature', signature: 'storm-line', after: 'services',
15
+ * render }`), outside the cap: up to two on the home page and one on service pages (`SIGNATURE_LIMITS`).
16
+ */
17
+ import type { ReactNode } from 'react';
18
+ import { type SectionSlot, type SectionTone } from '../primitives/layout.js';
19
+ import { type SkinEntry } from '../skin.js';
20
+ export type SignatureSectionSlot = SectionSlot | 'head' | 'eyebrow' | 'heading' | 'lead' | 'body';
21
+ export interface SignatureSectionLayout {
22
+ readonly align?: 'start' | 'center';
23
+ }
24
+ export type SignatureSectionSkin = SkinEntry<SignatureSectionSlot, never, SignatureSectionLayout>;
25
+ export interface SignatureSectionProps extends SignatureSectionSkin {
26
+ /** The signature's name, as the page offers it to `SectionSequence` and `strategy.ts` records it: `storm-line`. */
27
+ readonly name: string;
28
+ /** The H2. */
29
+ readonly title: string;
30
+ readonly eyebrow?: string;
31
+ readonly lead?: string;
32
+ /** Defaults to `signature-<name>-heading`. */
33
+ readonly headingId?: string;
34
+ /** Set by `SectionSequence`. */
35
+ readonly tone?: SectionTone;
36
+ /** Signature art, rendered `aria-hidden`. */
37
+ readonly decoration?: ReactNode;
38
+ /** The feature itself, built from the library's primitives. */
39
+ readonly children: ReactNode;
40
+ }
41
+ export declare function SignatureSection({ name, title, eyebrow, lead, headingId, tone, decoration, layout, classes, children }: SignatureSectionProps): import("react/jsx-runtime").JSX.Element;
42
+ //# sourceMappingURL=signature-section.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signature-section.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/signature-section.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAA2B,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrG,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAEjD,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAA;AACjG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;CACpC;AACD,MAAM,MAAM,oBAAoB,GAAG,SAAS,CAAC,oBAAoB,EAAE,KAAK,EAAE,sBAAsB,CAAC,CAAA;AAEjG,MAAM,WAAW,qBAAsB,SAAQ,oBAAoB;IACjE,mHAAmH;IACnH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,cAAc;IACd,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;IAC3B,6CAA6C;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,CAAA;IAC/B,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,qBAAqB,2CAmB7I"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SIGNATURE_NAME_PATTERN } from '@brandlab-digital/site-core/section-order';
3
+ import { Section, SectionHeading } from '../primitives/layout.js';
4
+ import { slot } from '../skin.js';
5
+ export function SignatureSection({ name, title, eyebrow, lead, headingId, tone, decoration, layout, classes, children }) {
6
+ if (!SIGNATURE_NAME_PATTERN.test(name)) {
7
+ throw new Error(`SignatureSection: "${name}" is not a signature name (lower-case words joined by hyphens, for example "storm-line").`);
8
+ }
9
+ if (title.trim() === '')
10
+ throw new Error(`SignatureSection "${name}": the heading is empty.`);
11
+ const id = headingId ?? `signature-${name}-heading`;
12
+ return (_jsxs(Section, { id: "signature", labelledBy: id, tone: tone, classes: classes, decoration: decoration, hooks: { 'data-bl-signature': name }, children: [_jsx(SectionHeading, { id: id, title: title, eyebrow: eyebrow, lead: lead, layout: { align: layout?.align ?? 'start' }, classes: { head: classes?.head, eyebrow: classes?.eyebrow, heading: classes?.heading, lead: classes?.lead } }), _jsx("div", { ...slot(classes, 'body'), children: children })] }));
13
+ }
14
+ //# sourceMappingURL=signature-section.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signature-section.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/signature-section.tsx"],"names":[],"mappings":";AAiBA,OAAO,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAA;AAClF,OAAO,EAAE,OAAO,EAAE,cAAc,EAAsC,MAAM,yBAAyB,CAAA;AACrG,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAyBjD,MAAM,UAAU,gBAAgB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAyB;IAC5I,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,2FAA2F,CAAC,CAAA;IACxI,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,0BAA0B,CAAC,CAAA;IAC7F,MAAM,EAAE,GAAG,SAAS,IAAI,aAAa,IAAI,UAAU,CAAA;IACnD,OAAO,CACL,MAAC,OAAO,IAAC,EAAE,EAAC,WAAW,EAAC,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,aAChI,KAAC,cAAc,IACb,EAAE,EAAE,EAAE,EACN,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,OAAO,EAAE,EAC3C,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAC3G,EACF,iBAAS,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAAG,QAAQ,GAAO,IACxC,CACX,CAAA;AACH,CAAC","sourcesContent":["/**\n * `SignatureSection` (`signature`, 3.16.0): the frame for one of a client's signature features, the sections that give\n * a site its own character (Superior's waterline story, AA's storm line, Finishing Touch's `FinishBoard`). Server\n * component. The site builds the body from the library's primitives in `src/site/signature/`; the frame keeps the\n * rules every section keeps:\n * - a `<section data-bl-section=\"signature\" data-bl-signature=\"<name>\">` labelled by its one H2, so K8, K11 and axe read\n * it like any other section;\n * - the name matches the `signature` the page offers `SectionSequence` and the look record in `strategy.ts`\n * (`homeSections.signatures`), which D4 compares with the competitors' and K13 holds the rendered page to;\n * - decoration is `aria-hidden`, and the body carries no client JS of its own (B1 budgets the page);\n * - claims inside it come from the business facts (`renderableCredentials`) or the reviews feed, as everywhere else\n * (T4, E8): a signature adds character, never an unsupported claim.\n *\n * `SectionSequence` places it where the site declares (`{ id: 'signature', signature: 'storm-line', after: 'services',\n * render }`), outside the cap: up to two on the home page and one on service pages (`SIGNATURE_LIMITS`).\n */\nimport type { ReactNode } from 'react'\nimport { SIGNATURE_NAME_PATTERN } from '@brandlab-digital/site-core/section-order'\nimport { Section, SectionHeading, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport { slot, type SkinEntry } from '../skin.js'\n\nexport type SignatureSectionSlot = SectionSlot | 'head' | 'eyebrow' | 'heading' | 'lead' | 'body'\nexport interface SignatureSectionLayout {\n readonly align?: 'start' | 'center'\n}\nexport type SignatureSectionSkin = SkinEntry<SignatureSectionSlot, never, SignatureSectionLayout>\n\nexport interface SignatureSectionProps extends SignatureSectionSkin {\n /** The signature's name, as the page offers it to `SectionSequence` and `strategy.ts` records it: `storm-line`. */\n readonly name: string\n /** The H2. */\n readonly title: string\n readonly eyebrow?: string\n readonly lead?: string\n /** Defaults to `signature-<name>-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n /** The feature itself, built from the library's primitives. */\n readonly children: ReactNode\n}\n\nexport function SignatureSection({ name, title, eyebrow, lead, headingId, tone, decoration, layout, classes, children }: SignatureSectionProps) {\n if (!SIGNATURE_NAME_PATTERN.test(name)) {\n throw new Error(`SignatureSection: \"${name}\" is not a signature name (lower-case words joined by hyphens, for example \"storm-line\").`)\n }\n if (title.trim() === '') throw new Error(`SignatureSection \"${name}\": the heading is empty.`)\n const id = headingId ?? `signature-${name}-heading`\n return (\n <Section id=\"signature\" labelledBy={id} tone={tone} classes={classes} decoration={decoration} hooks={{ 'data-bl-signature': name }}>\n <SectionHeading\n id={id}\n title={title}\n eyebrow={eyebrow}\n lead={lead}\n layout={{ align: layout?.align ?? 'start' }}\n classes={{ head: classes?.head, eyebrow: classes?.eyebrow, heading: classes?.heading, lead: classes?.lead }}\n />\n <div {...slot(classes, 'body')}>{children}</div>\n </Section>\n )\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
- export declare function CallBarController({ variant, className, children }: {
2
+ export declare function CallBarController({ variant, always, className, children, }: {
3
3
  readonly variant: string;
4
+ readonly always?: boolean | undefined;
4
5
  readonly className?: string | undefined;
5
6
  readonly children: ReactNode;
6
7
  }): import("react/jsx-runtime").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"call-bar-client.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/call-bar-client.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAG9C,wBAAgB,iBAAiB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAStK"}
1
+ {"version":3,"file":"call-bar-client.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/call-bar-client.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAG9C,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,MAAM,EACN,SAAS,EACT,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,2CAiBA"}
@@ -4,10 +4,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  import { usePathname } from 'next/navigation';
5
5
  import { useRef } from 'react';
6
6
  import { useStickyCallBar } from '../../navigation/use-sticky-call-bar.js';
7
- export function CallBarController({ variant, className, children }) {
7
+ export function CallBarController({ variant, always, className, children, }) {
8
8
  const ref = useRef(null);
9
9
  const pathname = usePathname();
10
- const shown = useStickyCallBar(ref, pathname);
11
- return (_jsx("div", { ref: ref, "data-bl": "callbar", "data-variant": variant, "data-state": shown ? 'shown' : 'hidden', inert: !shown, className: className, children: children }));
10
+ const shown = useStickyCallBar(ref, pathname, { always: always === true });
11
+ return (_jsx("div", { ref: ref, "data-bl": "callbar", "data-variant": variant, "data-show": always ? 'always' : undefined, "data-state": shown ? 'shown' : 'hidden', inert: !shown, className: className, children: children }));
12
12
  }
13
13
  //# sourceMappingURL=call-bar-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"call-bar-client.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/call-bar-client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,qHAAqH;AACrH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAA;AAE1E,MAAM,UAAU,iBAAiB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAuG;IACrK,MAAM,GAAG,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IACxC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IAC7C,OAAO,CACL,cAAK,GAAG,EAAE,GAAG,aAAU,SAAS,kBAAe,OAAO,gBAAc,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,YAChI,QAAQ,GACL,CACP,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\n/** The call bar's client leaf: shows and hides the server-rendered Call and quote buttons (F6 rules in the hook). */\nimport { usePathname } from 'next/navigation'\nimport { useRef, type ReactNode } from 'react'\nimport { useStickyCallBar } from '../../navigation/use-sticky-call-bar.js'\n\nexport function CallBarController({ variant, className, children }: { readonly variant: string; readonly className?: string | undefined; readonly children: ReactNode }) {\n const ref = useRef<HTMLDivElement>(null)\n const pathname = usePathname()\n const shown = useStickyCallBar(ref, pathname)\n return (\n <div ref={ref} data-bl=\"callbar\" data-variant={variant} data-state={shown ? 'shown' : 'hidden'} inert={!shown} className={className}>\n {children}\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"call-bar-client.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/call-bar-client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,qHAAqH;AACrH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAA;AAE1E,MAAM,UAAU,iBAAiB,CAAC,EAChC,OAAO,EACP,MAAM,EACN,SAAS,EACT,QAAQ,GAMT;IACC,MAAM,GAAG,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IACxC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,EAAE,CAAC,CAAA;IAC1E,OAAO,CACL,cACE,GAAG,EAAE,GAAG,aACA,SAAS,kBACH,OAAO,eACV,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,gBAC5B,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EACtC,KAAK,EAAE,CAAC,KAAK,EACb,SAAS,EAAE,SAAS,YAEnB,QAAQ,GACL,CACP,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\n/** The call bar's client leaf: shows and hides the server-rendered Call and quote buttons (F6 rules in the hook). */\nimport { usePathname } from 'next/navigation'\nimport { useRef, type ReactNode } from 'react'\nimport { useStickyCallBar } from '../../navigation/use-sticky-call-bar.js'\n\nexport function CallBarController({\n variant,\n always,\n className,\n children,\n}: {\n readonly variant: string\n readonly always?: boolean | undefined\n readonly className?: string | undefined\n readonly children: ReactNode\n}) {\n const ref = useRef<HTMLDivElement>(null)\n const pathname = usePathname()\n const shown = useStickyCallBar(ref, pathname, { always: always === true })\n return (\n <div\n ref={ref}\n data-bl=\"callbar\"\n data-variant={variant}\n data-show={always ? 'always' : undefined}\n data-state={shown ? 'shown' : 'hidden'}\n inert={!shown}\n className={className}\n >\n {children}\n </div>\n )\n}\n"]}
@@ -5,7 +5,11 @@ export type MobileCallBarSlot = 'root' | 'call' | 'quote';
5
5
  export declare const MOBILE_CALL_BAR_VARIANTS: readonly ["bar", "float"];
6
6
  /** `bar`: full width; `float`: two pills at the end. */
7
7
  export type MobileCallBarVariant = (typeof MOBILE_CALL_BAR_VARIANTS)[number];
8
- export type MobileCallBarSkin = SkinEntry<MobileCallBarSlot, MobileCallBarVariant>;
8
+ export interface MobileCallBarLayout {
9
+ /** `after-call-path` (the default): once the hero's buttons scroll out; `always`: from the first phone screen. */
10
+ readonly show?: 'after-call-path' | 'always';
11
+ }
12
+ export type MobileCallBarSkin = SkinEntry<MobileCallBarSlot, MobileCallBarVariant, MobileCallBarLayout>;
9
13
  export interface MobileCallBarProps extends MobileCallBarSkin {
10
14
  readonly phone: UiPhone;
11
15
  /** Words before the number, for example "Call". The button's name always includes the number. */
@@ -17,5 +21,5 @@ export interface MobileCallBarProps extends MobileCallBarSkin {
17
21
  */
18
22
  readonly compactLabel?: string | undefined;
19
23
  }
20
- export declare function MobileCallBar({ phone, phoneLabel, quote, compactLabel, variant, classes }: MobileCallBarProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function MobileCallBar({ phone, phoneLabel, quote, compactLabel, variant, layout, classes }: MobileCallBarProps): import("react/jsx-runtime").JSX.Element;
21
25
  //# sourceMappingURL=mobile-call-bar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-call-bar.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/mobile-call-bar.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAE1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AACzD,eAAO,MAAM,wBAAwB,2BAA4B,CAAA;AACjE,wDAAwD;AACxD,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAA;AAC5E,MAAM,MAAM,iBAAiB,GAAG,SAAS,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,CAAA;AAElF,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,iGAAiG;IACjG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC3C;AAED,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,OAAe,EAAE,OAAO,EAAE,EAAE,kBAAkB,2CAOrH"}
1
+ {"version":3,"file":"mobile-call-bar.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/mobile-call-bar.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAE1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AACzD,eAAO,MAAM,wBAAwB,2BAA4B,CAAA;AACjE,wDAAwD;AACxD,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAA;AAC5E,MAAM,WAAW,mBAAmB;IAClC,kHAAkH;IAClH,QAAQ,CAAC,IAAI,CAAC,EAAE,iBAAiB,GAAG,QAAQ,CAAA;CAC7C;AACD,MAAM,MAAM,iBAAiB,GAAG,SAAS,CAAC,iBAAiB,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,CAAA;AAEvG,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,iGAAiG;IACjG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC3C;AAED,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,OAAe,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,2CAO7H"}
@@ -7,12 +7,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  *
8
8
  * Both buttons stay on one line down to 360 px (3.15.0): the number never wraps or truncates; below 420 px the verb
9
9
  * ("Call") is visually hidden (the link's name keeps it) and the quote button shows `compactLabel` when given.
10
+ *
11
+ * `layout.show: 'always'` (3.16.0, as AA's live site): the bar shows from the first phone screen, beside the hero's
12
+ * buttons, still Call before Quote (K6), and still hides over the footer, a dialog and a focused field.
10
13
  */
11
14
  import { PhoneLink } from '../primitives/links.js';
12
15
  import { ShellQuoteButton } from './shell-quote.js';
13
16
  import { CallBarController } from './call-bar-client.js';
14
17
  export const MOBILE_CALL_BAR_VARIANTS = ['bar', 'float'];
15
- export function MobileCallBar({ phone, phoneLabel, quote, compactLabel, variant = 'bar', classes }) {
16
- return (_jsxs(CallBarController, { variant: variant, className: classes?.root, children: [_jsx(PhoneLink, { phone: phone, placement: "fab", label: phoneLabel, showIcon: true, variant: "button", classes: { root: classes?.call } }), _jsx(ShellQuoteButton, { label: quote.label, href: quote.href, placement: "floating", ctaId: "quote-callbar", compactLabel: compactLabel, classes: { root: classes?.quote } })] }));
18
+ export function MobileCallBar({ phone, phoneLabel, quote, compactLabel, variant = 'bar', layout, classes }) {
19
+ return (_jsxs(CallBarController, { variant: variant, always: layout?.show === 'always', className: classes?.root, children: [_jsx(PhoneLink, { phone: phone, placement: "fab", label: phoneLabel, showIcon: true, variant: "button", classes: { root: classes?.call } }), _jsx(ShellQuoteButton, { label: quote.label, href: quote.href, placement: "floating", ctaId: "quote-callbar", compactLabel: compactLabel, classes: { root: classes?.quote } })] }));
17
20
  }
18
21
  //# sourceMappingURL=mobile-call-bar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-call-bar.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/mobile-call-bar.tsx"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAGnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAIxD,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,KAAK,EAAE,OAAO,CAAU,CAAA;AAiBjE,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,GAAG,KAAK,EAAE,OAAO,EAAsB;IACpH,OAAO,CACL,MAAC,iBAAiB,IAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,aAC3D,KAAC,SAAS,IAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAC,KAAK,EAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,QAAC,OAAO,EAAC,QAAQ,EAAC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAI,EAC1H,KAAC,gBAAgB,IAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAC,UAAU,EAAC,KAAK,EAAC,eAAe,EAAC,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAI,IAClJ,CACrB,CAAA;AACH,CAAC","sourcesContent":["/**\n * F6: the phone call bar (server component). Call and quote always one tap away below 1024 px: it shows once the\n * hero's own call and quote buttons (`data-bl-call-path`) have scrolled out, and hides over the footer, the drawer, a\n * dialog, any call path and while a form field has focus. Call reports placement `fab`; the quote button sends\n * `lead_cta_click` with placement `floating`. Render it once, after the page's `<main>`.\n *\n * Both buttons stay on one line down to 360 px (3.15.0): the number never wraps or truncates; below 420 px the verb\n * (\"Call\") is visually hidden (the link's name keeps it) and the quote button shows `compactLabel` when given.\n */\nimport { PhoneLink } from '../primitives/links.js'\nimport { ShellQuoteButton } from './shell-quote.js'\nimport type { SkinEntry } from '../skin.js'\nimport type { UiPhone } from '../types.js'\nimport { CallBarController } from './call-bar-client.js'\nimport type { ShellQuote } from './mobile-drawer.js'\n\nexport type MobileCallBarSlot = 'root' | 'call' | 'quote'\nexport const MOBILE_CALL_BAR_VARIANTS = ['bar', 'float'] as const\n/** `bar`: full width; `float`: two pills at the end. */\nexport type MobileCallBarVariant = (typeof MOBILE_CALL_BAR_VARIANTS)[number]\nexport type MobileCallBarSkin = SkinEntry<MobileCallBarSlot, MobileCallBarVariant>\n\nexport interface MobileCallBarProps extends MobileCallBarSkin {\n readonly phone: UiPhone\n /** Words before the number, for example \"Call\". The button's name always includes the number. */\n readonly phoneLabel?: string | undefined\n readonly quote: ShellQuote\n /**\n * A shorter quote label for phones narrower than 420 px, for example \"Free quote\" for \"Get a Free Quote\". It must be\n * part of the quote label; the button's text and name stay the full label.\n */\n readonly compactLabel?: string | undefined\n}\n\nexport function MobileCallBar({ phone, phoneLabel, quote, compactLabel, variant = 'bar', classes }: MobileCallBarProps) {\n return (\n <CallBarController variant={variant} className={classes?.root}>\n <PhoneLink phone={phone} placement=\"fab\" label={phoneLabel} showIcon variant=\"button\" classes={{ root: classes?.call }} />\n <ShellQuoteButton label={quote.label} href={quote.href} placement=\"floating\" ctaId=\"quote-callbar\" compactLabel={compactLabel} classes={{ root: classes?.quote }} />\n </CallBarController>\n )\n}\n"]}
1
+ {"version":3,"file":"mobile-call-bar.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/mobile-call-bar.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAGnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAIxD,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,KAAK,EAAE,OAAO,CAAU,CAAA;AAqBjE,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,GAAG,KAAK,EAAE,MAAM,EAAE,OAAO,EAAsB;IAC5H,OAAO,CACL,MAAC,iBAAiB,IAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,KAAK,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,aAC9F,KAAC,SAAS,IAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAC,KAAK,EAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,QAAC,OAAO,EAAC,QAAQ,EAAC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAI,EAC1H,KAAC,gBAAgB,IAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAC,UAAU,EAAC,KAAK,EAAC,eAAe,EAAC,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAI,IAClJ,CACrB,CAAA;AACH,CAAC","sourcesContent":["/**\n * F6: the phone call bar (server component). Call and quote always one tap away below 1024 px: it shows once the\n * hero's own call and quote buttons (`data-bl-call-path`) have scrolled out, and hides over the footer, the drawer, a\n * dialog, any call path and while a form field has focus. Call reports placement `fab`; the quote button sends\n * `lead_cta_click` with placement `floating`. Render it once, after the page's `<main>`.\n *\n * Both buttons stay on one line down to 360 px (3.15.0): the number never wraps or truncates; below 420 px the verb\n * (\"Call\") is visually hidden (the link's name keeps it) and the quote button shows `compactLabel` when given.\n *\n * `layout.show: 'always'` (3.16.0, as AA's live site): the bar shows from the first phone screen, beside the hero's\n * buttons, still Call before Quote (K6), and still hides over the footer, a dialog and a focused field.\n */\nimport { PhoneLink } from '../primitives/links.js'\nimport { ShellQuoteButton } from './shell-quote.js'\nimport type { SkinEntry } from '../skin.js'\nimport type { UiPhone } from '../types.js'\nimport { CallBarController } from './call-bar-client.js'\nimport type { ShellQuote } from './mobile-drawer.js'\n\nexport type MobileCallBarSlot = 'root' | 'call' | 'quote'\nexport const MOBILE_CALL_BAR_VARIANTS = ['bar', 'float'] as const\n/** `bar`: full width; `float`: two pills at the end. */\nexport type MobileCallBarVariant = (typeof MOBILE_CALL_BAR_VARIANTS)[number]\nexport interface MobileCallBarLayout {\n /** `after-call-path` (the default): once the hero's buttons scroll out; `always`: from the first phone screen. */\n readonly show?: 'after-call-path' | 'always'\n}\nexport type MobileCallBarSkin = SkinEntry<MobileCallBarSlot, MobileCallBarVariant, MobileCallBarLayout>\n\nexport interface MobileCallBarProps extends MobileCallBarSkin {\n readonly phone: UiPhone\n /** Words before the number, for example \"Call\". The button's name always includes the number. */\n readonly phoneLabel?: string | undefined\n readonly quote: ShellQuote\n /**\n * A shorter quote label for phones narrower than 420 px, for example \"Free quote\" for \"Get a Free Quote\". It must be\n * part of the quote label; the button's text and name stay the full label.\n */\n readonly compactLabel?: string | undefined\n}\n\nexport function MobileCallBar({ phone, phoneLabel, quote, compactLabel, variant = 'bar', layout, classes }: MobileCallBarProps) {\n return (\n <CallBarController variant={variant} always={layout?.show === 'always'} className={classes?.root}>\n <PhoneLink phone={phone} placement=\"fab\" label={phoneLabel} showIcon variant=\"button\" classes={{ root: classes?.call }} />\n <ShellQuoteButton label={quote.label} href={quote.href} placement=\"floating\" ctaId=\"quote-callbar\" compactLabel={compactLabel} classes={{ root: classes?.quote }} />\n </CallBarController>\n )\n}\n"]}
@@ -311,6 +311,43 @@
311
311
  opacity: 0.6;
312
312
  }
313
313
 
314
+ /* 07-motif.css */
315
+ /* 3.16.0 brand motif tokens: a client's own marks, available to every section. Each default draws nothing, and none
316
+ overrides a property a skin sets, so a site opts in by setting the token:
317
+ --bl-motif-rule-display: block a short rule above each section H2 (size, weight, colour, gap tokens below);
318
+ --bl-motif-dash-display: inline-block a dash before each eyebrow;
319
+ --bl-motif-divider: 2px solid … a border between neighbouring sections;
320
+ --bl-motif-texture: url(…) a background image on the page's band;
321
+ --bl-motif-media-clip: polygon(…) a corner shape for section photos. */
322
+
323
+ [data-bl-section] h2[data-slot='heading']::before {
324
+ content: '';
325
+ display: var(--bl-motif-rule-display, none);
326
+ inline-size: var(--bl-motif-rule-size, 3rem);
327
+ block-size: var(--bl-motif-rule-weight, 3px);
328
+ margin-block-end: var(--bl-motif-rule-gap, 0.75rem);
329
+ background: var(--bl-motif-rule-color, currentColor);
330
+ }
331
+ [data-bl-section] [data-slot='eyebrow']::before,
332
+ [data-bl='eyebrow']::before {
333
+ content: '';
334
+ display: var(--bl-motif-dash-display, none);
335
+ inline-size: var(--bl-motif-dash-size, 1.5rem);
336
+ block-size: var(--bl-motif-dash-weight, 2px);
337
+ margin-inline-end: var(--bl-motif-dash-gap, 0.5rem);
338
+ vertical-align: middle;
339
+ background: var(--bl-motif-dash-color, currentColor);
340
+ }
341
+ [data-bl-section] + [data-bl-section] {
342
+ border-block-start: var(--bl-motif-divider, none);
343
+ }
344
+ [data-bl-section][data-tone='band'] {
345
+ background-image: var(--bl-motif-texture, none);
346
+ }
347
+ [data-bl-section] [data-bl='media'] {
348
+ clip-path: var(--bl-motif-media-clip, none);
349
+ }
350
+
314
351
  /* 10-header.css */
315
352
  /*
316
353
  * F2 header and F3 panels: sticky and solid, modes by the header's own width, panels always in the HTML. The drawer's
@@ -1163,6 +1200,36 @@
1163
1200
  [data-bl-section='hero'] [data-slot='scrim'] {
1164
1201
  display: none;
1165
1202
  }
1203
+ /* 3.16.0: the form's heading block, the seal on the photo and the photo's own treatment (all from the skin's tokens). */
1204
+ [data-bl-section='hero'] [data-slot='formHead'] {
1205
+ display: grid;
1206
+ gap: 0.25rem;
1207
+ margin-block-end: 1rem;
1208
+ }
1209
+ [data-bl-section='hero'] [data-slot='formHead'] :where(h2) {
1210
+ margin: 0;
1211
+ }
1212
+ [data-bl-section='hero'] > [data-slot='inner'] > [data-slot='media'] {
1213
+ position: relative;
1214
+ }
1215
+ [data-bl-section='hero'] [data-slot='media'] > [data-bl='media'] {
1216
+ overflow: hidden;
1217
+ border-radius: var(--bl-hero-photo-radius, 0);
1218
+ box-shadow: var(--bl-hero-photo-shadow, none);
1219
+ }
1220
+ [data-bl-section='hero'] [data-slot='media'] > [data-bl='media'] > img {
1221
+ filter: var(--bl-hero-photo-filter, none);
1222
+ }
1223
+ [data-bl-section='hero'] [data-slot='seal'] {
1224
+ position: absolute;
1225
+ z-index: 1;
1226
+ inset-block-end: 1rem;
1227
+ inset-inline-start: 1rem;
1228
+ display: grid;
1229
+ justify-items: center;
1230
+ max-width: 10rem;
1231
+ text-align: center;
1232
+ }
1166
1233
  /* The root is the bl-hero container, and a container never matches its own query, so the full-bleed root is set up
1167
1234
  here: it holds the absolute photo and scrim from 1024 px. */
1168
1235
  [data-bl-section='hero'][data-variant='full-bleed'] {
@@ -1228,6 +1295,21 @@
1228
1295
  position: absolute;
1229
1296
  inset: 0;
1230
1297
  }
1298
+ /* 3.16.0: a scrim on any variant, coloured by --bl-hero-scrim; `column` covers the text side only. */
1299
+ [data-bl-section='hero'][data-scrim] [data-slot='scrim'] {
1300
+ display: block;
1301
+ position: absolute;
1302
+ inset: 0;
1303
+ background: var(--bl-hero-scrim, none);
1304
+ pointer-events: none;
1305
+ }
1306
+ [data-bl-section='hero'][data-variant='full-bleed'][data-scrim='column'] [data-slot='scrim'] {
1307
+ inset-inline-end: var(--bl-hero-scrim-column-end, 40%);
1308
+ }
1309
+ [data-bl-section='hero'][data-variant='full-bleed'] [data-slot='seal'] {
1310
+ inset-inline: auto 1.5rem;
1311
+ inset-block-end: 1.5rem;
1312
+ }
1231
1313
  }
1232
1314
 
1233
1315
  /* 30-reviews.css */
@@ -1240,11 +1322,23 @@
1240
1322
  gap: 0.35rem 0.5rem;
1241
1323
  margin: 0;
1242
1324
  }
1243
- [data-bl='rating-chip'] > [data-slot='source'] {
1325
+ [data-bl='rating-chip'] > :is(a[data-slot='source'], [data-slot='more']) {
1244
1326
  display: inline-flex;
1245
1327
  align-items: center;
1246
1328
  min-height: 24px;
1247
1329
  }
1330
+ /* 3.16.0 figure display: the average large (--bl-rating-figure-size), then the stars, the count and the link. */
1331
+ [data-bl='rating-chip'][data-display='figure'] {
1332
+ align-items: baseline;
1333
+ }
1334
+ [data-bl='rating-chip'] [data-slot='figure'] {
1335
+ font-family: var(--bl-font-display, inherit);
1336
+ font-size: var(--bl-rating-figure-size, 2.5rem);
1337
+ line-height: 1;
1338
+ }
1339
+ [data-bl='rating-chip'][data-display='figure'] > [data-slot='more'] {
1340
+ flex-basis: 100%;
1341
+ }
1248
1342
 
1249
1343
  [data-bl-section='reviews'] {
1250
1344
  container: bl-reviews / inline-size;
@@ -1370,6 +1464,12 @@
1370
1464
  container: bl-cta / inline-size;
1371
1465
  isolation: isolate;
1372
1466
  }
1467
+ /* 3.16.0: the closing band's own colour, so it never reads as the footer. It outranks the shared band tone
1468
+ (38-sequence.css), which loads later. */
1469
+ [data-bl-section='cta'][data-tone='band'][data-variant] {
1470
+ background-color: var(--bl-color-cta, var(--bl-color-band, transparent));
1471
+ color: var(--bl-color-on-cta, var(--bl-color-on-band, inherit));
1472
+ }
1373
1473
  [data-bl-section='cta'] :where(h2, p) {
1374
1474
  margin: 0;
1375
1475
  }
@@ -1411,6 +1511,23 @@
1411
1511
  gap: 2.5rem;
1412
1512
  }
1413
1513
 
1514
+ @container bl-cta (min-width: 768px) {
1515
+ [data-bl-section='cta'][data-arrangement='split'] [data-slot='content'] {
1516
+ grid-template-columns: minmax(0, 1fr) auto;
1517
+ align-items: center;
1518
+ column-gap: 3rem;
1519
+ }
1520
+ [data-bl-section='cta'][data-arrangement='split'] [data-slot='content'] > :not([data-slot='actions']) {
1521
+ grid-column: 1;
1522
+ }
1523
+ [data-bl-section='cta'][data-arrangement='split'] [data-slot='actions'] {
1524
+ grid-column: 2;
1525
+ grid-row: 1 / span 3;
1526
+ justify-content: flex-end;
1527
+ margin-top: 0;
1528
+ }
1529
+ }
1530
+
1414
1531
  @container bl-cta (min-width: 1024px) {
1415
1532
  [data-bl-section='cta'][data-variant='form'] > [data-slot='inner'] {
1416
1533
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
@@ -1718,12 +1835,30 @@
1718
1835
  align-items: stretch;
1719
1836
  gap: var(--bl-gutter, 1.25rem);
1720
1837
  }
1721
- [data-bl-section='proof-strip'] [data-slot='item'] {
1838
+ [data-bl-section='proof-strip'] :is([data-slot='item'], [data-slot='link']) {
1722
1839
  display: flex;
1723
1840
  flex-wrap: wrap;
1724
1841
  align-items: center;
1725
1842
  gap: 0.25rem 0.5rem;
1726
1843
  }
1844
+ [data-bl-section='proof-strip'] [data-slot='link'] {
1845
+ min-height: 24px;
1846
+ color: inherit;
1847
+ }
1848
+ /* 3.16.0 tiles: the value as a large figure over its label. */
1849
+ [data-bl-section='proof-strip'][data-variant='tiles'] :is([data-slot='item'], [data-slot='link']) {
1850
+ flex-direction: column;
1851
+ align-items: flex-start;
1852
+ justify-content: flex-start;
1853
+ }
1854
+ [data-bl-section='proof-strip'][data-variant='tiles'] [data-slot='link'] {
1855
+ flex: 1;
1856
+ }
1857
+ [data-bl-section='proof-strip'][data-variant='tiles'] [data-slot='value'] {
1858
+ font-family: var(--bl-font-display, inherit);
1859
+ font-size: var(--bl-proof-figure-size, 2.25rem);
1860
+ line-height: 1;
1861
+ }
1727
1862
  [data-bl-section='proof-strip'] [data-slot='icon'] svg {
1728
1863
  width: 1.5em;
1729
1864
  height: 1.5em;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandlab-digital/site-next",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  "typecheck": "tsc --noEmit -p tsconfig.json"
26
26
  },
27
27
  "dependencies": {
28
- "@brandlab-digital/site-core": "3.15.0",
28
+ "@brandlab-digital/site-core": "3.16.0",
29
29
  "browser-image-compression": "2.0.2",
30
30
  "pdf-lib": "1.17.1",
31
31
  "server-only": "0.0.1",